Added subscription syncing

This commit is contained in:
daniel oeh 2013-09-01 13:49:19 +02:00
parent e29d117942
commit c5f848ead5
13 changed files with 414 additions and 2 deletions

View File

@ -143,6 +143,11 @@
android:enabled="true">
</service>
<service
android:name=".service.GpodnetSyncService"
android:enabled="true">
</service>
<activity
android:name=".activity.PreferenceActivity"
android:configChanges="keyboardHidden|orientation"
@ -377,6 +382,7 @@
</intent-filter>
</activity>
<receiver android:name=".receiver.ConnectivityActionReceiver">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>

View File

@ -17,5 +17,7 @@
<item name="skip_episode_item" type="id"/>
<item name="image_disk_cache_key" type="id"/>
<item name="imageloader_key" type="id"/>
<item name="notification_gpodnet_sync_error" type="id"/>
<item name="notification_gpodnet_sync_autherror" type="id"/>
</resources>

View File

@ -195,6 +195,8 @@
<string name="pref_update_interval_hours_manual">Manual</string>
<string name="pref_gpodnet_authenticate_title">Login</string>
<string name="pref_gpodnet_authenticate_sum">Login with your gpodder.net account in order to sync your subscriptions.</string>
<string name="pref_gpodnet_logout_title">Logout</string>
<string name="pref_gpodnet_logout_toast">Logout was successful</string>
<!-- Search -->
@ -268,6 +270,11 @@
<string name="gpodnetauth_finish_butsyncnow">Start sync now</string>
<string name="gpodnetauth_finish_butgomainscreen">Go to main screen</string>
<string name="gpodnetsync_auth_error_title">gpodder.net authentication error</string>
<string name="gpodnetsync_auth_error_descr">Wrong username or password</string>
<string name="gpodnetsync_error_title">gpodder.net sync error</string>
<string name="gpodnetsync_error_descr">An error occurred during syncing:\u0020</string>
<!-- Directory chooser -->
<string name="selected_folder_label">Selected folder:</string>
<string name="create_folder_label">Create folder</string>

View File

@ -92,6 +92,9 @@
android:summary="@string/pref_gpodnet_authenticate_sum">
<intent android:action=".activity.gpoddernet.GpodnetAuthenticationActivity"/>
</PreferenceScreen>
<Preference
android:key="pref_gpodnet_logout"
android:title="@string/pref_gpodnet_logout_title"/>
</PreferenceScreen>
</PreferenceCategory>

View File

@ -21,10 +21,12 @@ import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.asynctask.FlattrClickWorker;
import de.danoeh.antennapod.asynctask.OpmlExportWorker;
import de.danoeh.antennapod.preferences.GpodnetPreferences;
import de.danoeh.antennapod.preferences.UserPreferences;
import de.danoeh.antennapod.util.flattr.FlattrUtils;
@ -41,6 +43,8 @@ public class PreferenceActivity extends android.preference.PreferenceActivity {
private static final String PREF_ABOUT = "prefAbout";
private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir";
private static final String AUTO_DL_PREF_SCREEN = "prefAutoDownloadSettings";
private static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate";
private static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout";
private CheckBoxPreference[] selectedNetworks;
@ -156,11 +160,29 @@ public class PreferenceActivity extends android.preference.PreferenceActivity {
return true;
}
});
findPreference(PREF_GPODNET_LOGOUT).setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
GpodnetPreferences.logout();
Toast toast = Toast.makeText(PreferenceActivity.this, R.string.pref_gpodnet_logout_toast, Toast.LENGTH_SHORT);
toast.show();
updateGpodnetPreferenceScreen();
return true;
}
});
updateGpodnetPreferenceScreen();
buildUpdateIntervalPreference();
buildAutodownloadSelectedNetworsPreference();
setSelectedNetworksEnabled(UserPreferences
.isEnableAutodownloadWifiFilter());
}
private void updateGpodnetPreferenceScreen() {
final boolean loggedIn = GpodnetPreferences.loggedIn();
findPreference(PREF_GPODNET_LOGIN).setEnabled(!loggedIn);
findPreference(PREF_GPODNET_LOGOUT).setEnabled(loggedIn);
}
private void buildUpdateIntervalPreference() {

View File

@ -17,6 +17,7 @@ import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.gpoddernet.model.GpodnetDevice;
import de.danoeh.antennapod.preferences.GpodnetPreferences;
import de.danoeh.antennapod.preferences.UserPreferences;
import de.danoeh.antennapod.service.GpodnetSyncService;
import java.util.ArrayList;
import java.util.List;
@ -280,13 +281,19 @@ public class GpodnetAuthenticationActivity extends ActionBarActivity {
final Button sync = (Button) view.findViewById(R.id.butSyncNow);
final Button back = (Button) view.findViewById(R.id.butGoMainscreen);
sync.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GpodnetSyncService.sendSyncIntent(GpodnetAuthenticationActivity.this);
finish();
}
});
back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(GpodnetAuthenticationActivity.this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
});
}

