package com.gettingmobile.goodnews.sync; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.IBinder; import android.os.PowerManager; import android.util.Log; import com.gettingmobile.android.app.*; import com.gettingmobile.goodnews.Application; import com.gettingmobile.goodnews.R; import com.gettingmobile.goodnews.account.LoginCallback; import com.gettingmobile.goodnews.download.ContentDownloadService; import com.gettingmobile.goodnews.download.OfflineStrategy; import com.gettingmobile.goodnews.home.HomeActivity; import com.gettingmobile.google.reader.sync.SyncCallback; import com.gettingmobile.google.reader.sync.SyncException; import com.google.inject.Inject; public final class SyncService extends ForegroundService implements SyncCallback { private static final String LOG_TAG = "goodnews.SyncService"; private static final String ACTION_BASE = "com.gettingmobile.goodnews.action."; public static final String ACTION_SCHEDULE_SYNCS = ACTION_BASE + "SCHEDULE_SYNCS"; public static final String ACTION_SYNC_FULL = ACTION_BASE + "SYNC_FULL"; public static final String ACTION_SYNC_PUSH = ACTION_BASE + "SYNC_PUSH"; public static final String INTENT_EXTRA_AUTOMATION = ACTION_BASE + ".AUTOMATION"; public static final long IMMEDIATE_PUSH_DELAY = 5 * 1000; public static final long FAILED_DELAY = 60 * 1000; private static final int NOTIFICATION_ID = R.drawable.notify_sync; private static final int NOTIFICATION_ERROR = R.drawable.notify_sync_error; private static final int NOTIFICATION_PULL_SUCCESS = R.drawable.notify_sync_finished; private static final int STATE_IDLE = 0; private static final int STATE_PUSH_SYNCING = 1; private static final int STATE_FULL_SYNCING = 2; private boolean startedByIntent = false; @Inject private SyncServiceProxy serviceProxy = null; private final ServiceBinder<SyncService> binder = new ServiceBinder<SyncService>(this); private SyncThread syncThread = null; private PowerManager.WakeLock wakeLock = null; private int state = STATE_IDLE; private ProgressNotificationHelper progressNotificationHelper = null; private SimpleNotification syncErrorNotification = null; private final OnContinousSyncIntervalSettingChangeListener continousSyncIntervalSettingChangeListener = new OnContinousSyncIntervalSettingChangeListener(this); /* * public interface */ public boolean isIdle() { return state == STATE_IDLE; } public boolean isPushSyncing() { return state == STATE_PUSH_SYNCING; } public boolean isFullSyncing() { return state == STATE_FULL_SYNCING; } public boolean isSyncing() { return isFullSyncing() || isPushSyncing(); } public boolean isPushRequired() { return syncThread != null && syncThread.getSynchronizer().isPushRequired(); } /** * Starts a full sync. * @throws IllegalStateException if the service is not in the idle state. */ public void startFullSync() throws IllegalStateException { startSync(STATE_FULL_SYNCING, new SyncInvocator() { @Override protected void doSync() { syncThread.startFullSync(); } }); } public boolean scheduleContinousFullSyncIfApplicable() { Log.d(LOG_TAG, "Scheduling continous sync"); return scheduleSync(getApp().getSettings().getNextContinousSyncTimestampInMillis(), ACTION_SYNC_FULL); } /** * Starts a push sync. * @throws IllegalStateException if the service is not in the idle state. */ public void startPushSync() throws IllegalStateException { startSync(STATE_PUSH_SYNCING, new SyncInvocator() { @Override protected void doSync() { syncThread.startPushSync(); } }); } /** * Schedules a push sync if the immediate push option is active and the service is in the idle state or does nothing * otherwise. */ public void scheduleImmediatePushSyncIfApplicable() { if (isIdle() && getApp().getSettings().getPushImmediately() && isPushRequired()) { scheduleImmediatePushSync(); } } public boolean scheduleImmediatePushSync() { Log.d(LOG_TAG, "Scheduling immediate push sync"); return scheduleSync(System.currentTimeMillis() + IMMEDIATE_PUSH_DELAY, ACTION_SYNC_PUSH); } public void postProcessSyncIfRequired() { clearSuccess(); } public void cancelSync() { if (syncThread != null) { syncThread.getSynchronizer().cancel(); } } /* * intent handling */ public static Intent createStartIntent(Context context, String action, boolean automation) { final Intent intent = new Intent(context, SyncService.class); intent.setAction(action); intent.putExtra(INTENT_EXTRA_AUTOMATION, automation); return intent; } public static Intent createStartIntent(Context context, String action) { return createStartIntent(context, action, false); } protected boolean scheduleSync(long sysTimeInMillis, String action) { final AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE); final PendingIntent pi = PendingIntent.getBroadcast(this, 0, new Intent(action), 0); if (sysTimeInMillis > 0) { am.set(AlarmManager.RTC_WAKEUP, Math.max(sysTimeInMillis, getApp().getSettings().getLastFailedSyncTimestampInMillis() + FAILED_DELAY), pi); return true; } else { am.cancel(pi); return false; } } @Override public void onStart(Intent intent, int startId) { onStartCommand(intent, 0, startId); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(LOG_TAG, "received sync request by intent"); /* * is there already a pending intent? */ if (startedByIntent) { return 0; } /* * are we able to run? */ if (isSyncing()) { stopSelf(); return 0; } /* * schedule syncs if applicable (e.g. after boot or network connection available again) */ if (ACTION_SCHEDULE_SYNCS.equals(intent.getAction())) { if (!scheduleContinousFullSyncIfApplicable()) { scheduleImmediatePushSyncIfApplicable(); } stopSelf(); return 0; } /* * postpone sync if internet isn't available */ final boolean fullSync = ACTION_SYNC_FULL.equals(intent.getAction()); if (!getApp().isInternetAvailable()) { stopSelf(); return 0; } /* * are we allowed to sync (maybe Wifi is required for continous sync?) */ if (!intent.getBooleanExtra(INTENT_EXTRA_AUTOMATION, false) && getApp().getSettings().requiresWifiForContinousSync() && !getApp().isWifiConnected()) { stopSelf(); return 0; } /* * start */ startedByIntent = true; try { if (fullSync) { Log.d(LOG_TAG, "starting full sync triggered by intent"); startFullSync(); } else { Log.d(LOG_TAG, "starting push sync triggered by intent"); if (!isPushRequired()) { stopSelf(); return 0; } startPushSync(); } } catch (Throwable error) { Log.e(LOG_TAG, "An error occured while trying to sync triggered by intent.", error); startedByIntent = false; scheduleSyncIfApplicable(fullSync); stopSelf(); return 0; } return START_REDELIVER_INTENT; } protected void scheduleSyncIfApplicable(boolean fullSync) { if (fullSync) { scheduleContinousFullSyncIfApplicable(); } else { scheduleImmediatePushSyncIfApplicable(); } } /* * logic */ protected void startSync(int state, SyncInvocator invocator) { enterSyncingState(state); loginIfRequired(invocator); } protected void loginIfRequired(LoginCallback callback) { if (!getApp().isLoggedIn()) { try { getApp().getAccountHandler().login(callback); } catch (IllegalStateException ex) { stopForeground(); notifyError(R.string.login_credentials_missing); } } else { callback.onLoginFinished(null); } } protected void enterSyncingState(int state) throws IllegalStateException { if (this.state != STATE_IDLE) throw new IllegalStateException("Syning state can only be entered when in idle state."); Log.d(LOG_TAG, "Changing from idle state to state " + state); this.state = state; startForeground(); fireOnSyncStarted(); } protected void leaveSyncingState(Throwable error, int skipCount, int errorMsgId) throws IllegalStateException { Log.d(LOG_TAG, "Changing from state " + state + " to idle state"); stopForeground(); final boolean fullSync = isFullSyncing(); state = STATE_IDLE; if (error != null) { Log.e(LOG_TAG, "Sync failed!", error); notifyError(errorMsgId); } if (skipCount > 0) { Log.e(LOG_TAG, "Skipped " + skipCount + " item(s) during sync!"); } fireOnSyncFinished(fullSync, error); /* * reschedule full sync if applicable */ if (fullSync) { scheduleContinousFullSyncIfApplicable(); /* * trigger content download if applicable */ final OfflineStrategy offlineStrategy = getApp().getSettings().getOfflineStrategy(); if (error == null && (offlineStrategy == OfflineStrategy.SYNC || offlineStrategy == OfflineStrategy.READ_LIST)) { ContentDownloadService.start(getApp(), false); } } /* * handle service stop */ if (startedByIntent) { startedByIntent = false; Log.d(LOG_TAG, "Stopping sync triggered by intent"); stopSelf(); } else if (error != null) { scheduleImmediatePushSyncIfApplicable(); } } /* * sync callback */ @Override public void onProgressUpdate(int progress, int max) { Log.d(LOG_TAG, "Progress update: " + progress + "/" + max); progressNotificationHelper.setProgress(max, progress); getNotificationManager().notify(NOTIFICATION_ID, progressNotificationHelper.getNotification()); } @Override public void onSyncFinished(SyncException error, int unreadCount, int newUnreadCount, int skipCount) { final int msgId; if (error != null) { switch (error.getErrorCode()) { case CONNECTION: msgId = R.string.sync_failed_connection; break; case STORAGE: msgId = R.string.sync_failed_storage; break; case DEVICE_STORAGE_LOW: msgId = R.string.sync_failed_device_storage_low; break; case CANCELLED: msgId = 0; error = null; break; default: msgId = R.string.sync_failed_error; } } else { msgId = 0; if (isFullSyncing()) { notifySyncEnd(unreadCount, newUnreadCount); } } leaveSyncingState(error, skipCount, msgId); } /* * listener handling */ protected void fireOnSyncStarted() { for (SyncListener l : serviceProxy.getListeners()) { l.onSyncStarted(); } } protected void fireOnSyncFinished(boolean fullSync, Throwable error) { for (SyncListener l : serviceProxy.getListeners()) { l.onSyncFinished(fullSync, error); } } /* * helpers */ protected Application getApp() { return (Application) getApplication(); } protected NotificationManager getNotificationManager() { return (NotificationManager) getSystemService(NOTIFICATION_SERVICE); } protected PowerManager.WakeLock acquireWakeLock() { final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); final PowerManager.WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName()); wakeLock.acquire(); return wakeLock; } protected Intent createHomeActivityIntent() { return new Intent(this, HomeActivity.class); } protected void startForeground() { wakeLock = acquireWakeLock(); clearNotifications(); progressNotificationHelper.resetProgress(); progressNotificationHelper.setContentIntent(PendingIntent.getActivity( getApp(), 0, createHomeActivityIntent(), PendingIntent.FLAG_UPDATE_CURRENT)); super.startForeground(progressNotificationHelper.getNotification()); } public void stopForeground() { wakeLock.release(); super.stopForeground(); } protected void notifyError(int msgId) { syncErrorNotification.setContentText(this, msgId); getNotificationManager().notify(NOTIFICATION_ERROR, syncErrorNotification); } protected void clearError() { getNotificationManager().cancel(NOTIFICATION_ERROR); } private Notification createSyncSucceededNotification(int unreadCount) { final String msg; if (unreadCount > 1) { msg = String.format(getString(R.string.sync_notify_unread_items_msg), unreadCount); } else { msg = getString(R.string.sync_notify_unread_item_msg); } return SimpleNotificationBuilder.create( this, R.drawable.notify_sync_finished, R.string.sync_notify_unread_items_title). setContentIntent(createHomeActivityIntent()). setContentText(msg). setNumber(unreadCount). getNotification(); } protected void notifySyncEnd(int unreadCount, int newUnreadCount) { if (getApp().getSettings().shouldNotifyOnNewUnreadItems()) { if (newUnreadCount > 0) { getNotificationManager().notify(NOTIFICATION_PULL_SUCCESS, createSyncSucceededNotification(unreadCount)); } else { getNotificationManager().cancel(NOTIFICATION_PULL_SUCCESS); } } } protected void clearSuccess() { getNotificationManager().cancel(NOTIFICATION_PULL_SUCCESS); } protected void clearNotifications() { clearError(); clearSuccess(); } /* * life cycle management */ @Override public IBinder onBind(Intent intent) { return binder; } @Override protected int getForegroundId() { return NOTIFICATION_ID; } @Override public void onCreate() { super.onCreate(); Log.d(LOG_TAG, "Creating sync service"); syncThread = new SyncThread(getApp(), this); syncThread.start(); /* * prepare notification */ progressNotificationHelper = ProgressNotificationHelper.create( this, R.drawable.notify_sync, R.string.sync_notify_title); syncErrorNotification = new SimpleNotification(this, R.drawable.notify_sync_error, R.string.sync_notify_failed_title, true). setContentIntent(this, createHomeActivityIntent()); /* * handle continous syncing */ getApp().getSettings().registerChangeListener("sync_continous", continousSyncIntervalSettingChangeListener); scheduleImmediatePushSyncIfApplicable(); scheduleContinousFullSyncIfApplicable(); } @Override public void onDestroy() { Log.d(LOG_TAG, "Destroying sync service"); getApp().getSettings().unregisterChangeListener("sync_continous", continousSyncIntervalSettingChangeListener); syncThread.shutdown(); super.onDestroy(); } /* * inner classes */ abstract class SyncInvocator implements LoginCallback { @Override public final void onLoginStarted() { // not interested } @Override public final void onLoginFinished(Throwable error) { if (error != null) { leaveSyncingState(error, 0, R.string.login_failed); } else { doSync(); } } protected abstract void doSync(); } }