/* * Copyright (c) 2016 Google Inc. * * 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.firebase; import android.content.Context; import android.content.Intent; import com.firebase.client.DataSnapshot; import com.firebase.client.Firebase; import com.firebase.client.FirebaseError; import com.firebase.client.FirebaseException; import com.google.samples.apps.iosched.gcm.GCMRegistrationIntentService; import com.google.samples.apps.iosched.sync.userdata.UserAction; import com.google.samples.apps.iosched.sync.userdata.util.UserData; import com.google.samples.apps.iosched.sync.userdata.util.UserDataHelper; import com.google.samples.apps.iosched.util.AccountUtils; import com.google.samples.apps.iosched.util.FirebaseUtils; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import static com.google.samples.apps.iosched.util.LogUtils.LOGI; import static com.google.samples.apps.iosched.util.LogUtils.LOGW; import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag; /** * Extracts remote user data from a Firebase {@link DataSnapshot}, which is provided as an argument * when this is constructed. Merges remote user data with local user data, and it updates both * Firebase and the local DB to ensure data is consistent in both places. */ public class FirebaseDataReconciler { private static final String TAG = makeLogTag(FirebaseDataReconciler.class); /** * The {@link UserAction}s that triggered the sync. */ private List<UserAction> mActions; /** * The remote user data snapshot obtained from Firebase. */ private DataSnapshot mRemoteDataSnapshot; /** * The context of the current state of the application. */ private Context mContext; /** * The name of the currently chosen user account. */ private String mAccountName; /** * Holds user data retrieved from the local {@link android.content.ContentProvider}. */ private UserData mLocalUserData; /** * Holds user data retrieved from Firebase. */ private UserData mRemoteUserData; /** * Holds data generated by merging local and remote user data. */ private UserData mMergedUserData; /** * Helper for merging local and remote data. */ private MergeHelper mMergeHelper; /** * Constructor. * * @param context The {@link Context}. * @param accountName The name associated with the currently chosen account. * @param actions The list of {@link UserAction}s that triggered the sync. * @param remoteDataSnapshot The {@link DataSnapshot} of the remote user data stored in * Firebase. */ public FirebaseDataReconciler(Context context, String accountName, List<UserAction> actions, DataSnapshot remoteDataSnapshot) { this.mContext = context; this.mAccountName = accountName; this.mActions = actions; this.mRemoteDataSnapshot = remoteDataSnapshot; } /** * Parses the {@link DataSnapshot} object returned by Firebase and sets the remote user data * values in a {@link UserData} object. * * @return this (for method chaining). */ protected FirebaseDataReconciler buildRemoteDataObject() { mRemoteUserData = new UserData(); mRemoteUserData.setGcmKey((String) mRemoteDataSnapshot.child( UserData.JSON_ATTRIBUTE_GCM_KEY).getValue()); mRemoteUserData.setStarredSessions(getRemoteStarredSessionsMap()); mRemoteUserData.setViewedVideoIds(setRemoteUserDataItem(mRemoteDataSnapshot, UserData.JSON_ATTRIBUTE_VIEWED_VIDEOS)); return this; } /** * Extracts the starred sessions data from a {@link DataSnapshot} and returns the result as a * map where the keys are session IDs and the values are {@link UserData.StarredSession} * objects. */ private Map<String, UserData.StarredSession> getRemoteStarredSessionsMap() { // Store each starred session and the associated StarredSession data in a map. Map<String, UserData.StarredSession> starredSessionsMap = new HashMap<>(); // Get starred sessions snapshot. DataSnapshot starredSessionsDataSnapshot = mRemoteDataSnapshot.child( FirebaseUtils.FIREBASE_NODE_MY_SESSIONS); // Get snapshot of each starred session. for (DataSnapshot singleSessionDataSnapshot : starredSessionsDataSnapshot.getChildren()) { DataSnapshot inScheduleSnapshot = singleSessionDataSnapshot.child( FirebaseUtils.FIREBASE_NODE_IN_SCHEDULE); if (inScheduleSnapshot.getValue() != null) { UserData.StarredSession starredSession = new UserData.StarredSession( (boolean) inScheduleSnapshot.getValue(), (Long) singleSessionDataSnapshot.child (FirebaseUtils.FIREBASE_NODE_TIMESTAMP).getValue()); starredSessionsMap.put(singleSessionDataSnapshot.getKey(), starredSession); } } return starredSessionsMap; } /** * Gets the data stored in the local Db. * * @return this (for method chaining). */ public FirebaseDataReconciler buildLocalDataObject() { mLocalUserData = UserDataHelper.getUserData(mActions); mLocalUserData.setGcmKey(AccountUtils.getGcmKey(mContext, mAccountName)); return this; } /** * Merges user data from the local DB with remote data from Firebase. * * @return this (for method chaining). */ public FirebaseDataReconciler merge() { mMergedUserData = new UserData(); mMergeHelper = new MergeHelper(mLocalUserData, mRemoteUserData, mMergedUserData, FirebaseUtils.getFirebaseUid(mContext)); mMergeHelper.mergeGCMKeys(); mMergeHelper.mergeUnsyncedActions(mActions); LOGI(TAG, "local user data = " + UserDataHelper.toJsonString(mLocalUserData)); LOGI(TAG, "remote user data = " + UserDataHelper.toJsonString(mRemoteUserData)); LOGI(TAG, "merged user data = " + UserDataHelper.toJsonString(mMergedUserData)); return this; } /** * Updates the remote Firebase db if the remote data has become stale. * * @return this (for method chaining). */ public FirebaseDataReconciler updateRemote() { if (remoteDataChanged()) { try { Firebase firebaseRef = new Firebase(FirebaseUtils.getFirebaseUrl(mContext, mAccountName)); firebaseRef.updateChildren(mMergeHelper.getPendingFirebaseUpdatesMap(), new Firebase.CompletionListener() { @Override public void onComplete(final FirebaseError firebaseError, final Firebase firebase) { if (firebaseError == null) { LOGI(TAG, "User data updated in Firebase"); } else { LOGW(TAG, "User data NOT updated in Firebase: firebaseError = " + firebaseError); } } }); } catch (FirebaseException e) { LOGW(TAG, "Could not update Firebase: " + e); } } else { LOGI(TAG, "No changes to remote data. Not updating Firebase."); } return this; } /** * Updates the local DB if the local data has become stale. Updates gcm key in {@link * android.content.SharedPreferences}. * * @return this (for method chaining). */ public FirebaseDataReconciler updateLocal() { if (mMergeHelper.isLocalGcmKeyUpdateNeeded()) { LOGI(TAG, "Updating local gcm key after sync"); AccountUtils.setGcmKey(mContext, mAccountName, mMergedUserData.getGcmKey()); LOGI(TAG, "triggering GCM registration after sync"); mContext.startService(new Intent(mContext, GCMRegistrationIntentService.class)); } if (localDataChanged()) { LOGI(TAG, "Updating local user data after merge."); UserDataHelper.setLocalUserData(mContext, mMergedUserData, mAccountName); } else { LOGI(TAG, "No changes to local data. Not updating ContentProvider."); } return this; } /** * Returns whether local user data changed after the sync. */ public boolean localDataChanged() { return !mMergedUserData.equals(mLocalUserData); } /** * Returns if Firebase data changed after the sync. */ public boolean remoteDataChanged() { return !mMergedUserData.equals(mRemoteUserData); } /** * Utility function that parses the {@link DataSnapshot} object returned by Firebase, locates a * child DataSnapshot, and sets the value of a single * {@link com.google.samples.apps.iosched.sync.userdata.util.UserData} attribute. * * @param dataSnapshot The {@link DataSnapshot} object returned by Firebase. * @param childPath The path used to find a nested {@link DataSnapshot}. * @return A set containing the values found at the {@link DataSnapshot} obtained using {@code * childPath}. */ private Set<String> setRemoteUserDataItem(DataSnapshot dataSnapshot, String childPath) { Set<String> dataSet = new HashSet<>(); DataSnapshot childDataSnapshot = dataSnapshot.child(childPath); for (DataSnapshot record : childDataSnapshot.getChildren()) { dataSet.add(record.getKey()); } return dataSet; } }