View File

@ -16,10 +16,17 @@ public class GpodnetPreferences {
public static final String PREF_GPODNET_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password";
public static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID";
public static final String PREF_LAST_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp";
private static String username;
private static String password;
private static String deviceID;
/**
* Last value returned by getSubscriptionChanges call. Will be used for all subsequent calls of getSubscriptionChanges.
*/
private static long lastSyncTimestamp;
private static boolean preferencesLoaded = false;
private static SharedPreferences getPreferences() {
@ -32,7 +39,7 @@ public class GpodnetPreferences {
username = prefs.getString(PREF_GPODNET_USERNAME, null);
password = prefs.getString(PREF_GPODNET_PASSWORD, null);
deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null);
lastSyncTimestamp = prefs.getLong(PREF_LAST_SYNC_TIMESTAMP, 0);
preferencesLoaded = true;
}
}
@ -43,6 +50,11 @@ public class GpodnetPreferences {
editor.commit();
}
private static void writePreference(String key, long value) {
SharedPreferences.Editor editor = getPreferences().edit();
editor.putLong(key, value);
editor.commit();
}
public static String getUsername() {
ensurePreferencesLoaded();
@ -73,4 +85,29 @@ public class GpodnetPreferences {
GpodnetPreferences.deviceID = deviceID;
writePreference(PREF_GPODNET_DEVICEID, deviceID);
}
public static long getLastSyncTimestamp() {
ensurePreferencesLoaded();
return lastSyncTimestamp;
}
public static void setLastSyncTimestamp(long lastSyncTimestamp) {
GpodnetPreferences.lastSyncTimestamp = lastSyncTimestamp;
writePreference(PREF_LAST_SYNC_TIMESTAMP, lastSyncTimestamp);
}
/**
* Returns true if device ID, username and password have a non-null value
*/
public static boolean loggedIn() {
ensurePreferencesLoaded();
return deviceID != null && username != null && password != null;
}
public static void logout() {
setUsername(null);
setPassword(null);
setDeviceID(null);
setLastSyncTimestamp(0);
}
}

View File

