Java tutorial
/** * Copyright 2013 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.drive.appdatapreferences; import java.io.IOException; import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; import com.google.api.client.extensions.android.http.AndroidHttp; import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException; import com.google.api.client.json.gson.GsonFactory; import com.google.api.services.drive.Drive; import com.google.drive.appdatapreferences.tasks.GetOrCreatePreferencesDriveTask; import com.google.drive.appdatapreferences.tasks.UpdatePreferencesDriveTask; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; /** * Manages the synchronization between an appdata preferences file * and a local SharedPreferences instance. * * @author jbd@google.com (Burcu Dogan) */ public class AppdataPreferencesSyncer { public static final String LAST_UPDATE_KEY = "_last_update"; private static AppdataPreferencesSyncer sInstance; private final Context mContext; private GoogleAccountCredential mCredential; private SharedPreferences mPreferences; private OnChangeListener mOnChangeListener; private OnUserRecoverableAuthExceptionExceptionListener mOnExceptionListener; private AppdataPreferencesSyncManager mSyncManager; private String mLastSyncedJson; private AppdataSyncStrategy mSyncStrategy; /** * Gets the singleton {@code AppdataPreferencesSyncer} instance. * @param context Context of the application */ public static AppdataPreferencesSyncer get(Context context) { if (sInstance == null) { sInstance = new AppdataPreferencesSyncer(context); } return sInstance; } /** * Private {@code AppdataPreferencesSyncer} constructor. * @param context Context of the application */ private AppdataPreferencesSyncer(Context context) { mContext = context; mSyncStrategy = new AppdataDefaultSyncStrategy(); } /** * Binds a {@code SharedPreferences} object to a Google account. * @param credential Google account credentials. * @param preferences Preferences to be bound to the Google account. */ public void bind(GoogleAccountCredential credential, SharedPreferences preferences) { setCredential(credential); setPreferences(preferences); } public void setSyncStrategy(AppdataSyncStrategy syncStrategy) { this.mSyncStrategy = syncStrategy; } /** * Syncs the preferences file with an appdata preferences file. * * Synchronization steps: * 1. If there are local changes, sync the latest local version with remote * and ignore merge conflicts. The last write wins. * 2. If there are no local changes, fetch the latest remote version. If * it includes changes, notify that preferences have changed. */ public synchronized void sync() { // TODO: don't silently ignore the sync operation // notify user that preferences and credential are not set. if (mPreferences == null || mCredential == null) { Log.w(TAG, "Preferences or credential are not set. Skipping synchronization."); return; } // check if the values are changed since last update Map<String, ?> localValues = mPreferences.getAll(); String localJson = GSON.toJson(localValues); Log.d(TAG, "Local JSON: " + localJson); try { String remoteJson = new GetOrCreatePreferencesDriveTask(getDriveService()).execute(); Log.d(TAG, "Remote JSON: " + remoteJson); if (remoteJson == null || remoteJson.length() == 0) { Log.d(TAG, "Nothing on remote. Let's populate it."); updateRemote(localJson); return; } Type type = new TypeToken<HashMap<String, Object>>() { }.getType(); Map<String, Object> remoteValues = GSON.fromJson(remoteJson, type); AppdataSyncStrategy.SyncContext context = new AppdataSyncStrategy.SyncContext(mLastSyncedJson, remoteJson, remoteValues, localJson, localValues); AppdataSyncStrategy.Resolution resolution = mSyncStrategy.getSyncResolution(context); switch (resolution) { case DO_NOTHING: Log.d(TAG, "Nothing to do."); break; case PUSH_LOCAL_TO_REMOTE: Log.d(TAG, "Pushing local values to remote"); updateRemote(localJson); break; case PUSH_REMOTE_TO_LOCAL: Log.d(TAG, "Pushing remote values to local"); updateLocal(remoteJson, remoteValues); break; default: throw new IllegalStateException("Unexpected sync resolution: " + resolution); } } catch (IOException e) { handleException(e); } } /** * Gets the context. */ public Context getContext() { return mContext; } /** * Gets the sync manager. */ public AppdataPreferencesSyncManager getSyncManager() { return mSyncManager; } /** * Gets the credential. */ public GoogleAccountCredential getCredential() { return mCredential; } /** * Gets the preferences. */ public SharedPreferences getPreferences() { return mPreferences; } /** * Constructs a Drive service in the current context and with the * credentials use to initiate AppdataPreferences instance. * @return Drive service instance. */ public Drive getDriveService() { Drive service = new Drive.Builder(AndroidHttp.newCompatibleTransport(), new GsonFactory(), mCredential) .build(); return service; } /** * Sets the credential and starts a periodic sync for the * selected account. * @param credential User's credential */ public void setCredential(GoogleAccountCredential credential) { mCredential = credential; mSyncManager = new AppdataPreferencesSyncManager(credential.getSelectedAccount()); mSyncManager.startPeriodicSync(); } /** * Sets the {@code SharedPreferences} instance. * @param preferences {@code SharedPreferences} instance to be synced. */ public void setPreferences(SharedPreferences preferences) { mPreferences = preferences; } /** * Sets a {@code UserRecoverableAuthExceptionListener}. * @param listener */ public void setOnUserRecoverableAuthExceptionListener( OnUserRecoverableAuthExceptionExceptionListener listener) { mOnExceptionListener = listener; } /** * Sets the OnChangeListener. * @param listener */ public void setOnChangeListener(OnChangeListener listener) { mOnChangeListener = listener; } /** * Updates the remote preferences file with the given JSON content. * @param json New contents of the remote preferences file in JSON. * @throws IOException */ private void updateRemote(String json) throws IOException { Log.d(TAG, "Updating the remote preferences file"); // update the remote new UpdatePreferencesDriveTask(getDriveService()).execute(json); mLastSyncedJson = json; } /** * Updates the local SharedPreferences instance with the remote * changes and calls OnChangeListener. * @throws IOException * @param remoteJson */ private void updateLocal(String remoteJson) throws IOException { Log.d(TAG, "Updating the local preferences file"); // update the local preferences HashMap<String, Object> remoteObj = null; Type type = new TypeToken<HashMap<String, Object>>() { }.getType(); remoteObj = GSON.fromJson(remoteJson, type); updateLocal(remoteJson, remoteObj); } private void updateLocal(String remoteJson, Map<String, Object> remoteObj) { Utils.replaceValues(mPreferences, remoteObj); // Notify if there are changes if (!remoteJson.equals(mLastSyncedJson) && mOnChangeListener != null) { mOnChangeListener.onChange(mPreferences); mLastSyncedJson = remoteJson; } } /** * Handles API exceptions and notifies OnExceptionListener * if given exception is a UserRecoverableAuthIOException. * @param exception Exception to handle */ private void handleException(Exception exception) { Log.w(TAG, exception); if (mOnExceptionListener == null) { return; } if (exception instanceof UserRecoverableAuthIOException) { mOnExceptionListener.onUserRecoverableAuthException((UserRecoverableAuthIOException) exception); } else { exception.printStackTrace(); } } /** * Called when remote preferences file is changed and changes * are synced to the local SharedPreferences instance. */ public interface OnChangeListener { public void onChange(SharedPreferences prefs); } /** * Called when Google Drive API requests responds with a 401 or 403 and * user's permission is required. */ public interface OnUserRecoverableAuthExceptionExceptionListener { public void onUserRecoverableAuthException(UserRecoverableAuthIOException ex); } final public static Gson GSON = new GsonBuilder().create(); final public static String TAG = "syncer"; }