package com.zegoggles.smssync.service; import android.content.Context; import android.os.AsyncTask; import android.util.Log; import com.fsck.k9.mail.AuthenticationFailedException; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.store.imap.XOAuth2AuthenticationFailedException; import com.squareup.otto.Subscribe; import com.zegoggles.smssync.App; import com.zegoggles.smssync.R; import com.zegoggles.smssync.auth.OAuth2Client; import com.zegoggles.smssync.auth.TokenRefreshException; import com.zegoggles.smssync.auth.TokenRefresher; import com.zegoggles.smssync.calendar.CalendarAccessor; import com.zegoggles.smssync.contacts.ContactAccessor; import com.zegoggles.smssync.contacts.ContactGroupIds; import com.zegoggles.smssync.mail.BackupImapStore; import com.zegoggles.smssync.mail.CallFormatter; import com.zegoggles.smssync.mail.ConversionResult; import com.zegoggles.smssync.mail.DataType; import com.zegoggles.smssync.mail.MessageConverter; import com.zegoggles.smssync.mail.PersonLookup; import com.zegoggles.smssync.preferences.AuthPreferences; import com.zegoggles.smssync.preferences.Preferences; import com.zegoggles.smssync.service.state.BackupState; import com.zegoggles.smssync.service.state.SmsSyncState; import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.Locale; import static com.zegoggles.smssync.App.LOCAL_LOGV; import static com.zegoggles.smssync.App.TAG; import static com.zegoggles.smssync.mail.DataType.CALLLOG; import static com.zegoggles.smssync.mail.DataType.Defaults; import static com.zegoggles.smssync.mail.DataType.MMS; import static com.zegoggles.smssync.mail.DataType.SMS; import static com.zegoggles.smssync.service.state.SmsSyncState.BACKUP; import static com.zegoggles.smssync.service.state.SmsSyncState.CALC; import static com.zegoggles.smssync.service.state.SmsSyncState.CANCELED_BACKUP; import static com.zegoggles.smssync.service.state.SmsSyncState.ERROR; import static com.zegoggles.smssync.service.state.SmsSyncState.FINISHED_BACKUP; import static com.zegoggles.smssync.service.state.SmsSyncState.LOGIN; class BackupTask extends AsyncTask<BackupConfig, BackupState, BackupState> { private final SmsBackupService service; private final BackupItemsFetcher fetcher; private final MessageConverter converter; private final CalendarSyncer calendarSyncer; private final AuthPreferences authPreferences; private final Preferences preferences; private final ContactAccessor contactAccessor; private final TokenRefresher tokenRefresher; BackupTask(@NotNull SmsBackupService service) { final Context context = service.getApplicationContext(); this.service = service; this.authPreferences = service.getAuthPreferences(); this.preferences = service.getPreferences(); this.fetcher = new BackupItemsFetcher(context, context.getContentResolver(), new BackupQueryBuilder(context)); PersonLookup personLookup = new PersonLookup(service.getContentResolver()); this.converter = new MessageConverter(context, preferences, authPreferences.getUserEmail(), personLookup, ContactAccessor.Get.instance()); this.contactAccessor = ContactAccessor.Get.instance(); 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); } BackupTask(SmsBackupService service, BackupItemsFetcher fetcher, MessageConverter messageConverter, CalendarSyncer syncer, AuthPreferences authPreferences, Preferences preferences, ContactAccessor accessor, TokenRefresher refresher) { this.service = service; this.fetcher = fetcher; this.converter = messageConverter; this.calendarSyncer = syncer; this.authPreferences = authPreferences; this.preferences = preferences; this.contactAccessor = accessor; this.tokenRefresher = refresher; } @Override protected void onPreExecute() { App.bus.register(this); } @Subscribe public void userCanceled(UserCanceled canceled) { cancel(false); } @Override protected BackupState doInBackground(BackupConfig... params) { if (params == null || params.length == 0) throw new IllegalArgumentException("No config passed"); final BackupConfig config = params[0]; if (config.skip) { return skip(config.typesToBackup); } else { return acquireLocksAndBackup(config); } } private BackupState acquireLocksAndBackup(BackupConfig config) { try { service.acquireLocks(); return fetchAndBackupItems(config); } finally { service.releaseLocks(); } } private BackupState fetchAndBackupItems(BackupConfig config) { BackupCursors cursors = null; try { final ContactGroupIds groupIds = contactAccessor.getGroupContactIds(service.getContentResolver(), config.groupToBackup); cursors = new BulkFetcher(fetcher).fetch(config.typesToBackup, groupIds, config.maxItemsPerSync); final int itemsToSync = cursors.count(); if (itemsToSync > 0) { appLog(R.string.app_log_backup_messages, cursors.count(SMS), cursors.count(MMS), cursors.count(CALLLOG)); if (config.debug) { appLog(R.string.app_log_backup_messages_with_config, config); } return backupCursors(cursors, config.imapStore, config.backupType, itemsToSync); } else { appLog(R.string.app_log_skip_backup_no_items); if (preferences.isFirstBackup()) { // If this is the first backup we need to write something to MAX_SYNCED_DATE // such that we know that we've performed a backup before. SMS.setMaxSyncedDate(service, Defaults.MAX_SYNCED_DATE); MMS.setMaxSyncedDate(service, Defaults.MAX_SYNCED_DATE); } Log.i(TAG, "Nothing to do."); return transition(FINISHED_BACKUP, null); } } catch (XOAuth2AuthenticationFailedException e) { return handleAuthError(config, e); } catch (AuthenticationFailedException e) { return transition(ERROR, e); } catch (MessagingException e) { return transition(ERROR, e); } finally { if (cursors != null) { cursors.close(); } } } 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 BackupState skip(Iterable<DataType> types) { appLog(R.string.app_log_skip_backup_skip_messages); for (DataType type : types) { type.setMaxSyncedDate(service, fetcher.getMostRecentTimestamp(type)); } Log.i(TAG, "All messages skipped."); return new BackupState(FINISHED_BACKUP, 0, 0, BackupType.MANUAL, null, null); } private void appLog(int id, Object... args) { service.appLog(id, args); } private void appLogDebug(String message, Object... args) { service.appLogDebug(message, args); } private BackupState transition(SmsSyncState smsSyncState, Exception exception) { return service.transition(smsSyncState, exception); } @Override protected void onProgressUpdate(BackupState... progress) { if (progress != null && progress.length > 0 && !isCancelled()) { post(progress[0]); } } @Override protected void onPostExecute(BackupState result) { if (result != null) { post(result); } App.bus.unregister(this); } @Override protected void onCancelled() { post(transition(CANCELED_BACKUP, null)); App.bus.unregister(this); } private void post(BackupState state) { if (state == null) return; App.bus.post(state); } private BackupState backupCursors(BackupCursors cursors, BackupImapStore store, BackupType backupType, int itemsToSync) throws MessagingException { Log.i(TAG, String.format(Locale.ENGLISH, "Starting backup (%d messages)", itemsToSync)); publish(LOGIN); store.checkSettings(); try { publish(CALC); int backedUpItems = 0; while (!isCancelled() && cursors.hasNext()) { BackupCursors.CursorAndType cursor = cursors.next(); if (LOCAL_LOGV) Log.v(TAG, "backing up: " + cursor); ConversionResult result = converter.convertMessages(cursor.cursor, cursor.type); if (!result.isEmpty()) { List<Message> messages = result.getMessages(); if (LOCAL_LOGV) { Log.v(TAG, String.format(Locale.ENGLISH, "sending %d %s message(s) to server.", messages.size(), cursor.type)); } store.getFolder(cursor.type).appendMessages(messages); if (cursor.type == CALLLOG && calendarSyncer != null) { calendarSyncer.syncCalendar(result); } cursor.type.setMaxSyncedDate(service, result.getMaxDate()); backedUpItems += messages.size(); } else { Log.w(TAG, "no messages converted"); itemsToSync -= 1; } publishProgress(new BackupState(BACKUP, backedUpItems, itemsToSync, backupType, cursor.type, null)); } return new BackupState(FINISHED_BACKUP, backedUpItems, itemsToSync, backupType, null, null); } finally { store.closeFolders(); } } private void publish(SmsSyncState state) { publish(state, null); } private void publish(SmsSyncState state, Exception exception) { publishProgress(service.transition(state, exception)); } }