@ -0,0 +1,257 @@
package de.danoeh.antennapod.service;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.feed.Feed;
import de.danoeh.antennapod.gpoddernet.GpodnetService;
import de.danoeh.antennapod.gpoddernet.GpodnetServiceAuthenticationException;
import de.danoeh.antennapod.gpoddernet.GpodnetServiceException;
import de.danoeh.antennapod.gpoddernet.model.GpodnetSubscriptionChange;
import de.danoeh.antennapod.preferences.GpodnetPreferences;
import de.danoeh.antennapod.storage.DBReader;
import de.danoeh.antennapod.storage.DBTasks;
import de.danoeh.antennapod.storage.DownloadRequestException;
import de.danoeh.antennapod.storage.DownloadRequester;
import de.danoeh.antennapod.util.NetworkUtils;
import java.util.Date;
import java.util.List;
/**
* Synchronizes local subscriptions with gpodder.net service. The service should be started with an ACTION_UPLOAD_CHANGES,
* ACTION_DOWNLOAD_CHANGES or ACTION_SYNC as an action argument. This class also provides static methods for starting the GpodnetSyncService.
*/
public class GpodnetSyncService extends Service {
private static final String TAG = "GpodnetSyncService";
private static final long WAIT_INTERVAL = 5000L;
public static final String ARG_ACTION = "action";
/**
* Starts a new upload action. The service will not upload immediately, but wait for a certain amount of time in
* case any other upload requests occur.
*/
public static final String ACTION_UPLOAD_CHANGES = "de.danoeh.antennapod.intent.action.upload_changes";
/**
* Starts a new download action. The service will download all changes in the subscription list since the last sync.
* New subscriptions will be added to the database, removed subscriptions will be removed from the database
*/
public static final String ACTION_DOWNLOAD_CHANGES = "de.danoeh.antennapod.intent.action.download_changes";
/**
* Starts a new upload action immediately and a new download action after that.
*/
public static final String ACTION_SYNC = "de.danoeh.antennapod.intent.action.sync";
private GpodnetService service;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null;
if (action != null && action.equals(ACTION_UPLOAD_CHANGES)) {
Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL));
uploadWaiterThread.restart();
} else if (action != null && action.equals(ACTION_DOWNLOAD_CHANGES)) {
new Thread() {
@Override
public void run() {
downloadChanges();
}
}.start();
} else if (action != null && action.equals(ACTION_SYNC)) {
new Thread() {
@Override
public void run() {
uploadChanges();
downloadChanges();
}
}.start();
}
return START_FLAG_REDELIVERY;
}
@Override
public void onDestroy() {
super.onDestroy();
if (AppConfig.DEBUG) Log.d(TAG, "onDestroy");
uploadWaiterThread.interrupt();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private GpodnetService tryLogin() throws GpodnetServiceException {
if (service == null) {
service = new GpodnetService();
service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
}
return service;
}
private synchronized void downloadChanges() {
if (GpodnetPreferences.loggedIn() && NetworkUtils.networkAvailable(GpodnetSyncService.this)) {
if (AppConfig.DEBUG) Log.d(TAG, "Downloading changes");
try {
GpodnetService service = tryLogin();
GpodnetSubscriptionChange changes = service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), GpodnetPreferences.getLastSyncTimestamp());
if (AppConfig.DEBUG) Log.d(TAG, "Changes " + changes.toString());
GpodnetPreferences.setLastSyncTimestamp(changes.getTimestamp());
List<String> subscriptionList = DBReader.getFeedListDownloadUrls(GpodnetSyncService.this);
for (String downloadUrl : changes.getAdded()) {
if (!subscriptionList.contains(downloadUrl)) {
Feed feed = new Feed(downloadUrl, new Date());
DownloadRequester.getInstance().downloadFeed(GpodnetSyncService.this, feed);
}
}
for (String downloadUrl : changes.getRemoved()) {
DBTasks.removeFeedWithDownloadUrl(GpodnetSyncService.this, downloadUrl);
}
} catch (GpodnetServiceException e) {
e.printStackTrace();
updateErrorNotification(e);
} catch (DownloadRequestException e) {
e.printStackTrace();
}
}
stopSelf();
}
private synchronized void uploadChanges() {
if (GpodnetPreferences.loggedIn() && NetworkUtils.networkAvailable(GpodnetSyncService.this)) {
try {
if (AppConfig.DEBUG) Log.d(TAG, "Uploading subscription list");
GpodnetService service = tryLogin();
List<String> subscriptions = DBReader.getFeedListDownloadUrls(GpodnetSyncService.this);
if (AppConfig.DEBUG) Log.d(TAG, "Uploading subscriptions: " + subscriptions.toString());
service.uploadSubscriptions(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), subscriptions);
} catch (GpodnetServiceException e) {
e.printStackTrace();
updateErrorNotification(e);
}
}
stopSelf();
}
private void updateErrorNotification(GpodnetServiceException exception) {
if (AppConfig.DEBUG) Log.d(TAG, "Posting error notification");
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
final String title;
final String description;
final int id;
if (exception instanceof GpodnetServiceAuthenticationException) {
title = getString(R.string.gpodnetsync_auth_error_title);
description = getString(R.string.gpodnetsync_auth_error_descr);
id = R.id.notification_gpodnet_sync_autherror;
} else {
title = getString(R.string.gpodnetsync_error_title);
description = getString(R.string.gpodnetsync_error_descr) + exception.getMessage();
id = R.id.notification_gpodnet_sync_error;
}
Notification notification = builder.setContentTitle(title)
.setContentText(description)
.setSmallIcon(R.drawable.stat_notify_sync_error)
.setAutoCancel(true)
.build();
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(id, notification);
}
private WaiterThread uploadWaiterThread = new WaiterThread(WAIT_INTERVAL) {
@Override
public void onWaitCompleted() {
uploadChanges();
}
};
private abstract class WaiterThread {
private long waitInterval;
private Thread thread;
private WaiterThread(long waitInterval) {
this.waitInterval = waitInterval;
reinit();
}
public abstract void onWaitCompleted();
public void exec() {
if (!thread.isAlive()) {
thread.start();
}
}
private void reinit() {
if (thread != null && thread.isAlive()) {
Log.d(TAG, "Interrupting waiter thread");
thread.interrupt();
}
thread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(waitInterval);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!isInterrupted()) {
synchronized (this) {
onWaitCompleted();
}
}
}
};
}
public void restart() {
reinit();
exec();
}
public void interrupt() {
if (thread != null && thread.isAlive()) {
thread.interrupt();
}
}
}
public static void sendActionDownloadIntent(Context context) {
if (GpodnetPreferences.loggedIn()) {
Intent intent = new Intent(context, GpodnetSyncService.class);
intent.putExtra(ARG_ACTION, ACTION_DOWNLOAD_CHANGES);
context.startService(intent);
}
}
public static void sendActionUploadIntent(Context context) {
if (GpodnetPreferences.loggedIn()) {
Intent intent = new Intent(context, GpodnetSyncService.class);
intent.putExtra(ARG_ACTION, ACTION_UPLOAD_CHANGES);
context.startService(intent);
}
}
public static void sendSyncIntent(Context context) {
if (GpodnetPreferences.loggedIn()) {
Intent intent = new Intent(context, GpodnetSyncService.class);
intent.putExtra(ARG_ACTION, ACTION_SYNC);
context.startService(intent);
}
}
}

