/* * 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.archframework.QueryEnum; 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 com.google.samples.apps.iosched.provider.ScheduleContract.MyFeedbackSubmitted; import com.google.samples.apps.iosched.provider.ScheduleContract.MyViewedVideos; import com.google.samples.apps.iosched.provider.ScheduleContractHelper; 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 user 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 settings_prefs * * // 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; protected int mIoExceptions = 0; 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() { // Although we have a dirty flag per item, we need all schedule/viewed videos 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<>(); // Get schedule data pending sync. Cursor scheduleData = mContext.getContentResolver().query( MySchedule.buildMyScheduleUri(mAccountName), UserDataQueryEnum.MY_SCHEDULE.getProjection(), null, null, null); if (scheduleData != null) { while (scheduleData.moveToNext()) { UserAction userAction = new UserAction(); userAction.sessionId = scheduleData.getString( scheduleData.getColumnIndex(MySchedule.SESSION_ID)); Integer inSchedule = scheduleData.getInt( scheduleData.getColumnIndex(MySchedule.MY_SCHEDULE_IN_SCHEDULE)); if (inSchedule == 0) { userAction.type = UserAction.TYPE.REMOVE_STAR; } else { userAction.type = UserAction.TYPE.ADD_STAR; } userAction.requiresSync = scheduleData.getInt( scheduleData.getColumnIndex(MySchedule.MY_SCHEDULE_DIRTY_FLAG)) == 1; userAction.timestamp = scheduleData.getLong( scheduleData.getColumnIndex(MySchedule.MY_SCHEDULE_TIMESTAMP)); actions.add(userAction); if (!hasPendingLocalData && userAction.requiresSync) { hasPendingLocalData = true; } } scheduleData.close(); } // Get video viewed data pending sync. Cursor videoViewed = mContext.getContentResolver().query( ScheduleContract.MyViewedVideos.buildMyViewedVideosUri(mAccountName), UserDataQueryEnum.MY_VIEWED_VIDEO.getProjection(), null, null, null); if (videoViewed != null) { while (videoViewed.moveToNext()) { UserAction userAction = new UserAction(); userAction.videoId = videoViewed.getString( videoViewed.getColumnIndex(MyViewedVideos.VIDEO_ID)); userAction.type = UserAction.TYPE.VIEW_VIDEO; userAction.requiresSync = videoViewed.getInt( videoViewed.getColumnIndex( MyViewedVideos.MY_VIEWED_VIDEOS_DIRTY_FLAG)) == 1; actions.add(userAction); if (!hasPendingLocalData && userAction.requiresSync) { hasPendingLocalData = true; } } videoViewed.close(); } // Get feedback submitted data pending sync. Cursor feedbackSubmitted = mContext.getContentResolver().query( MyFeedbackSubmitted.buildMyFeedbackSubmittedUri(mAccountName), UserDataQueryEnum.MY_FEEDBACK_SUBMITTED.getProjection(), null, null, null); if (feedbackSubmitted != null) { while (feedbackSubmitted.moveToNext()) { UserAction userAction = new UserAction(); userAction.sessionId = feedbackSubmitted.getString( feedbackSubmitted.getColumnIndex(MyFeedbackSubmitted.SESSION_ID)); userAction.type = UserAction.TYPE.VIEW_VIDEO; userAction.requiresSync = feedbackSubmitted.getInt( feedbackSubmitted.getColumnIndex( MyFeedbackSubmitted.MY_FEEDBACK_SUBMITTED_DIRTY_FLAG)) == 1; actions.add(userAction); if (!hasPendingLocalData && userAction.requiresSync) { hasPendingLocalData = true; } } feedbackSubmitted.close(); } Log.d(TAG, "Starting User Data 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<>(); for (UserAction action : actions) { Uri baseUri; String with; String[] withSelectionValue; String dirtyField; if (action.type == UserAction.TYPE.VIEW_VIDEO) { baseUri = MyViewedVideos.buildMyViewedVideosUri(mAccountName); with = MyViewedVideos.VIDEO_ID + "=?"; withSelectionValue = new String[]{action.videoId}; dirtyField = MyViewedVideos.MY_VIEWED_VIDEOS_DIRTY_FLAG; } else if (action.type == UserAction.TYPE.SUBMIT_FEEDBACK) { baseUri = MyFeedbackSubmitted.buildMyFeedbackSubmittedUri(mAccountName); with = MyFeedbackSubmitted.SESSION_ID + "=?"; withSelectionValue = new String[]{action.sessionId}; dirtyField = MyFeedbackSubmitted.MY_FEEDBACK_SUBMITTED_DIRTY_FLAG; } else { baseUri = MySchedule.buildMyScheduleUri(mAccountName); with = MySchedule.SESSION_ID + "=? AND " + MySchedule.MY_SCHEDULE_IN_SCHEDULE + "=?"; withSelectionValue = new String[]{action.sessionId, action.type == UserAction.TYPE.ADD_STAR ? "1" : "0"}; dirtyField = MySchedule.MY_SCHEDULE_DIRTY_FLAG; } ContentProviderOperation op = ContentProviderOperation.newUpdate( ScheduleContractHelper.setUriAsCalledFromSyncAdapter(baseUri)) .withSelection(with, withSelectionValue) .withValue(dirtyField, 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); } } public void incrementIoExceptions() { mIoExceptions++; } public int getIoExcpetions() { return mIoExceptions; } private enum UserDataQueryEnum implements QueryEnum { MY_SCHEDULE(0, new String[]{MySchedule.SESSION_ID, MySchedule.MY_SCHEDULE_IN_SCHEDULE, MySchedule.MY_SCHEDULE_DIRTY_FLAG, MySchedule.MY_SCHEDULE_TIMESTAMP}), MY_FEEDBACK_SUBMITTED(0, new String[]{MyFeedbackSubmitted.SESSION_ID, MyFeedbackSubmitted.MY_FEEDBACK_SUBMITTED_DIRTY_FLAG}), MY_VIEWED_VIDEO(0, new String[]{MyViewedVideos.VIDEO_ID, MyViewedVideos.MY_VIEWED_VIDEOS_DIRTY_FLAG}); private int id; private String[] projection; UserDataQueryEnum(int id, String[] projection) { this.id = id; this.projection = projection; } @Override public int getId() { return id; } @Override public String[] getProjection() { return projection; } } }