private BackupState acquireLocksAndBackup(BackupConfig config) { try { service.acquireLocks(); return fetchAndBackupItems(config); } finally { service.releaseLocks(); } }
private void moveToState(BackupState state) { backupStateChanged(state); App.post(state); }
private void backup(BackupType backupType) { getNotifier().cancel(NOTIFICATION_ID_WARNING); try { // set initial state state = new BackupState(INITIAL, 0, 0, backupType, null, null); EnumSet<DataType> enabledTypes = getEnabledBackupTypes(); checkPermissions(enabledTypes); if (backupType != SKIP) { checkCredentials(); if (getPreferences().isUseOldScheduler()) { legacyCheckConnectivity(); } } appLog(R.string.app_log_start_backup, backupType); getBackupTask().execute(getBackupConfig(backupType, enabledTypes, getBackupImapStore())); } catch (MessagingException e) { Log.w(TAG, e); moveToState(state.transition(ERROR, e)); } catch (ConnectivityException e) { moveToState(state.transition(ERROR, e)); } catch (RequiresLoginException e) { appLog(R.string.app_log_missing_credentials); moveToState(state.transition(ERROR, e)); } catch (BackupDisabledException e) { moveToState(state.transition(FINISHED_BACKUP, e)); } catch (MissingPermissionException e) { moveToState(state.transition(ERROR, e)); } }
@SuppressWarnings("deprecation") private void legacyCheckConnectivity() throws ConnectivityException { NetworkInfo active = getConnectivityManager().getActiveNetworkInfo(); if (active == null || !active.isConnectedOrConnecting()) { throw new NoConnectionException(); } if (getPreferences().isWifiOnly() && isBackgroundTask() && !isConnectedViaWifi()) { throw new RequiresWifiException(); } }
@Override protected void handleIntent(final Intent intent) { if (intent == null) return; // NB: should not happen with START_NOT_STICKY final BackupType backupType = BackupType.fromIntent(intent); if (LOCAL_LOGV) { Log.v(TAG, "handleIntent(" + intent + ", " + (intent.getExtras() == null ? "null" : intent.getExtras().keySet()) + ", " + intent.getAction() + ", type="+backupType+")"); } appLog(R.string.app_log_backup_requested, getString(backupType.resId)); // Only start a backup if there's no other operation going on at this time. if (!isWorking() && SmsRestoreService.isServiceIdle()) { backup(backupType); } else { appLog(R.string.app_log_skip_backup_already_running); } }
private void handleErrorState(BackupState state) { if (state.isAuthException()) { appLog(R.string.app_log_backup_failed_authentication, state.getDetailedErrorMessage(getResources())); if (shouldNotifyUser(state)) { notifyUser(NOTIFICATION_ID_WARNING, notificationBuilder(stat_sys_warning, getString(R.string.notification_auth_failure), getString(getAuthPreferences().useXOAuth() ? R.string.status_auth_failure_details_xoauth : R.string.status_auth_failure_details_plain))); } } else if (state.isConnectivityError()) { appLog(R.string.app_log_backup_failed_connectivity, state.getDetailedErrorMessage(getResources())); } else if (state.isPermissionException()) { if (state.backupType != MANUAL) { Bundle extras = new Bundle(); extras.putStringArray(MainActivity.EXTRA_PERMISSIONS, state.getMissingPermissions()); notifyUser(NOTIFICATION_ID_WARNING, notificationBuilder(R.drawable.ic_notification, getString(R.string.notification_missing_permission), formatMissingPermissionDetails(getResources(), state.getMissingPermissions())) .setContentIntent(getPendingIntent(extras))); } } else { appLog(R.string.app_log_backup_failed_general_error, state.getDetailedErrorMessage(getResources())); if (shouldNotifyUser(state)) { notifyUser(NOTIFICATION_ID_WARNING, notificationBuilder(stat_sys_warning, getString(R.string.notification_general_error), state.getErrorMessage(getResources()))); } } }
@Test public void shouldHandleAuthErrorAndTokenCouldBeRefreshed() throws Exception { mockFetch(SMS, 1); when(converter.convertMessages(any(Cursor.class), notNull(DataType.class))).thenReturn(result(SMS, 1)); XOAuth2AuthenticationFailedException exception = mock(XOAuth2AuthenticationFailedException.class); when(exception.getStatus()).thenReturn(400); when(store.getFolder(notNull(DataType.class), same(dataTypePreferences))).thenThrow(exception); when(service.getBackupImapStore()).thenReturn(store); task.doInBackground(config); verify(tokenRefresher).refreshOAuth2Token(); verify(service, times(2)).transition(SmsSyncState.LOGIN, null); verify(service, times(2)).transition(SmsSyncState.CALC, null); verify(service).transition(SmsSyncState.ERROR, exception); // make sure locks only get acquired+released once verify(service).acquireLocks(); verify(service).releaseLocks(); }
@Test public void shouldAcquireAndReleaseLocksDuringBackup() throws Exception { mockAllFetchEmpty(); task.doInBackground(config); verify(service).acquireLocks(); verify(service).releaseLocks(); verify(service).transition(SmsSyncState.FINISHED_BACKUP, null); }
@Subscribe public void backupStateChanged(BackupState state) { if (this.state == state) return; this.state = state; if (this.state.isInitialState()) return; if (state.isError()) { handleErrorState(state); } if (state.isRunning()) { if (state.backupType == MANUAL) { notifyAboutBackup(state); } } else { appLogDebug(state.toString()); appLog(state.isCanceled() ? R.string.app_log_backup_canceled : R.string.app_log_backup_finished); scheduleNextBackup(state); stopForeground(true); stopSelf(); } }
@Test public void shouldCheckForEnabledDataTypes() throws Exception { when(dataTypePreferences.enabled()).thenReturn(EnumSet.noneOf(DataType.class)); Intent intent = new Intent(); when(authPreferences.isLoginInformationSet()).thenReturn(true); shadowConnectivityManager.setBackgroundDataSetting(true); service.handleIntent(intent); verifyZeroInteractions(backupTask); assertThat(service.getState().exception).isInstanceOf(BackupDisabledException.class); assertThat(service.getState().state).isEqualTo(SmsSyncState.FINISHED_BACKUP); }
BackupTask(@NonNull SmsBackupService service) { final Context context = service.getApplicationContext(); this.service = service; this.authPreferences = service.getAuthPreferences(); this.preferences = service.getPreferences(); this.fetcher = new BackupItemsFetcher( context.getContentResolver(), new BackupQueryBuilder(preferences.getDataTypePreferences())); PersonLookup personLookup = new PersonLookup(service.getContentResolver()); this.contactAccessor = new ContactAccessor(); this.converter = new MessageConverter(context, service.getPreferences(), authPreferences.getUserEmail(), personLookup, contactAccessor); if (preferences.isCallLogCalendarSyncEnabled()) { calendarSyncer = new CalendarSyncer( CalendarAccessor.Get.instance(service.getContentResolver()), preferences.getCallLogCalendarId(), personLookup, new CallFormatter(context.getResources()) ); } else { calendarSyncer = null; } this.tokenRefresher = new TokenRefresher(service, new OAuth2Client(authPreferences.getOAuth2ClientId()), authPreferences); }
@Test public void shouldScheduleNextRegularBackupAfterFinished() throws Exception { shadowConnectivityManager.setBackgroundDataSetting(true); Intent intent = new Intent(REGULAR.name()); service.handleIntent(intent); verify(backupTask).execute(any(BackupConfig.class)); service.backupStateChanged(service.transition(SmsSyncState.FINISHED_BACKUP, null)); verify(backupJobs).scheduleRegular(); assertThat(shadowOf(service).isStoppedBySelf()).isTrue(); assertThat(shadowOf(service).isForegroundStopped()).isTrue(); }
@Before public void before() { initMocks(this); config = getBackupConfig(EnumSet.of(SMS)); when(service.getApplicationContext()).thenReturn(RuntimeEnvironment.application); when(service.getState()).thenReturn(state); when(preferences.getDataTypePreferences()).thenReturn(dataTypePreferences); task = new BackupTask(service, fetcher, converter, syncer, authPreferences, preferences, accessor, tokenRefresher); context = RuntimeEnvironment.application; }
@Before public void before() { initMocks(this); sentNotifications = new ArrayList<NotificationCompat.Builder>(); service = new SmsBackupService() { @Override public Context getApplicationContext() { return RuntimeEnvironment.application; } @Override public Resources getResources() { return getApplicationContext().getResources(); } @Override protected BackupTask getBackupTask() { return backupTask; } @Override protected BackupJobs getBackupJobs() { return backupJobs; } @Override protected Preferences getPreferences() { return preferences; } @Override public int checkPermission(String permission, int pid, int uid) { return PERMISSION_GRANTED; } @Override protected AuthPreferences getAuthPreferences() { return authPreferences; } @Override protected void notifyUser(int icon, NotificationCompat.Builder builder) { sentNotifications.add(builder); } }; shadowConnectivityManager = shadowOf(service.getConnectivityManager()); shadowWifiManager = shadowOf(service.getWifiManager()); service.onCreate(); when(authPreferences.getStoreUri()).thenReturn("imap+ssl+://xoauth:foooo@imap.gmail.com:993"); when(authPreferences.isLoginInformationSet()).thenReturn(true); when(preferences.getBackupContactGroup()).thenReturn(ContactGroup.EVERYBODY); when(preferences.isUseOldScheduler()).thenReturn(true); when(preferences.getDataTypePreferences()).thenReturn(dataTypePreferences); when(dataTypePreferences.enabled()).thenReturn(EnumSet.of(DataType.SMS)); }
private void scheduleNextBackup(BackupState state) { if (state.backupType == REGULAR && getPreferences().isUseOldScheduler()) { final Job nextSync = getBackupJobs().scheduleRegular(); if (nextSync != null) { JobTrigger.ExecutionWindowTrigger trigger = (JobTrigger.ExecutionWindowTrigger) nextSync.getTrigger(); Date date = new Date(System.currentTimeMillis() + (trigger.getWindowStart() * 1000)); appLog(R.string.app_log_scheduled_next_sync, DateFormat.format("kk:mm", date)); } else { appLog(R.string.app_log_no_next_sync); } } // else job already persisted }
private BackupState transition(SmsSyncState smsSyncState, Exception exception) { return service.transition(smsSyncState, exception); }
@Override public void onDestroy() { super.onDestroy(); if (LOCAL_LOGV) Log.v(TAG, "SmsBackupService#onDestroy(state=" + getState() + ")"); service = null; }
private BackupState handleAuthError(BackupConfig config, XOAuth2AuthenticationFailedException e) { if (e.getStatus() == 400) { appLogDebug("need to perform xoauth2 token refresh"); if (config.currentTry < 1) { try { tokenRefresher.refreshOAuth2Token(); // we got a new token, let's handleAuthError one more time - we need to pass in a new store object // since the auth params on it are immutable appLogDebug("token refreshed, retrying"); return fetchAndBackupItems(config.retryWithStore(service.getBackupImapStore())); } catch (MessagingException ignored) { Log.w(TAG, ignored); } catch (TokenRefreshException refreshException) { appLogDebug("error refreshing token: "+refreshException+", cause="+refreshException.getCause()); } } else { appLogDebug("no new token obtained, giving up"); } } else { appLogDebug("unexpected xoauth status code " + e.getStatus()); } return transition(ERROR, e); }
private void appLogDebug(String message, Object... args) { service.appLogDebug(message, args); }
private void appLog(int id, Object... args) { service.appLog(id, args); }