View File

@ -75,6 +75,27 @@ public final class DBReader {
return feeds;
}
/**
* Returns a list with the download URLs of all feeds.
* @param context A context that is used for opening the database connection.
* @return A list of Strings with the download URLs of all feeds.
* */
public static List<String> getFeedListDownloadUrls(final Context context) {
PodDBAdapter adapter = new PodDBAdapter(context);
List<String> result = new ArrayList<String>();
adapter.open();
Cursor feeds = adapter.getFeedCursorDownloadUrls();
if (feeds.moveToFirst()) {
do {
result.add(feeds.getString(1));
} while (feeds.moveToNext());
}
feeds.close();
adapter.close();
return result;
}
/**
* Returns a list of 'expired Feeds', i.e. Feeds that have not been updated for a certain amount of time.
*

View File

@ -23,6 +23,7 @@ import de.danoeh.antennapod.feed.FeedImage;
import de.danoeh.antennapod.feed.FeedItem;
import de.danoeh.antennapod.feed.FeedMedia;
import de.danoeh.antennapod.preferences.UserPreferences;
import de.danoeh.antennapod.service.GpodnetSyncService;
import de.danoeh.antennapod.service.PlaybackService;
import de.danoeh.antennapod.service.download.DownloadStatus;
import de.danoeh.antennapod.util.DownloadError;
@ -39,6 +40,39 @@ public final class DBTasks {
private DBTasks() {
}
/**
* Removes the feed with the given download url. This method should NOT be executed on the GUI thread.
* @param context Used for accessing the db
* @param downloadUrl URL of the feed.
* */
public static void removeFeedWithDownloadUrl(Context context, String downloadUrl) {
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
Cursor cursor = adapter.getFeedCursorDownloadUrls();
long feedID = 0;
if (cursor.moveToFirst()) {
do {
if (cursor.getString(1).equals(downloadUrl)) {
feedID = cursor.getLong(0);
}
} while (cursor.moveToNext());
}
cursor.close();
adapter.close();
if (feedID != 0) {
try {
DBWriter.deleteFeed(context, feedID).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
} else {
Log.w(TAG, "removeFeedWithDownloadUrl: Could not find feed with url: " + downloadUrl);
}
}
/**
* Starts playback of a FeedMedia object's file. This method will build an Intent based on the given parameters to
* start the {@link PlaybackService}.
@ -110,6 +144,8 @@ public final class DBTasks {
refreshFeeds(context, DBReader.getFeedList(context));
}
isRefreshing.set(false);
GpodnetSyncService.sendSyncIntent(context);
}
}.start();
} else {

View File

@ -18,6 +18,7 @@ import android.util.Log;
import de.danoeh.antennapod.AppConfig;
import de.danoeh.antennapod.feed.*;
import de.danoeh.antennapod.preferences.PlaybackPreferences;
import de.danoeh.antennapod.service.GpodnetSyncService;
import de.danoeh.antennapod.service.PlaybackService;
import de.danoeh.antennapod.service.download.DownloadStatus;
import de.danoeh.antennapod.util.QueueAccess;
@ -171,6 +172,8 @@ public class DBWriter {
}
adapter.removeFeed(feed);
adapter.close();
GpodnetSyncService.sendActionUploadIntent(context);
EventDistributor.getInstance().sendFeedUpdateBroadcast();
}
}
@ -614,6 +617,7 @@ public class DBWriter {
adapter.setCompleteFeed(feed);
adapter.close();
GpodnetSyncService.sendActionUploadIntent(context);
EventDistributor.getInstance().sendFeedUpdateBroadcast();
}
});

View File

@ -659,6 +659,10 @@ public class PodDBAdapter {
return c;
}
public final Cursor getFeedCursorDownloadUrls() {
return db.query(TABLE_NAME_FEEDS, new String[]{KEY_ID, KEY_DOWNLOAD_URL}, null, null, null, null, null);
}
public final Cursor getExpiredFeedsCursor(long expirationTime) {
Cursor c = db.query(TABLE_NAME_FEEDS, null, "?<?", new String[]{
KEY_LASTUPDATE, String.valueOf(System.currentTimeMillis() - expirationTime)}, null, null,

View File

@ -60,4 +60,10 @@ public class NetworkUtils {
Log.d(TAG, "Network for auto-dl is not available");
return false;
}
public static boolean networkAvailable(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getActiveNetworkInfo();
return info != null && info.isConnected();
}
}