com.google.drive.appdatapreferences.AppdataPreferencesSyncer.java Source code

Java tutorial

Introduction

Here is the source code for com.google.drive.appdatapreferences.AppdataPreferencesSyncer.java

Source

/**
 * 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";

}