/* * 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.text.TextUtils; import com.firebase.client.DataSnapshot; import com.firebase.client.Firebase; import com.firebase.client.FirebaseError; import com.firebase.client.ValueEventListener; import com.google.samples.apps.iosched.sync.userdata.AbstractUserDataSyncHelper; import com.google.samples.apps.iosched.sync.userdata.UserAction; import com.google.samples.apps.iosched.util.FirebaseUtils; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static com.google.samples.apps.iosched.util.LogUtils.LOGD; 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; /** * Performs Firebase authentication if necessary and syncs local data with remote data in Firebase. * This class queries live Firebase data to perform a sync, explicitly not relying on the {@link * Firebase#keepSynced(boolean)} functionality in order to have more control over conflict * resolution for different types of data being stored. */ public class FirebaseUserDataSyncHelper extends AbstractUserDataSyncHelper implements FirebaseAuthCallbacks { private static final String TAG = makeLogTag(FirebaseUserDataSyncHelper.class); /** * Wait time for the Firebase sync to complete before we exit from {@code syncImpl()}. */ public static final int AWAIT_TIMEOUT_IN_MILLISECONDS = 10000; // 10 seconds. /** * Tracks if the sync process changed local data. */ private boolean mDataChanged = false; /** * Lock used to prevent {@code syncImpl()} from exiting before we're done syncing with * Firebase. */ private CountDownLatch mCountDownLatch; /** * A list of {@link UserAction}s that are involved in the data sync. */ private List<UserAction> mActions; /** * Constructor. * * @param context The {@link Context}. * @param accountName The name associated with the currently chosen account. */ public FirebaseUserDataSyncHelper(Context context, String accountName) { super(context, accountName); } @Override protected boolean syncImpl(final List<UserAction> actions, final boolean hasPendingLocalData) { mActions = actions; mCountDownLatch = new CountDownLatch(1); // The Firebase shard where data is synced. String firebaseUrl = FirebaseUtils.getFirebaseUrl(mContext, mAccountName); if (TextUtils.isEmpty(firebaseUrl)) { LOGW(TAG, "Cannot proceed with user data sync: Firebase url is not known."); return mDataChanged; } // The Firebase reference used to sync data. final Firebase firebaseRef = new Firebase(firebaseUrl); boolean authenticated = !TextUtils.isEmpty(FirebaseUtils.getFirebaseUid(mContext)); if (authenticated) { LOGW(TAG, "Already authenticated with Firebase."); performSync(actions); } else { // Authenticate and wait for onAuthSucceeded() to fire before performing sync. new FirebaseAuthHelper(mContext, firebaseRef, this).authenticate(); } try { // Make the current thread wait until we've heard back from Firebase. LOGW(TAG, "Waiting until the latch has counted down to zero"); mCountDownLatch.await(AWAIT_TIMEOUT_IN_MILLISECONDS, TimeUnit.MILLISECONDS); } catch (InterruptedException exception) { LOGW(TAG, "Waiting thread awakened prematurely", exception); } LOGD(TAG, "local data changed after sync = " + mDataChanged); return mDataChanged; } /** * Syncs local data with remote data in Firebase. Assumes Firebase authentication has * successfully completed. See {@link FirebaseDataReconciler} for details on how remote and * local data is merged. * * @param actions The user actions that triggered the sync. */ private void performSync(final List<UserAction> actions) { // Do a one-time read of Firebase data (equivalent to an asynchronous query call) located // at /<data_path>/<uid>/. FirebaseUtils.getDataUIDRef(mContext, mAccountName).addListenerForSingleValueEvent( new ValueEventListener() { @Override public void onDataChange(final DataSnapshot dataSnapshot) { FirebaseDataReconciler firebaseDataReconciler = new FirebaseDataReconciler(mContext, mAccountName, actions, dataSnapshot); firebaseDataReconciler.buildRemoteDataObject() .buildLocalDataObject() .merge() .updateRemote() .updateLocal(); FirebaseUserDataSyncHelper.this.mDataChanged = firebaseDataReconciler.localDataChanged(); LOGW(TAG, "Done syncing with Firebase. Decrementing latch count."); mCountDownLatch.countDown(); } @Override public void onCancelled(final FirebaseError firebaseError) { LOGW(TAG, "firebaseError = " + firebaseError); mCountDownLatch.countDown(); } }); } @Override public void onAuthSucceeded(final String uid) { FirebaseUtils.setFirebaseUid(mContext, mAccountName, uid); performSync(mActions); } @Override public void onAuthFailed() { // Clear out the <uid> previously obtained from Firebase. FirebaseUtils.setFirebaseUid(mContext, mAccountName, ""); mCountDownLatch.countDown(); incrementIoExceptions(); } }