From 2329ccc32bdc80a4901533e7acacfa927b15bf1b Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Thu, 12 Mar 2015 17:48:43 -0700 Subject: [PATCH] Bug 1117830 - Part 3: ReadingListSyncAdapter. r=nalexander --- .../base/reading/ReadingListSyncAdapter.java | 268 +++++++++++++++++- 1 file changed, 264 insertions(+), 4 deletions(-) diff --git a/mobile/android/base/reading/ReadingListSyncAdapter.java b/mobile/android/base/reading/ReadingListSyncAdapter.java index 4033b7d02577..8d04c6f86282 100644 --- a/mobile/android/base/reading/ReadingListSyncAdapter.java +++ b/mobile/android/base/reading/ReadingListSyncAdapter.java @@ -4,22 +4,282 @@ package org.mozilla.gecko.reading; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.NoSuchAlgorithmException; +import java.util.Collection; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.mozilla.gecko.background.common.PrefsBranch; +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.background.fxa.FxAccountClient; +import org.mozilla.gecko.background.fxa.FxAccountClient20; +import org.mozilla.gecko.background.fxa.FxAccountUtils; +import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient.RequestDelegate; +import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException; +import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10; +import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10.AuthorizationResponse; +import org.mozilla.gecko.browserid.BrowserIDKeyPair; +import org.mozilla.gecko.browserid.JSONWebTokenUtils; +import org.mozilla.gecko.db.BrowserContract.ReadingListItems; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; +import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine; +import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate; +import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition; +import org.mozilla.gecko.fxa.login.Married; +import org.mozilla.gecko.fxa.login.State; +import org.mozilla.gecko.fxa.login.State.StateLabel; +import org.mozilla.gecko.fxa.login.StateFactory; +import org.mozilla.gecko.fxa.sync.FxAccountSyncDelegate; +import org.mozilla.gecko.sync.net.AuthHeaderProvider; +import org.mozilla.gecko.sync.net.BearerAuthHeaderProvider; import android.accounts.Account; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; +import android.content.ContentResolver; import android.content.Context; +import android.content.SharedPreferences; import android.content.SyncResult; import android.os.Bundle; public class ReadingListSyncAdapter extends AbstractThreadedSyncAdapter { - public ReadingListSyncAdapter(Context context, boolean autoInitialize) { - super(context, autoInitialize); + public static final String OAUTH_CLIENT_ID_FENNEC = "3332a18d142636cb"; + public static final String OAUTH_SCOPE_READINGLIST = "readinglist"; + + private static final String LOG_TAG = ReadingListSyncAdapter.class.getSimpleName(); + private static final long TIMEOUT_SECONDS = 60; + protected final ExecutorService executor; + + public ReadingListSyncAdapter(Context context, boolean autoInitialize) { + super(context, autoInitialize); + this.executor = Executors.newSingleThreadExecutor(); + } + + + static final class SyncAdapterSynchronizerDelegate implements ReadingListSynchronizerDelegate { + private final FxAccountSyncDelegate syncDelegate; + private final ContentProviderClient cpc; + private final SyncResult result; + + SyncAdapterSynchronizerDelegate(FxAccountSyncDelegate syncDelegate, + ContentProviderClient cpc, + SyncResult result) { + this.syncDelegate = syncDelegate; + this.cpc = cpc; + this.result = result; } @Override - public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { - final AndroidFxAccount fxAccount = new AndroidFxAccount(getContext(), account); + public void onUnableToSync(Exception e) { + Logger.warn(LOG_TAG, "Unable to sync.", e); + cpc.release(); + syncDelegate.handleError(e); } + + @Override + public void onStatusUploadComplete(Collection uploaded, + Collection failed) { + Logger.debug(LOG_TAG, "Step: onStatusUploadComplete"); + this.result.stats.numEntries += 1; // TODO: Bug 1140809. + } + + @Override + public void onNewItemUploadComplete(Collection uploaded, + Collection failed) { + Logger.debug(LOG_TAG, "Step: onNewItemUploadComplete"); + this.result.stats.numEntries += 1; // TODO: Bug 1140809. + } + + @Override + public void onModifiedUploadComplete() { + Logger.debug(LOG_TAG, "Step: onModifiedUploadComplete"); + this.result.stats.numEntries += 1; // TODO: Bug 1140809. + } + + @Override + public void onDownloadComplete() { + Logger.debug(LOG_TAG, "Step: onDownloadComplete"); + this.result.stats.numInserts += 1; // TODO: Bug 1140809. + } + + @Override + public void onComplete() { + Logger.info(LOG_TAG, "Reading list synchronization complete."); + cpc.release(); + syncDelegate.handleSuccess(); + } + } + + @Override + public void onPerformSync(final Account account, final Bundle extras, final String authority, final ContentProviderClient provider, final SyncResult syncResult) { + Logger.setThreadLogTag(ReadingListConstants.GLOBAL_LOG_TAG); + Logger.resetLogging(); + + final Context context = getContext(); + final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); + + // If this sync was triggered by user action, this will be true. + final boolean isImmediate = (extras != null) && + (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false) || + extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)); + + final CountDownLatch latch = new CountDownLatch(1); + final FxAccountSyncDelegate syncDelegate = new FxAccountSyncDelegate(latch, syncResult, fxAccount); + try { + final State state; + try { + state = fxAccount.getState(); + } catch (Exception e) { + Logger.error(LOG_TAG, "Unable to sync.", e); + return; + } + + final String oauthServerUri = ReadingListConstants.OAUTH_ENDPOINT_PROD; + final String authServerEndpoint = fxAccount.getAccountServerURI(); + final String audience = FxAccountUtils.getAudienceForURL(oauthServerUri); // The assertion gets traded in for an oauth bearer token. + + final SharedPreferences sharedPrefs = fxAccount.getReadingListPrefs(); + final FxAccountClient client = new FxAccountClient20(authServerEndpoint, executor); + final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine(); + + stateMachine.advance(state, StateLabel.Married, new LoginStateMachineDelegate() { + @Override + public FxAccountClient getClient() { + return client; + } + + @Override + public long getCertificateDurationInMilliseconds() { + return 12 * 60 * 60 * 1000; + } + + @Override + public long getAssertionDurationInMilliseconds() { + return 15 * 60 * 1000; + } + + @Override + public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { + return StateFactory.generateKeyPair(); + } + + @Override + public void handleTransition(Transition transition, State state) { + Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel()); + } + + @Override + public void handleFinal(State state) { + Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel()); + fxAccount.setState(state); + + // TODO: scheduling, notifications. + try { + if (state.getStateLabel() != StateLabel.Married) { + syncDelegate.handleCannotSync(state); + return; + } + + final Married married = (Married) state; + final String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER); + JSONWebTokenUtils.dumpAssertion(assertion); + + final String clientID = OAUTH_CLIENT_ID_FENNEC; + final String scope = OAUTH_SCOPE_READINGLIST; + syncWithAssertion(clientID, scope, assertion, sharedPrefs, extras); + } catch (Exception e) { + syncDelegate.handleError(e); + return; + } + } + + private void syncWithAssertion(final String client_id, final String scope, final String assertion, + final SharedPreferences sharedPrefs, final Bundle extras) { + final FxAccountOAuthClient10 oauthClient = new FxAccountOAuthClient10(oauthServerUri, executor); + Logger.debug(LOG_TAG, "OAuth fetch."); + oauthClient.authorization(client_id, assertion, null, scope, new RequestDelegate() { + @Override + public void handleSuccess(AuthorizationResponse result) { + Logger.debug(LOG_TAG, "OAuth success."); + syncWithAuthorization(result, sharedPrefs, extras); + } + + @Override + public void handleFailure(FxAccountAbstractClientRemoteException e) { + Logger.error(LOG_TAG, "OAuth failure.", e); + syncDelegate.handleError(e); + } + + @Override + public void handleError(Exception e) { + Logger.error(LOG_TAG, "OAuth error.", e); + syncDelegate.handleError(e); + } + }); + } + + private void syncWithAuthorization(AuthorizationResponse authResponse, + SharedPreferences sharedPrefs, + Bundle extras) { + final AuthHeaderProvider auth = new BearerAuthHeaderProvider(authResponse.access_token); + + final String endpointString = ReadingListConstants.DEFAULT_DEV_ENDPOINT; + final URI endpoint; + Logger.info(LOG_TAG, "XXX Syncing to " + endpointString); + try { + endpoint = new URI(endpointString); + } catch (URISyntaxException e) { + // Should never happen. + Logger.error(LOG_TAG, "Unexpected malformed URI for reading list service: " + endpointString); + syncDelegate.handleError(e); + return; + } + + final PrefsBranch branch = new PrefsBranch(sharedPrefs, "readinglist."); + final ReadingListClient remote = new ReadingListClient(endpoint, auth); + final ContentProviderClient cpc = getContentProviderClient(context); // TODO: make sure I'm always released! + + final ReadingListStorage local = new LocalReadingListStorage(cpc); + final ReadingListSynchronizer synchronizer = new ReadingListSynchronizer(branch, remote, local); + + synchronizer.syncAll(new SyncAdapterSynchronizerDelegate(syncDelegate, cpc, syncResult)); + // TODO: backoffs, and everything else handled by a SessionCallback. + } + }); + + latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + Logger.info(LOG_TAG, "Reading list sync done."); + + } catch (Exception e) { + Logger.error(LOG_TAG, "Got error syncing.", e); + syncDelegate.handleError(e); + } + /* + * TODO: + * * Account error notifications. How do we avoid these overlapping with Sync? + * * Pickling. How do we avoid pickling twice if you use both Sync and RL? + */ + + /* + * TODO: + * * Auth. + * * Server URI lookup. + * * Syncing. + * * Error handling. + * * Backoff and retry-after. + * * Sync scheduling. + * * Forcing syncs/interactive use. + */ + } + + + private ContentProviderClient getContentProviderClient(Context context) { + final ContentResolver contentResolver = context.getContentResolver(); + final ContentProviderClient client = contentResolver.acquireContentProviderClient(ReadingListItems.CONTENT_URI); + return client; + } }