| package de.danoeh.antennapod.core.service; |
| |
| import android.app.Notification; |
| import android.app.NotificationManager; |
| import android.app.PendingIntent; |
| import android.app.Service; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.os.IBinder; |
| import android.support.v4.app.NotificationCompat; |
| import android.support.v4.util.ArrayMap; |
| import android.util.Log; |
| import android.util.Pair; |
| |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Map; |
| |
| import de.danoeh.antennapod.core.ClientConfig; |
| import de.danoeh.antennapod.core.R; |
| import de.danoeh.antennapod.core.feed.Feed; |
| import de.danoeh.antennapod.core.feed.FeedItem; |
| import de.danoeh.antennapod.core.feed.FeedMedia; |
| import de.danoeh.antennapod.core.gpoddernet.GpodnetService; |
| import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceAuthenticationException; |
| import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; |
| import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; |
| import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionGetResponse; |
| import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionPostResponse; |
| import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange; |
| import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse; |
| import de.danoeh.antennapod.core.preferences.GpodnetPreferences; |
| import de.danoeh.antennapod.core.storage.DBReader; |
| import de.danoeh.antennapod.core.storage.DBTasks; |
| import de.danoeh.antennapod.core.storage.DBWriter; |
| import de.danoeh.antennapod.core.storage.DownloadRequestException; |
| import de.danoeh.antennapod.core.storage.DownloadRequester; |
| import de.danoeh.antennapod.core.util.NetworkUtils; |
| |
| /** |
| * Synchronizes local subscriptions with gpodder.net service. The service should be started with 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"; |
| |
| public static final String ACTION_SYNC = "de.danoeh.antennapod.intent.action.sync"; |
| public static final String ACTION_SYNC_SUBSCRIPTIONS = "de.danoeh.antennapod.intent.action.sync_subscriptions"; |
| public static final String ACTION_SYNC_ACTIONS = "de.danoeh.antennapod.intent.action.sync_ACTIONS"; |
| |
| private GpodnetService service; |
| |
| private boolean syncSubscriptions = false; |
| private boolean syncActions = false; |
| |
| @Override |
| public int onStartCommand(Intent intent, int flags, int startId) { |
| final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null; |
| if (action != null) { |
| switch(action) { |
| case ACTION_SYNC: |
| syncSubscriptions = true; |
| syncActions = true; |
| break; |
| case ACTION_SYNC_SUBSCRIPTIONS: |
| syncSubscriptions = true; |
| break; |
| case ACTION_SYNC_ACTIONS: |
| syncActions = true; |
| break; |
| default: |
| Log.e(TAG, "Received invalid intent: action argument is invalid"); |
| } |
| if(syncSubscriptions || syncActions) { |
| Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL)); |
| syncWaiterThread.restart(); |
| } |
| } else { |
| Log.e(TAG, "Received invalid intent: action argument is null"); |
| } |
| return START_FLAG_REDELIVERY; |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| Log.d(TAG, "onDestroy"); |
| syncWaiterThread.interrupt(); |
| } |
| |
| @Override |
| public IBinder onBind(Intent intent) { |
| return null; |
| } |
| |
| private synchronized GpodnetService tryLogin() throws GpodnetServiceException { |
| if (service == null) { |
| service = new GpodnetService(); |
| service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); |
| } |
| return service; |
| } |
| |
| |
| private synchronized void sync() { |
| if (GpodnetPreferences.loggedIn() == false || NetworkUtils.networkAvailable() == false) { |
| stopSelf(); |
| return; |
| } |
| if(syncSubscriptions) { |
| syncSubscriptionChanges(); |
| syncSubscriptions = false; |
| } |
| if(syncActions) { |
| syncEpisodeActions(); |
| syncActions = false; |
| } |
| stopSelf(); |
| } |
| |
| private synchronized void syncSubscriptionChanges() { |
| final long timestamp = GpodnetPreferences.getLastSubscriptionSyncTimestamp(); |
| try { |
| final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(); |
| Collection<String> localAdded = GpodnetPreferences.getAddedFeedsCopy(); |
| Collection<String> localRemoved = GpodnetPreferences.getRemovedFeedsCopy(); |
| GpodnetService service = tryLogin(); |
| |
| // first sync: download all subscriptions... |
| GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(), |
| GpodnetPreferences.getDeviceID(), timestamp); |
| long newTimeStamp = subscriptionChanges.getTimestamp(); |
| |
| Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges); |
| processSubscriptionChanges(localSubscriptions, localAdded, localRemoved, subscriptionChanges); |
| |
| if(timestamp == 0) { |
| // this is this apps first sync with gpodder: |
| // only submit changes gpodder has not just sent us |
| localAdded = localSubscriptions; |
| localAdded.removeAll(subscriptionChanges.getAdded()); |
| localRemoved.removeAll(subscriptionChanges.getRemoved()); |
| } |
| if(localAdded.size() > 0 || localRemoved.size() > 0) { |
| Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s", |
| localAdded, localRemoved)); |
| GpodnetUploadChangesResponse uploadResponse = service.uploadChanges(GpodnetPreferences.getUsername(), |
| GpodnetPreferences.getDeviceID(), localAdded, localRemoved); |
| newTimeStamp = uploadResponse.timestamp; |
| Log.d(TAG, "Upload changes response: " + uploadResponse); |
| GpodnetPreferences.removeAddedFeeds(localAdded); |
| GpodnetPreferences.removeRemovedFeeds(localRemoved); |
| } |
| GpodnetPreferences.setLastSubscriptionSyncTimestamp(newTimeStamp); |
| clearErrorNotifications(); |
| } catch (GpodnetServiceException e) { |
| e.printStackTrace(); |
| updateErrorNotification(e); |
| } catch (DownloadRequestException e) { |
| e.printStackTrace(); |
| } |
| } |
| |
| private synchronized void processSubscriptionChanges(List<String> localSubscriptions, |
| Collection<String> localAdded, |
| Collection<String> localRemoved, |
| GpodnetSubscriptionChange changes) throws DownloadRequestException { |
| // local changes are always superior to remote changes! |
| // add subscription if (1) not already subscribed and (2) not just unsubscribed |
| for (String downloadUrl : changes.getAdded()) { |
| if (false == localSubscriptions.contains(downloadUrl) && |
| false == localRemoved.contains(downloadUrl)) { |
| Feed feed = new Feed(downloadUrl, null); |
| DownloadRequester.getInstance().downloadFeed(this, feed); |
| } |
| } |
| // remove subscription if not just subscribed (again) |
| for (String downloadUrl : changes.getRemoved()) { |
| if(false == localAdded.contains(downloadUrl)) { |
| DBTasks.removeFeedWithDownloadUrl(GpodnetSyncService.this, downloadUrl); |
| } |
| } |
| } |
| |
| private synchronized void syncEpisodeActions() { |
| final long timestamp = GpodnetPreferences.getLastEpisodeActionsSyncTimestamp(); |
| Log.d(TAG, "last episode actions sync timestamp: " + timestamp); |
| try { |
| GpodnetService service = tryLogin(); |
| |
| // download episode actions |
| GpodnetEpisodeActionGetResponse getResponse = service.getEpisodeChanges(timestamp); |
| long lastUpdate = getResponse.getTimestamp(); |
| Log.d(TAG, "Downloaded episode actions: " + getResponse); |
| List<GpodnetEpisodeAction> remoteActions = getResponse.getEpisodeActions(); |
| |
| List<GpodnetEpisodeAction> localActions = GpodnetPreferences.getQueuedEpisodeActions(); |
| processEpisodeActions(localActions, remoteActions); |
| |
| // upload local actions |
| if(localActions.size() > 0) { |
| Log.d(TAG, "Uploading episode actions: " + localActions); |
| GpodnetEpisodeActionPostResponse postResponse = service.uploadEpisodeActions(localActions); |
| lastUpdate = postResponse.timestamp; |
| Log.d(TAG, "Upload episode response: " + postResponse); |
| GpodnetPreferences.removeQueuedEpisodeActions(localActions); |
| } |
| GpodnetPreferences.setLastEpisodeActionsSyncTimestamp(lastUpdate); |
| clearErrorNotifications(); |
| } catch (GpodnetServiceException e) { |
| e.printStackTrace(); |
| updateErrorNotification(e); |
| } catch (DownloadRequestException e) { |
| e.printStackTrace(); |
| } |
| } |
| |
| |
| private synchronized void processEpisodeActions(List<GpodnetEpisodeAction> localActions, |
| List<GpodnetEpisodeAction> remoteActions) throws DownloadRequestException { |
| if(remoteActions.size() == 0) { |
| return; |
| } |
| Map<Pair<String, String>, GpodnetEpisodeAction> localMostRecentPlayAction = new ArrayMap<>(); |
| for(GpodnetEpisodeAction action : localActions) { |
| Pair key = new Pair(action.getPodcast(), action.getEpisode()); |
| GpodnetEpisodeAction mostRecent = localMostRecentPlayAction.get(key); |
| if (mostRecent == null || mostRecent.getTimestamp() == null) { |
| localMostRecentPlayAction.put(key, action); |
| } else if (mostRecent.getTimestamp().before(action.getTimestamp())) { |
| localMostRecentPlayAction.put(key, action); |
| } |
| } |
| |
| // make sure more recent local actions are not overwritten by older remote actions |
| Map<Pair<String, String>, GpodnetEpisodeAction> mostRecentPlayAction = new ArrayMap<>(); |
| for (GpodnetEpisodeAction action : remoteActions) { |
| switch (action.getAction()) { |
| case NEW: |
| FeedItem newItem = DBReader.getFeedItem(action.getPodcast(), action.getEpisode()); |
| if(newItem != null) { |
| DBWriter.markItemPlayed(newItem, FeedItem.UNPLAYED, true); |
| } else { |
| Log.i(TAG, "Unknown feed item: " + action); |
| } |
| break; |
| case DOWNLOAD: |
| break; |
| case PLAY: |
| Pair key = new Pair(action.getPodcast(), action.getEpisode()); |
| GpodnetEpisodeAction localMostRecent = localMostRecentPlayAction.get(key); |
| if(localMostRecent == null || |
| localMostRecent.getTimestamp() == null || |
| localMostRecent.getTimestamp().before(action.getTimestamp())) { |
| GpodnetEpisodeAction mostRecent = mostRecentPlayAction.get(key); |
| if (mostRecent == null || mostRecent.getTimestamp() == null) { |
| mostRecentPlayAction.put(key, action); |
| } else if (action.getTimestamp() != null && mostRecent.getTimestamp().before(action.getTimestamp())) { |
| mostRecentPlayAction.put(key, action); |
| } else { |
| Log.d(TAG, "No date information in action, skipping it"); |
| } |
| } |
| break; |
| case DELETE: |
| // NEVER EVER call DBWriter.deleteFeedMediaOfItem() here, leads to an infinite loop |
| break; |
| } |
| } |
| for (GpodnetEpisodeAction action : mostRecentPlayAction.values()) { |
| FeedItem playItem = DBReader.getFeedItem(action.getPodcast(), action.getEpisode()); |
| if (playItem != null) { |
| FeedMedia media = playItem.getMedia(); |
| media.setPosition(action.getPosition() * 1000); |
| DBWriter.setFeedMedia(media); |
| if(playItem.getMedia().hasAlmostEnded()) { |
| DBWriter.markItemPlayed(playItem, FeedItem.PLAYED, true); |
| DBWriter.addItemToPlaybackHistory(playItem.getMedia()); |
| } |
| } |
| } |
| } |
| |
| private void clearErrorNotifications() { |
| NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); |
| nm.cancel(R.id.notification_gpodnet_sync_error); |
| nm.cancel(R.id.notification_gpodnet_sync_autherror); |
| } |
| |
| private void updateErrorNotification(GpodnetServiceException exception) { |
| 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; |
| } |
| |
| PendingIntent activityIntent = ClientConfig.gpodnetCallbacks.getGpodnetSyncServiceErrorNotificationPendingIntent(this); |
| Notification notification = builder.setContentTitle(title) |
| .setContentText(description) |
| .setContentIntent(activityIntent) |
| .setSmallIcon(R.drawable.stat_notify_sync_error) |
| .setAutoCancel(true) |
| .build(); |
| NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); |
| nm.notify(id, notification); |
| } |
| |
| private WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) { |
| @Override |
| public void onWaitCompleted() { |
| sync(); |
| } |
| }; |
| |
| 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 sendSyncIntent(Context context) { |
| if (GpodnetPreferences.loggedIn()) { |
| Intent intent = new Intent(context, GpodnetSyncService.class); |
| intent.putExtra(ARG_ACTION, ACTION_SYNC); |
| context.startService(intent); |
| } |
| } |
| |
| public static void sendSyncSubscriptionsIntent(Context context) { |
| if (GpodnetPreferences.loggedIn()) { |
| Intent intent = new Intent(context, GpodnetSyncService.class); |
| intent.putExtra(ARG_ACTION, ACTION_SYNC_SUBSCRIPTIONS); |
| context.startService(intent); |
| } |
| } |
| |
| public static void sendSyncActionsIntent(Context context) { |
| if (GpodnetPreferences.loggedIn()) { |
| Intent intent = new Intent(context, GpodnetSyncService.class); |
| intent.putExtra(ARG_ACTION, ACTION_SYNC_ACTIONS); |
| context.startService(intent); |
| } |
| } |
| } |