com.google.samples.apps.iosched.sync.userdata.http.HTTPUserDataSyncHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.google.samples.apps.iosched.sync.userdata.http.HTTPUserDataSyncHelper.java

Source

/**
 * Copyright 2014 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.samples.apps.iosched.sync.userdata.http;

import android.content.Context;
import android.text.TextUtils;
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.api.services.drive.DriveScopes;
import com.google.samples.apps.iosched.sync.SyncHelper;
import com.google.samples.apps.iosched.sync.userdata.AbstractUserDataSyncHelper;
import com.google.samples.apps.iosched.sync.userdata.UserAction;
import com.google.samples.apps.iosched.sync.userdata.util.UserDataHelper;
import com.google.samples.apps.iosched.util.AccountUtils;

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static com.google.samples.apps.iosched.util.LogUtils.LOGD;
import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag;

/**
 * Helper class that syncs starred sessions data with Drive's AppData folder using direct
 * HTTP Drive API through google-api-client library.
 *
 * Based on https://github.com/googledrive/appdatapreferences-android
 */
public class HTTPUserDataSyncHelper extends AbstractUserDataSyncHelper {
    private static final String GCM_KEY_PREFIX = "GCM:";

    private GoogleAccountCredential mCredentials;

    /**
     * Private {@code HTTPUserDataSyncHelper} constructor.
     * @param context Context of the application
     */
    public HTTPUserDataSyncHelper(Context context, String accountName) {
        super(context, accountName);
        mCredentials = GoogleAccountCredential.usingOAuth2(mContext,
                java.util.Arrays.asList(DriveScopes.DRIVE_APPDATA));
        mCredentials.setSelectedAccountName(mAccountName);
    }

    private String extractGcmKey(Set<String> remote) {
        String remoteGcmKey = null;
        Set<String> toRemove = new HashSet<String>();
        for (String s : remote) {
            if (s.startsWith(GCM_KEY_PREFIX)) {
                toRemove.add(s);
                remoteGcmKey = s.substring(GCM_KEY_PREFIX.length());
                LOGD(TAG, "Remote data came with GCM key: " + AccountUtils.sanitizeGcmKey(remoteGcmKey));
            }
        }
        for (String s : toRemove) {
            remote.remove(s);
        }
        return remoteGcmKey;
    }

    /**
     * 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.
     */
    protected boolean syncImpl(List<UserAction> actions, boolean hasPendingLocalData) {
        try {
            LOGD(TAG, "Now syncing user data.");
            Set<String> remote = UserDataHelper.fromString(fetchRemote());
            Set<String> local = UserDataHelper.getSessionIDs(actions);

            String remoteGcmKey = extractGcmKey(remote);
            String localGcmKey = AccountUtils.getGcmKey(mContext, mAccountName);
            LOGD(TAG, "Local GCM key: " + AccountUtils.sanitizeGcmKey(localGcmKey));
            LOGD(TAG, "Remote GCM key: "
                    + (remoteGcmKey == null ? "(null)" : AccountUtils.sanitizeGcmKey(remoteGcmKey)));

            // if the remote data came with a GCM key, it should override ours
            if (!TextUtils.isEmpty(remoteGcmKey)) {
                if (remoteGcmKey.equals(localGcmKey)) {
                    LOGD(TAG, "Remote GCM key is the same as local, so no action necessary.");
                } else {
                    LOGD(TAG, "Remote GCM key is different from local. OVERRIDING local.");
                    localGcmKey = remoteGcmKey;
                    AccountUtils.setGcmKey(mContext, mAccountName, localGcmKey);
                }
            }

            // If remote data is the same as local, and the remote end already has a GCM key,
            // there is nothing we need to do.
            if (remote.equals(local) && !TextUtils.isEmpty(remoteGcmKey)) {
                LOGD(TAG, "Update is not needed (local is same as remote, and remote has key)");
                return false;
            }

            Set<String> merged;
            if (hasPendingLocalData || TextUtils.isEmpty(remoteGcmKey)) {
                // merge local dirty actions into remote content
                if (hasPendingLocalData) {
                    LOGD(TAG, "Has pending local data, merging.");
                    merged = mergeDirtyActions(actions, remote);
                } else {
                    LOGD(TAG, "No pending local data, just updating remote GCM key.");
                    merged = remote;
                }
                // add the GCM key special item
                merged.add(GCM_KEY_PREFIX + localGcmKey);
                // save to remote
                LOGD(TAG, "Sending user data to Drive, gcm key " + AccountUtils.sanitizeGcmKey(localGcmKey));
                new UpdateFileDriveTask(getDriveService()).execute(UserDataHelper.toSessionsString(merged));
            } else {
                merged = remote;
            }

            UserDataHelper.setLocalStarredSessions(mContext, merged, mAccountName);
            return true;
        } catch (IOException e) {
            handleException(e);
        }
        return false;
    }

    /**
     * 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(), mCredentials)
                .setApplicationName(mContext.getApplicationInfo().name).build();
        return service;
    }

    /**
     * Updates the remote preferences file with the given JSON content.
     * @throws IOException
     */
    private Set<String> mergeDirtyActions(List<UserAction> actions, Set<String> starredSessions)
            throws IOException {
        // apply "dirty" actions:
        for (UserAction action : actions) {
            if (action.requiresSync) {
                if (UserAction.TYPE.ADD_STAR.equals(action.type)) {
                    starredSessions.add(action.sessionId);
                } else {
                    starredSessions.remove(action.sessionId);
                }
            }
        }
        return starredSessions;
    }

    /**
     * Fetches the remote file.
     * @throws IOException
     */
    private String fetchRemote() throws IOException {
        String json = new GetOrCreateFIleDriveTask(getDriveService()).execute();
        Log.v(TAG, "Got this content from remote myschedule: [" + json + "]");
        return json;
    }

    /**
     * Handles API exceptions and notifies OnExceptionListener
     * if given exception is a UserRecoverableAuthIOException.
     * @param exception Exception to handle
     */
    private void handleException(Exception exception) {
        Log.e(TAG, "Could not sync myschedule", exception);
        if (exception != null && exception instanceof UserRecoverableAuthIOException) {
            throw new SyncHelper.AuthException();
        }
    }

    private static final String TAG = makeLogTag(HTTPUserDataSyncHelper.class);

}