Request sync when there's no mediaItem in a mediaSet.

This is to fix the problem where media items haven't been sync'ed when the album
set or album is viewed for the first time.

+ Add MediaSet.SyncListener.
+ Make AlbumPage and AlbumSetPage implement SyncListener.
+ Implement requestSync() for ComboAlbum and ComboAlbumSet.
+ add ActivityState.isDestroyed(). This also fixes the problem where
  StateManager.finishState() may be called twice.

Bug: 5337899
Change-Id: I25364c3ac25721a2650701c5d7931bfb6daa9303
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 26d80e7..74ba1f3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -199,6 +199,11 @@
     <!-- This toast message is shown when network connection is lost while doing clustering -->
     <string name="no_connectivity">Some locations couldn\'t be identified due to network problems.</string>
 
+    <!-- This toast message is shown when failed to load the album data. [CHAR LIMIT=NONE] -->
+    <string name="sync_album_error">Failed to download the photos in this album. Please retry later.</string>
+    <!-- This toast message is shown when failed to load the album list data. [CHAR LIMIT=NONE] -->
+    <string name="sync_album_set_error">Failed to download the list of albums. Please retry later.</string>
+
     <!-- The title of the menu item to let user choose the which portion of
          the media items the user wants to see. When pressed, a submenu will
          appear and user can choose one of "show images only",
diff --git a/src/com/android/gallery3d/app/ActivityState.java b/src/com/android/gallery3d/app/ActivityState.java
index 6a0c72c..519eaff 100644
--- a/src/com/android/gallery3d/app/ActivityState.java
+++ b/src/com/android/gallery3d/app/ActivityState.java
@@ -46,6 +46,8 @@
         ResultEntry next;
     }
 
+    private boolean mDestroyed = false;
+
     protected ActivityState() {
     }
 
@@ -139,5 +141,10 @@
     }
 
     protected void onDestroy() {
+        mDestroyed = true;
+    }
+
+    boolean isDestroyed() {
+        return mDestroyed;
     }
 }
diff --git a/src/com/android/gallery3d/app/AlbumDataAdapter.java b/src/com/android/gallery3d/app/AlbumDataAdapter.java
index 9934cf8..42388ea 100644
--- a/src/com/android/gallery3d/app/AlbumDataAdapter.java
+++ b/src/com/android/gallery3d/app/AlbumDataAdapter.java
@@ -147,7 +147,6 @@
             mContentStart = contentStart;
             mContentEnd = contentEnd;
         }
