/* * 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; import android.content.*; import android.database.Cursor; import android.net.Uri; import android.util.Log; import com.google.samples.apps.iosched.appwidget.ScheduleWidgetProvider; import com.google.samples.apps.iosched.gcm.ServerUtilities; import com.google.samples.apps.iosched.provider.ScheduleContract; import com.google.samples.apps.iosched.provider.ScheduleContract.MySchedule; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static com.google.samples.apps.iosched.util.LogUtils.LOGD; import static com.google.samples.apps.iosched.util.LogUtils.LOGW; import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag; /** * Helper class that syncs starred sessions data in a Drive's AppData folder. * * Protocode: * * // when user clicks on "star": * session UI: run updateSession() * this.updateSession(): * send addstar/removestar to contentProvider * send broadcast to update any dependent UI * save user actions as pending in shared preferences * * // on sync * syncadapter: call this.sync() * this.sync(): * fetch remote content * if pending actions: * apply to content and update remote * if modified content != last synced content: * update contentProvider * send broadcast to update any dependent UI * * */ public abstract class AbstractUserDataSyncHelper { private static final String TAG = makeLogTag(AbstractUserDataSyncHelper.class); protected Context mContext; protected String mAccountName; public AbstractUserDataSyncHelper(Context context, String accountName) { this.mContext = context; this.mAccountName = accountName; } protected abstract boolean syncImpl(List<UserAction> actions, boolean hasPendingLocalData); /** * Create a copy of current pending actions and delegate the * proper sync'ing to the concrete subclass on the method syncImpl. * */ public boolean sync() { // get data pending sync: Cursor scheduleData = mContext.getContentResolver().query( MySchedule.buildMyScheduleUri(mContext, mAccountName), MyScheduleQuery.PROJECTION, null, null, null); if (scheduleData == null) { return false; } // Although we have a dirty flag per item, we need all schedule to sync, because it's all // sync'ed at once to a file on AppData folder. We only use the dirty flag to decide if // the local content was changed or not. If it was, we replace the remote content. boolean hasPendingLocalData = false; ArrayList<UserAction> actions = new ArrayList<UserAction>(); while (scheduleData.moveToNext()) { UserAction userAction = new UserAction(); userAction.sessionId = scheduleData.getString(MyScheduleQuery.SESSION_ID); Integer inSchedule = scheduleData.getInt(MyScheduleQuery.IN_SCHEDULE); if (inSchedule == 0) { userAction.type = UserAction.TYPE.REMOVE_STAR; } else { userAction.type = UserAction.TYPE.ADD_STAR; } userAction.requiresSync = scheduleData.getInt(MyScheduleQuery.DIRTY_FLAG) == 1; actions.add(userAction); if (!hasPendingLocalData && userAction.requiresSync) { hasPendingLocalData = true; } } scheduleData.close(); Log.d(TAG, "Starting Drive AppData sync. hasPendingData = " + hasPendingLocalData); boolean dataChanged = syncImpl(actions, hasPendingLocalData); if (hasPendingLocalData) { resetDirtyFlag(actions); // Notify other devices via GCM ServerUtilities.notifyUserDataChanged(mContext); } if (dataChanged) { LOGD(TAG, "Notifying changes on paths related to user data on Content Resolver."); ContentResolver resolver = mContext.getContentResolver(); for (String path : ScheduleContract.USER_DATA_RELATED_PATHS) { Uri uri = ScheduleContract.BASE_CONTENT_URI.buildUpon().appendPath(path).build(); resolver.notifyChange(uri, null); } mContext.sendBroadcast(ScheduleWidgetProvider.getRefreshBroadcastIntent(mContext, false)); } return dataChanged; } private void resetDirtyFlag(ArrayList<UserAction> actions) { ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); for (UserAction action: actions) { ContentProviderOperation op = ContentProviderOperation.newUpdate( ScheduleContract.addCallerIsSyncAdapterParameter( MySchedule.buildMyScheduleUri(mContext, mAccountName))) .withSelection(MySchedule.SESSION_ID + "=? AND " + MySchedule.MY_SCHEDULE_IN_SCHEDULE + "=?", new String[]{action.sessionId, action.type == UserAction.TYPE.ADD_STAR ? "1" : "0"}) .withValue(MySchedule.MY_SCHEDULE_DIRTY_FLAG, 0) .build(); LOGD(TAG, op.toString()); ops.add(op); } try { ContentProviderResult[] result = mContext.getContentResolver().applyBatch( ScheduleContract.CONTENT_AUTHORITY, ops); LOGD(TAG, "Result of cleaning dirty flags is "+ Arrays.toString(result)); } catch (Exception ex) { LOGW(TAG, "Could not update dirty flags. Ignoring.", ex); } } private interface MyScheduleQuery { String[] PROJECTION = { MySchedule.SESSION_ID, MySchedule.MY_SCHEDULE_IN_SCHEDULE, MySchedule.MY_SCHEDULE_DIRTY_FLAG, }; int SESSION_ID = 0; int IN_SCHEDULE= 1; int DIRTY_FLAG = 2; } }