/**
* 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);
}