-        MediaItem[] data = mData;
         long[] itemVersion = mItemVersion;
         long[] setVersion = mSetVersion;
         if (contentStart >= end || start >= contentEnd) {
@@ -168,9 +167,6 @@
     public void setActiveWindow(int start, int end) {
         if (start == mActiveStart && end == mActiveEnd) return;
 
-        mActiveStart = start;
-        mActiveEnd = end;
-
         Utils.assertTrue(start <= end
                 && end - start <= mData.length && end <= mSize);
 
diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java
index 1f2c291..faaa800 100644
--- a/src/com/android/gallery3d/app/AlbumPage.java
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -60,7 +60,7 @@
 import java.util.Random;
 
 public class AlbumPage extends ActivityState implements GalleryActionBar.ClusterRunner,
-        SelectionManager.SelectionListener {
+        SelectionManager.SelectionListener, MediaSet.SyncListener {
     @SuppressWarnings("unused")
     private static final String TAG = "AlbumPage";
 
@@ -102,7 +102,7 @@
     private ProgressDialog mProgressDialog;
     private Future<?> mPendingTask;
 
-    private Future<Void> mSyncTask = null;
+    private Future<Integer> mSyncTask = null;
 
     private GLView mRootPane = new GLView() {
         private float mMatrix[] = new float[16];
@@ -360,6 +360,7 @@
 
     @Override
     protected void onDestroy() {
+        super.onDestroy();
         if (mAlbumDataAdapter != null) {
             mAlbumDataAdapter.setLoadingListener(null);
         }
@@ -566,6 +567,24 @@
         mActionModeHandler.updateSupportedOperation(path, selected);
     }
 
+    @Override
+    public void onSyncDone(final MediaSet mediaSet, final int resultCode) {
+        Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName()) + " result="
+                + resultCode);
+        ((Activity) mActivity).runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                if (!mIsActive) return;
+                mediaSet.notifyContentChanged(); // force reload to handle spinner
+
+                if (resultCode == MediaSet.SYNC_RESULT_ERROR) {
+                    Toast.makeText((Context) mActivity, R.string.sync_album_error,
+                            Toast.LENGTH_LONG).show();
+                }
+            }
+        });
+    }
+
     private class MyLoadingListener implements LoadingListener {
         @Override
         public void onLoadingStarted() {
@@ -577,7 +596,7 @@
             if (!mIsActive) return;
             if (mAlbumDataAdapter.size() == 0) {
                 if (mSyncTask == null) {
-                    mSyncTask = mMediaSet.requestSync();
+                    mSyncTask = mMediaSet.requestSync(AlbumPage.this);
                 }
                 if (mSyncTask.isDone()){
                     Toast.makeText((Context) mActivity,
diff --git a/src/com/android/gallery3d/app/AlbumSetPage.java b/src/com/android/gallery3d/app/AlbumSetPage.java
index 74d60c4..6951fa3 100644
--- a/src/com/android/gallery3d/app/AlbumSetPage.java
+++ b/src/com/android/gallery3d/app/AlbumSetPage.java
@@ -54,10 +54,12 @@
 import com.android.gallery3d.ui.SelectionManager;
 import com.android.gallery3d.ui.SlotView;
 import com.android.gallery3d.ui.StaticBackground;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
 
 public class AlbumSetPage extends ActivityState implements
         SelectionManager.SelectionListener, GalleryActionBar.ClusterRunner,
-        EyePosition.EyePositionListener {
+        EyePosition.EyePositionListener, MediaSet.SyncListener {
     @SuppressWarnings("unused")
     private static final String TAG = "AlbumSetPage";
 
@@ -100,6 +102,8 @@
     private float mY;
     private float mZ;
 
+    private Future<Integer> mSyncTask = null;
+
     private final GLView mRootPane = new GLView() {
         private final float mMatrix[] = new float[16];
 
@@ -291,6 +295,10 @@
         DetailsHelper.pause();
         GalleryActionBar actionBar = mActivity.getGalleryActionBar();
         if (actionBar != null) actionBar.hideClusterMenu();
+        if (mSyncTask != null) {
+            mSyncTask.cancel();
+            mSyncTask = null;
+        }
     }
 
     @Override
@@ -552,6 +560,26 @@
         mDetailsHelper.show();
     }
 
+    @Override
+    public void onSyncDone(final MediaSet mediaSet, final int resultCode) {
+        if (resultCode == MediaSet.SYNC_RESULT_ERROR) {
+            Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName()) + " result="
+                    + resultCode);
+        }
+        ((Activity) mActivity).runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                if (!mIsActive) return;
+                mediaSet.notifyContentChanged(); // force reload to handle spinner
+
+                if (resultCode == MediaSet.SYNC_RESULT_ERROR) {
+                    Toast.makeText((Context) mActivity, R.string.sync_album_set_error,
+                            Toast.LENGTH_LONG).show();
+                }
+            }
+        });
+    }
+
     private class MyLoadingListener implements LoadingListener {
         public void onLoadingStarted() {
             GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
@@ -559,11 +587,21 @@
 
         public void onLoadingFinished() {
             if (!mIsActive) return;
-            GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
-            if (mAlbumSetDataAdapter.size() == 0) {
-                Toast.makeText((Context) mActivity,
-                        R.string.empty_album, Toast.LENGTH_LONG).show();
-                if (mActivity.getStateManager().getStateCount() > 1) {
+
+            if (mSyncTask == null) {
+                // Request sync in case the mediaSet hasn't been sync'ed before.
+                mSyncTask = mMediaSet.requestSync(AlbumSetPage.this);
+            }
+            if (mSyncTask.isDone()){
+                // The mediaSet is in sync. Turn off the loading indicator.
+                GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
+
+                // Only show toast when there's no album and we are going to finish
+                // the page. Toast is redundant if we are going to stay on this page.
+                if ((mAlbumSetDataAdapter.size() == 0)
+                        && (mActivity.getStateManager().getStateCount() > 1)) {
+                    Toast.makeText((Context) mActivity,
+                            R.string.empty_album, Toast.LENGTH_LONG).show();
                     mActivity.getStateManager().finishState(AlbumSetPage.this);
                 }
             }
diff --git a/src/com/android/gallery3d/app/StateManager.java b/src/com/android/gallery3d/app/StateManager.java
index 9c55fbd..556a06a 100644
--- a/src/com/android/gallery3d/app/StateManager.java
+++ b/src/com/android/gallery3d/app/StateManager.java
@@ -161,9 +161,14 @@
     void finishState(ActivityState state) {
         Log.v(TAG, "finishState " + state.getClass());
         if (state != mStack.peek().activityState) {
-            throw new IllegalArgumentException("The stateview to be finished"
-                    + " is not at the top of the stack: " + state + ", "
-                    + mStack.peek().activityState);
+            if (state.isDestroyed()) {
+                Log.d(TAG, "The state is already destroyed");
+                return;
+            } else {
+                throw new IllegalArgumentException("The stateview to be finished"
+                        + " is not at the top of the stack: " + state + ", "
+                        + mStack.peek().activityState);
+            }
         }
 
         // Remove the top state.
diff --git a/src/com/android/gallery3d/data/ComboAlbum.java b/src/com/android/gallery3d/data/ComboAlbum.java
index 8ca2077..69ab62e 100644
--- a/src/com/android/gallery3d/data/ComboAlbum.java
+++ b/src/com/android/gallery3d/data/ComboAlbum.java
@@ -17,6 +17,7 @@
 package com.android.gallery3d.data;
 
 import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.util.Future;
 
 import java.util.ArrayList;
 
@@ -84,4 +85,9 @@
     public void onContentDirty() {
         notifyContentChanged();
     }
+
+    @Override
+    public Future<Integer> requestSync(SyncListener listener) {
+        return requestSyncOnEmptySets(mSets, listener);
+    }
 }
diff --git a/src/com/android/gallery3d/data/ComboAlbumSet.java b/src/com/android/gallery3d/data/ComboAlbumSet.java
index aa19603..16adc12 100644
--- a/src/com/android/gallery3d/data/ComboAlbumSet.java
+++ b/src/com/android/gallery3d/data/ComboAlbumSet.java
@@ -18,6 +18,7 @@
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.util.Future;
 
 // ComboAlbumSet combines multiple media sets into one. It lists all sub
 // media sets from the input album sets.
@@ -77,4 +78,9 @@
     public void onContentDirty() {
         notifyContentChanged();
     }
+
+    @Override
+    public Future<Integer> requestSync(SyncListener listener) {
+        return requestSyncOnEmptySets(mSets, listener);
+    }
 }
diff --git a/src/com/android/gallery3d/data/MediaSet.java b/src/com/android/gallery3d/data/MediaSet.java
index 99f00a0..a54067e 100644
--- a/src/com/android/gallery3d/data/MediaSet.java
+++ b/src/com/android/gallery3d/data/MediaSet.java
@@ -16,9 +16,11 @@
 
 package com.android.gallery3d.data;
 
+import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.util.Future;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.WeakHashMap;
 
 // MediaSet is a directory-like data structure.
@@ -34,6 +36,22 @@
     public static final int MEDIAITEM_BATCH_FETCH_COUNT = 500;
     public static final int INDEX_NOT_FOUND = -1;
 
+    public static final int SYNC_RESULT_SUCCESS = 0;
+    public static final int SYNC_RESULT_CANCELLED = 1;
+    public static final int SYNC_RESULT_ERROR = 2;
+
+    /** Listener to be used with requestSync(SyncListener). */
+    public static interface SyncListener {
+        /**
+         * Called when the sync task completed. Completion may be due to normal termination,
+         * an exception, or cancellation.
+         *
+         * @param mediaSet the MediaSet that's done with sync
+         * @param resultCode one of the SYNC_RESULT_* constants
+         */
+        void onSyncDone(MediaSet mediaSet, int resultCode);
+    }
+
     public MediaSet(Path path, long version) {
         super(path, version);
     }
@@ -190,11 +208,21 @@
         return start;
     }
 
-    public Future<Void> requestSync() {
+    /**
+     * Requests sync on this MediaSet. It returns a Future object that can be used by the caller
+     * to query the status of the sync. The sync result code is one of the SYNC_RESULT_* constants
+     * defined in this class and can be obtained by Future.get().
+     *
+     * Subclasses should perform sync on a different thread.
+     *
+     * The default implementation here returns a Future stub that does nothing and returns
+     * SYNC_RESULT_SUCCESS by get().
+     */
+    public Future<Integer> requestSync(SyncListener listener) {
         return FUTURE_STUB;
     }
 
-    private static final Future<Void> FUTURE_STUB = new Future<Void>() {
+    private static final Future<Integer> FUTURE_STUB = new Future<Integer>() {
         @Override
         public void cancel() {}
 
@@ -209,11 +237,101 @@
         }
 
         @Override
-        public Void get() {
-            return null;
+        public Integer get() {
+            return SYNC_RESULT_SUCCESS;
         }
 
         @Override
         public void waitDone() {}
     };
+
+    protected Future<Integer> requestSyncOnEmptySets(MediaSet[] sets, SyncListener listener) {
+        MultiSetSyncFuture future = new MultiSetSyncFuture(listener);
+        future.requestSyncOnEmptySets(sets);
+        return future;
+    }
+
+    private class MultiSetSyncFuture implements Future<Integer>, SyncListener {
+        private static final String TAG = "Gallery.MultiSetSync";
+
+        private final HashMap<MediaSet, Future<Integer>> mMediaSetMap =
+                new HashMap<MediaSet, Future<Integer>>();
+        private final SyncListener mListener;
+
+        private boolean mIsCancelled = false;
+        private int mResult = -1;
+
+        MultiSetSyncFuture(SyncListener listener) {
+            mListener = listener;
+        }
+
+        synchronized void requestSyncOnEmptySets(MediaSet[] sets) {
+            for (MediaSet set : sets) {
+                if ((set.getMediaItemCount() == 0) && !mMediaSetMap.containsKey(set)) {
+                    // Sync results are handled in this.onSyncDone().
+                    Future<Integer> future = set.requestSync(this);
+                    if (!future.isDone()) {
+                        mMediaSetMap.put(set, future);
+                        Log.d(TAG, "  request sync: " + Utils.maskDebugInfo(set.getName()));
+                    }
+                }
+            }
+            Log.d(TAG, "requestSyncOnEmptySets actual=" + mMediaSetMap.size());
+        }
+
+        @Override
+        public synchronized void cancel() {
+            if (mIsCancelled) return;
+            mIsCancelled = true;
+            for (Future<Integer> future : mMediaSetMap.values()) future.cancel();
+            mMediaSetMap.clear();
+            if (mResult < 0) mResult = SYNC_RESULT_CANCELLED;
+        }
+
+        @Override
+        public synchronized boolean isCancelled() {
+            return mIsCancelled;
+        }
+
+        @Override
+        public synchronized boolean isDone() {
+            return mMediaSetMap.isEmpty();
+        }
+
+        @Override
+        public synchronized Integer get() {
+            waitDone();
+            return mResult;
+        }
+
+        @Override
+        public synchronized void waitDone() {
+            try {
+                while (!isDone()) wait();
+            } catch (InterruptedException e) {
+                Log.d(TAG, "waitDone() interrupted");
+            }
+        }
+
+        // SyncListener callback
+        @Override
+        public void onSyncDone(MediaSet mediaSet, int resultCode) {
+            SyncListener listener = null;
+            synchronized (this) {
+                if (mMediaSetMap.remove(mediaSet) != null) {
+                    Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName())
+                            + " #pending=" + mMediaSetMap.size());
+                    if (resultCode == SYNC_RESULT_ERROR) {
+                        mResult = SYNC_RESULT_ERROR;
+                    }
+                    if (mMediaSetMap.isEmpty()) {
+                        if (mResult < 0) mResult = SYNC_RESULT_SUCCESS;
+                        notifyAll();
+                        listener = mListener;
+                    }
+                }
+            }
+            if (listener != null) listener.onSyncDone(MediaSet.this, mResult);
+        }
+    }
 }