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