blob: 0b90cef6cfdade31d1b23748075cff2f731aa74d [file] [log] [blame]
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, new Date(0));
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);
}
}
}