Remove panorama checks from supported operations.

 Bug 7351383
 Bug 7349438
 Move panorama support checks from getSupportedOperations so
 that calls to getSupportedOperations are consistent. Panorama
 checks are moved to only based on callbacks.

Change-Id: Id9ff138204df84c6fb0a4c971dcea59f1220aee2
diff --git a/src/com/android/gallery3d/app/GalleryAppImpl.java b/src/com/android/gallery3d/app/GalleryAppImpl.java
index c4507b3..443bc47 100644
--- a/src/com/android/gallery3d/app/GalleryAppImpl.java
+++ b/src/com/android/gallery3d/app/GalleryAppImpl.java
@@ -63,6 +63,7 @@
                 state, PackageManager.DONT_KILL_APP);
 
         mStitchingProgressManager = LightCycleHelper.createStitchingManagerInstance(this);
+        mStitchingProgressManager.addChangeListener(getDataManager());
     }
 
     @Override
diff --git a/src/com/android/gallery3d/app/PanoramaMetadataSupport.java b/src/com/android/gallery3d/app/PanoramaMetadataSupport.java
new file mode 100644
index 0000000..d404225
--- /dev/null
+++ b/src/com/android/gallery3d/app/PanoramaMetadataSupport.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.gallery3d.app;
+
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.PanoramaMetadataJob;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata;
+
+import java.util.ArrayList;
+
+/**
+ * This class breaks out the off-thread panorama support checks so that the
+ * complexity can be shared between UriImage and LocalImage, which need to
+ * support panoramas.
+ */
+public class PanoramaMetadataSupport implements FutureListener<PanoramaMetadata> {
+    private Object mLock = new Object();
+    private Future<PanoramaMetadata> mGetPanoMetadataTask;
+    private PanoramaMetadata mPanoramaMetadata;
+    private ArrayList<PanoramaSupportCallback> mCallbacksWaiting;
+    private MediaObject mMediaObject;
+
+    public PanoramaMetadataSupport(MediaObject mediaObject) {
+        mMediaObject = mediaObject;
+    }
+
+    public void getPanoramaSupport(GalleryApp app, PanoramaSupportCallback callback) {
+        synchronized (mLock) {
+            if (mPanoramaMetadata != null) {
+                callback.panoramaInfoAvailable(mMediaObject, mPanoramaMetadata.mUsePanoramaViewer,
+                        mPanoramaMetadata.mIsPanorama360);
+            } else {
+                if (mCallbacksWaiting == null) {
+                    mCallbacksWaiting = new ArrayList<PanoramaSupportCallback>();
+                    mGetPanoMetadataTask = app.getThreadPool().submit(
+                            new PanoramaMetadataJob(app.getAndroidContext(),
+                                    mMediaObject.getContentUri()), this);
+
+                }
+                mCallbacksWaiting.add(callback);
+            }
+        }
+    }
+
+    public void clearCachedValues() {
+        synchronized (mLock) {
+            if (mPanoramaMetadata != null) {
+                mPanoramaMetadata = null;
+            } else if (mGetPanoMetadataTask != null) {
+                mGetPanoMetadataTask.cancel();
+                for (PanoramaSupportCallback cb : mCallbacksWaiting) {
+                    cb.panoramaInfoAvailable(mMediaObject, false, false);
+                }
+                mGetPanoMetadataTask = null;
+                mCallbacksWaiting = null;
+            }
+        }
+    }
+
+    @Override
+    public void onFutureDone(Future<PanoramaMetadata> future) {
+        synchronized (mLock) {
+            mPanoramaMetadata = future.get();
+            for (PanoramaSupportCallback cb : mCallbacksWaiting) {
+                cb.panoramaInfoAvailable(mMediaObject,
+                        mPanoramaMetadata.mUsePanoramaViewer,
+                        mPanoramaMetadata.mIsPanorama360);
+            }
+            mGetPanoMetadataTask = null;
+            mCallbacksWaiting = null;
+        }
+   }
+}
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
index a548e70..f900c40 100644
--- a/src/com/android/gallery3d/app/PhotoPage.java
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -48,7 +48,7 @@
 import com.android.gallery3d.data.MediaDetails;
 import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.data.MediaObject;
-import com.android.gallery3d.data.MediaObject.SupportedOperationsListener;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.data.MtpSource;
 import com.android.gallery3d.data.Path;
@@ -91,6 +91,8 @@
     private static final int MSG_UPDATE_PHOTO_UI = 12;
     private static final int MSG_UPDATE_PROGRESS = 13;
     private static final int MSG_UPDATE_DEFERRED = 14;
+    private static final int MSG_UPDATE_SHARE_URI = 15;
+    private static final int MSG_UPDATE_PANORAMA_UI = 16;
 
     private static final int HIDE_BARS_TIMEOUT = 3500;
     private static final int UNFREEZE_GLROOT_TIMEOUT = 250;
@@ -166,7 +168,7 @@
     private boolean mSkipUpdateCurrentPhoto = false;
     private static final long CAMERA_SWITCH_CUTOFF_THRESHOLD_MS = 300;
 
-    private static final long DEFERRED_UPDATE_MS = 150;
+    private static final long DEFERRED_UPDATE_MS = 250;
     private boolean mDeferredUpdateWaiting = false;
     private long mDeferUpdateUntil = Long.MAX_VALUE;
 
@@ -182,20 +184,37 @@
             new MyMenuVisibilityListener();
     private UpdateProgressListener mProgressListener;
 
-    private SupportedOperationsListener mSupportedOperationsListener =
-        new SupportedOperationsListener() {
-            @Override
-            public void onChange(MediaObject item, int operations) {
-                if (item == mCurrentPhoto) {
-                    if (mPhotoView.getFilmMode()
-                            && SystemClock.uptimeMillis() < mDeferUpdateUntil) {
-                        requestDeferredUpdate();
-                    } else {
-                        mHandler.sendEmptyMessage(MSG_UPDATE_PHOTO_UI);
-                    }
-                }
+    private final PanoramaSupportCallback mUpdatePanoramaMenuItemsCallback = new PanoramaSupportCallback() {
+        @Override
+        public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+                boolean isPanorama360) {
+            if (mediaObject == mCurrentPhoto) {
+                mHandler.obtainMessage(MSG_UPDATE_PANORAMA_UI, isPanorama360 ? 1 : 0, 0,
+                        mediaObject).sendToTarget();
             }
-        };
+        }
+    };
+
+    private final PanoramaSupportCallback mRefreshBottomControlsCallback = new PanoramaSupportCallback() {
+        @Override
+        public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+                boolean isPanorama360) {
+            if (mediaObject == mCurrentPhoto) {
+                mHandler.obtainMessage(MSG_REFRESH_BOTTOM_CONTROLS, isPanorama ? 1 : 0, 0, mediaObject).sendToTarget();
+            }
+        }
+    };
+
+    private final PanoramaSupportCallback mUpdateShareURICallback = new PanoramaSupportCallback() {
+        @Override
+        public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+                boolean isPanorama360) {
+            if (mediaObject == mCurrentPhoto) {
+                mHandler.obtainMessage(MSG_UPDATE_SHARE_URI, isPanorama360 ? 1 : 0, 0, mediaObject)
+                        .sendToTarget();
+            }
+        }
+    };
 
     public static interface Model extends PhotoView.Model {
         public void resume();
@@ -230,8 +249,9 @@
         }
 
         private void sendUpdate(Uri uri, int message) {
-            boolean isCurrentPhoto = mCurrentPhoto instanceof LocalImage
-                    && mCurrentPhoto.getContentUri().equals(uri);
+            MediaObject currentPhoto = mCurrentPhoto;
+            boolean isCurrentPhoto = currentPhoto instanceof LocalImage
+                    && currentPhoto.getContentUri().equals(uri);
             if (isCurrentPhoto) {
                 mHandler.sendEmptyMessage(message);
             }
@@ -278,7 +298,9 @@
                         break;
                     }
                     case MSG_REFRESH_BOTTOM_CONTROLS: {
-                        if (mBottomControls != null) mBottomControls.refresh();
+                        if (mCurrentPhoto == message.obj && mBottomControls != null) {
+                            mBottomControls.refresh(message.arg1 != 0);
+                        }
                         break;
                     }
                     case MSG_ON_FULL_SCREEN_CHANGED: {
@@ -347,6 +369,28 @@
                         updateProgressBar();
                         break;
                     }
+                    case MSG_UPDATE_SHARE_URI: {
+                        if (mCurrentPhoto == message.obj) {
+                            boolean isPanorama360 = message.arg1 != 0;
+                            Uri contentUri = mCurrentPhoto.getContentUri();
+                            Intent panoramaIntent = null;
+                            if (isPanorama360) {
+                                panoramaIntent = createSharePanoramaIntent(contentUri);
+                            }
+                            Intent shareIntent = createShareIntent(mCurrentPhoto);
+
+                            mActionBar.setShareIntents(panoramaIntent, shareIntent);
+                            setNfcBeamPushUri(contentUri);
+                        }
+                        break;
+                    }
+                    case MSG_UPDATE_PANORAMA_UI: {
+                        if (mCurrentPhoto == message.obj) {
+                            boolean isPanorama360 = message.arg1 != 0;
+                            updatePanoramaUI(isPanorama360);
+                        }
+                        break;
+                    }
                     default: throw new AssertionError(message.what);
                 }
             }
@@ -540,18 +584,17 @@
     }
 
     @Override
-    public boolean canDisplayBottomControl(int control) {
-        if (mCurrentPhoto == null) return false;
+    public boolean canDisplayBottomControl(int control, boolean isPanorama) {
+        if (mCurrentPhoto == null) {
+            return false;
+        }
         switch(control) {
             case R.id.photopage_bottom_control_edit:
                 return mHaveImageEditor && mShowBars
-                        && (mCurrentPhoto.getSupportedOperations()
-                        & MediaItem.SUPPORT_EDIT) != 0
-                        && mCurrentPhoto.getMediaType()
-                        == MediaObject.MEDIA_TYPE_IMAGE;
+                        && (mCurrentPhoto.getSupportedOperations() & MediaItem.SUPPORT_EDIT) != 0
+                        && mCurrentPhoto.getMediaType() == MediaObject.MEDIA_TYPE_IMAGE;
             case R.id.photopage_bottom_control_panorama:
-                return (mCurrentPhoto.getSupportedOperations()
-                        & MediaItem.SUPPORT_PANORAMA) != 0;
+                return isPanorama;
             default:
                 return false;
         }
@@ -593,24 +636,18 @@
         mNfcPushUris[0] = uri;
     }
 
-    private Intent createShareIntent(Path path) {
-        DataManager manager = mActivity.getDataManager();
-        int type = manager.getMediaType(path);
+    private static Intent createShareIntent(MediaObject mediaObject) {
+        int type = mediaObject.getMediaType();
         return new Intent(Intent.ACTION_SEND)
                 .setType(MenuExecutor.getMimeType(type))
-                .putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path))
+                .putExtra(Intent.EXTRA_STREAM, mediaObject.getContentUri())
                 .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
     }
 
-    private Intent createSharePanoramaIntent(Path path) {
-        DataManager manager = mActivity.getDataManager();
-        int supported = manager.getSupportedOperations(path);
-        if ((supported & MediaObject.SUPPORT_PANORAMA360) == 0) {
-            return null;
-        }
+    private static Intent createSharePanoramaIntent(Uri contentUri) {
         return new Intent(Intent.ACTION_SEND)
                 .setType(GalleryUtils.MIME_TYPE_PANORAMA360)
-                .putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path))
+                .putExtra(Intent.EXTRA_STREAM, contentUri)
                 .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
     }
 
@@ -634,15 +671,6 @@
                 REQUEST_EDIT);
     }
 
-    private void updateShareURI(Path path) {
-        DataManager manager = mActivity.getDataManager();
-        mActionBar.setShareIntents(
-                createSharePanoramaIntent(path),
-                createShareIntent(path));
-        Uri uri = manager.getContentUri(path);
-        setNfcBeamPushUri(uri);
-    }
-
     private void requestDeferredUpdate() {
         mDeferUpdateUntil = SystemClock.uptimeMillis() + DEFERRED_UPDATE_MS;
         if (!mDeferredUpdateWaiting) {
@@ -663,25 +691,20 @@
         }
 
         updateMenuOperations();
-        if (mBottomControls != null) mBottomControls.refresh();
+        refreshBottomControlsWhenReady();
         if (mShowDetails) {
             mDetailsHelper.reloadDetails();
         }
         if ((mSecureAlbum == null)
                 && (mCurrentPhoto.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) {
-            updateShareURI(mCurrentPhoto.getPath());
+            mCurrentPhoto.getPanoramaSupport(mUpdateShareURICallback);
         }
         updateProgressBar();
     }
 
     private void updateCurrentPhoto(MediaItem photo) {
         if (mCurrentPhoto == photo) return;
-        if (mCurrentPhoto != null) {
-            mCurrentPhoto.setSupportedOperationsListener(null);
-        }
         mCurrentPhoto = photo;
-        mCurrentPhoto.setSupportedOperationsListener(
-                mSupportedOperationsListener);
         if (mPhotoView.getFilmMode()) {
             requestDeferredUpdate();
         } else {
@@ -721,22 +744,7 @@
             supportedOperations &= ~MediaObject.SUPPORT_EDIT;
         }
         MenuExecutor.updateMenuOperation(menu, supportedOperations);
-        if ((supportedOperations & MediaObject.SUPPORT_PANORAMA360) != 0) {
-            mActivity.invalidateOptionsMenu();
-            item = menu.findItem(R.id.action_share);
-            if (item != null) {
-                item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
-                item.setTitle(
-                        mActivity.getResources().getString(R.string.share_as_photo));
-            }
-        } else if ((supportedOperations & MediaObject.SUPPORT_SHARE) != 0) {
-            item = menu.findItem(R.id.action_share);
-            if (item != null) {
-                item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
-                item.setTitle(
-                        mActivity.getResources().getString(R.string.share));
-            }
-        }
+        mCurrentPhoto.getPanoramaSupport(mUpdatePanoramaMenuItemsCallback);
     }
 
     private boolean canDoSlideShow() {
@@ -763,7 +771,7 @@
         mActionBar.show();
         mActivity.getGLRoot().setLightsOutMode(false);
         refreshHidingMessage();
-        if (mBottomControls != null) mBottomControls.refresh();
+        refreshBottomControlsWhenReady();
     }
 
     private void hideBars() {
@@ -772,7 +780,7 @@
         mActionBar.hide();
         mActivity.getGLRoot().setLightsOutMode(true);
         mHandler.removeMessages(MSG_HIDE_BARS);
-        if (mBottomControls != null) mBottomControls.refresh();
+        refreshBottomControlsWhenReady();
     }
 
     private void refreshHidingMessage() {
@@ -1283,9 +1291,7 @@
         mPhotoView.pause();
         mHandler.removeMessages(MSG_HIDE_BARS);
         mHandler.removeMessages(MSG_REFRESH_BOTTOM_CONTROLS);
-        if (mBottomControls != null) {
-            mBottomControls.refresh();
-        }
+        refreshBottomControlsWhenReady();
         mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
         if (mShowSpinner) {
             mActionBar.disableAlbumModeMenu(true);
@@ -1302,7 +1308,7 @@
 
     @Override
     public void onFilmModeChanged(boolean enabled) {
-        mHandler.sendEmptyMessage(MSG_REFRESH_BOTTOM_CONTROLS);
+        refreshBottomControlsWhenReady();
         if (enabled) {
             mHandler.removeMessages(MSG_HIDE_BARS);
         } else {
@@ -1362,9 +1368,7 @@
         mActionBar.setDisplayOptions(
                 ((mSecureAlbum == null) && (mSetPathString != null)), false);
         mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
-        if (mBottomControls != null) {
-            mBottomControls.refresh();
-        }
+        refreshBottomControlsWhenReady();
         if (mShowSpinner) {
             mActionBar.enableAlbumModeMenu(
                     GalleryActionBar.ALBUM_FILMSTRIP_MODE_SELECTED, this);
@@ -1427,4 +1431,42 @@
             switchToGrid();
         }
     }
+
+    @Override
+    public void refreshBottomControlsWhenReady() {
+        if (mBottomControls == null) {
+            return;
+        }
+        MediaObject currentPhoto = mCurrentPhoto;
+        if (currentPhoto == null) {
+            mHandler.obtainMessage(MSG_REFRESH_BOTTOM_CONTROLS, 0, 0, currentPhoto).sendToTarget();
+        } else {
+            currentPhoto.getPanoramaSupport(mRefreshBottomControlsCallback);
+        }
+    }
+
+    private void updatePanoramaUI(boolean isPanorama360) {
+        Menu menu = mActionBar.getMenu();
+
+        // it could be null if onCreateActionBar has not been called yet
+        if (menu == null) {
+            return;
+        }
+
+        MenuExecutor.updateMenuForPanorama(menu, isPanorama360, isPanorama360);
+
+        if (isPanorama360) {
+            MenuItem item = menu.findItem(R.id.action_share);
+            if (item != null) {
+                item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+                item.setTitle(mActivity.getResources().getString(R.string.share_as_photo));
+            }
+        } else if ((mCurrentPhoto.getSupportedOperations() & MediaObject.SUPPORT_SHARE) != 0) {
+            MenuItem item = menu.findItem(R.id.action_share);
+            if (item != null) {
+                item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+                item.setTitle(mActivity.getResources().getString(R.string.share));
+            }
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/app/PhotoPageBottomControls.java b/src/com/android/gallery3d/app/PhotoPageBottomControls.java
index 40588e1..e58e454 100644
--- a/src/com/android/gallery3d/app/PhotoPageBottomControls.java
+++ b/src/com/android/gallery3d/app/PhotoPageBottomControls.java
@@ -23,7 +23,6 @@
 import android.view.ViewGroup;
 import android.view.animation.AlphaAnimation;
 import android.view.animation.Animation;
-import android.view.animation.ScaleAnimation;
 import android.widget.RelativeLayout;
 
 import com.android.gallery3d.R;
@@ -34,8 +33,9 @@
 public class PhotoPageBottomControls implements OnClickListener {
     public interface Delegate {
         public boolean canDisplayBottomControls();
-        public boolean canDisplayBottomControl(int control);
+        public boolean canDisplayBottomControl(int control, boolean isPanorama);
         public void onBottomControlClicked(int control);
+        public void refreshBottomControlsWhenReady();
     }
 
     private Delegate mDelegate;
@@ -76,7 +76,7 @@
         mContainerAnimIn.setDuration(CONTAINER_ANIM_DURATION_MS);
         mContainerAnimOut.setDuration(CONTAINER_ANIM_DURATION_MS);
 
-        refresh();
+        mDelegate.refreshBottomControlsWhenReady();
     }
 
     private void hide() {
@@ -93,7 +93,7 @@
         mContainer.setVisibility(View.VISIBLE);
     }
 
-    public void refresh() {
+    public void refresh(boolean isPanorama) {
         boolean visible = mDelegate.canDisplayBottomControls();
         boolean containerVisibilityChanged = (visible != mContainerVisible);
         if (containerVisibilityChanged) {
@@ -109,7 +109,7 @@
         }
         for (View control : mControlsVisible.keySet()) {
             Boolean prevVisibility = mControlsVisible.get(control);
-            boolean curVisibility = mDelegate.canDisplayBottomControl(control.getId());
+            boolean curVisibility = mDelegate.canDisplayBottomControl(control.getId(), isPanorama);
             if (prevVisibility.booleanValue() != curVisibility) {
                 if (!containerVisibilityChanged) {
                     control.clearAnimation();
diff --git a/src/com/android/gallery3d/data/DataManager.java b/src/com/android/gallery3d/data/DataManager.java
index 408a24b..4ec7b6d 100644
--- a/src/com/android/gallery3d/data/DataManager.java
+++ b/src/com/android/gallery3d/data/DataManager.java
@@ -16,15 +16,15 @@
 
 package com.android.gallery3d.data;
 
-import android.content.Intent;
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Handler;
-import android.support.v4.content.LocalBroadcastManager;
 
 import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.StitchingChangeListener;
 import com.android.gallery3d.common.ApiHelper;
 import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
 import com.android.gallery3d.data.MediaSet.ItemConsumer;
 import com.android.gallery3d.data.MediaSource.PathId;
 import com.android.gallery3d.picasasource.PicasaSource;
@@ -49,7 +49,7 @@
 // path. And it's used to identify a specific media set even if the process is
 // killed and re-created, so child keys should be stable identifiers.
 
-public class DataManager {
+public class DataManager implements StitchingChangeListener {
     public static final int INCLUDE_IMAGE = 1;
     public static final int INCLUDE_VIDEO = 2;
     public static final int INCLUDE_ALL = INCLUDE_IMAGE | INCLUDE_VIDEO;
@@ -247,10 +247,8 @@
         return getMediaObject(path).getSupportedOperations();
     }
 
-    // getAll will cause this call to wait if any of the operations
-    // are expensive to compute. Do not call in UI thread.
-    public int getSupportedOperations(Path path, boolean getAll) {
-        return getMediaObject(path).getSupportedOperations(getAll);
+    public void getPanoramaSupport(Path path, PanoramaSupportCallback callback) {
+        getMediaObject(path).getPanoramaSupport(callback);
     }
 
     public void delete(Path path) {
@@ -351,4 +349,25 @@
             }
         }
     }
+
+    @Override
+    public void onStitchingQueued(Uri uri) {
+        // Do nothing.
+    }
+
+    @Override
+    public void onStitchingResult(Uri uri) {
+        Path path = findPathByUri(uri, null);
+        if (path != null) {
+            MediaObject mediaObject = getMediaObject(path);
+            if (mediaObject != null) {
+                mediaObject.clearCachedPanoramaSupport();
+            }
+        }
+    }
+
+    @Override
+    public void onStitchingProgress(Uri uri, int progress) {
+        // Do nothing.
+    }
 }
diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java
index 61961d8..b2be124 100644
--- a/src/com/android/gallery3d/data/LocalImage.java
+++ b/src/com/android/gallery3d/data/LocalImage.java
@@ -19,7 +19,6 @@
 import android.annotation.TargetApi;
 import android.content.ContentResolver;
 import android.content.ContentValues;
-import android.content.Context;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -33,14 +32,11 @@
 import android.util.Log;
 
 import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.PanoramaMetadataSupport;
 import com.android.gallery3d.app.StitchingProgressManager;
 import com.android.gallery3d.common.ApiHelper;
 import com.android.gallery3d.common.BitmapUtils;
-import com.android.gallery3d.util.Future;
-import com.android.gallery3d.util.FutureListener;
 import com.android.gallery3d.util.GalleryUtils;
-import com.android.gallery3d.util.LightCycleHelper;
-import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata;
 import com.android.gallery3d.util.ThreadPool.Job;
 import com.android.gallery3d.util.ThreadPool.JobContext;
 import com.android.gallery3d.util.UpdateHelper;
@@ -104,11 +100,7 @@
 
     public int rotation;
 
-    private Object mLock = new Object();
-    private Future<PanoramaMetadata> mGetPanoMetadataTask;
-    private boolean mPanoramaMetadataInitialized;
-    private PanoramaMetadata mPanoramaMetadata;
-    private SupportedOperationsListener mListener;
+    private PanoramaMetadataSupport mPanoramaMetadata = new PanoramaMetadataSupport(this);
 
     public LocalImage(Path path, GalleryApp application, Cursor cursor) {
         super(path, nextVersionNumber());
@@ -257,63 +249,17 @@
         if (GalleryUtils.isValidLocation(latitude, longitude)) {
             operation |= SUPPORT_SHOW_ON_MAP;
         }
-
-        if (mPanoramaMetadata != null && mPanoramaMetadata.mUsePanoramaViewer) {
-            operation |= SUPPORT_PANORAMA;
-            if (mPanoramaMetadata.mIsPanorama360) {
-                operation |= SUPPORT_PANORAMA360;
-                // disable destructive rotate and crop for 360 degree panorama
-                operation &= ~(SUPPORT_ROTATE | SUPPORT_CROP);
-            }
-        }
         return operation;
     }
 
     @Override
-    public int getSupportedOperations(boolean getAll) {
-        synchronized (mLock) {
-            if (getAll && !mPanoramaMetadataInitialized) {
-                if (mGetPanoMetadataTask == null) {
-                    mGetPanoMetadataTask = getThreadPool().submit(
-                            new PanoramaMetadataJob(mApplication.getAndroidContext(),
-                                getContentUri()));
-                }
-                mPanoramaMetadata = mGetPanoMetadataTask.get();
-                mPanoramaMetadataInitialized = true;
-            }
-        }
-        return getSupportedOperations();
+    public void getPanoramaSupport(PanoramaSupportCallback callback) {
+        mPanoramaMetadata.getPanoramaSupport(mApplication, callback);
     }
 
     @Override
-    public void setSupportedOperationsListener(SupportedOperationsListener l) {
-        synchronized (mLock) {
-            if (l == null) {
-                if (mGetPanoMetadataTask != null) {
-                    mGetPanoMetadataTask.cancel();
-                    mGetPanoMetadataTask = null;
-                }
-            } else {
-                if (mGetPanoMetadataTask == null) {
-                    mGetPanoMetadataTask = getThreadPool().submit(
-                            new PanoramaMetadataJob(mApplication.getAndroidContext(),
-                                getContentUri()),
-                            new FutureListener<PanoramaMetadata>() {
-                                @Override
-                        public void onFutureDone(Future<PanoramaMetadata> future) {
-                            mGetPanoMetadataTask = null;
-                            if (future.isCancelled()) return;
-                            mPanoramaMetadata = future.get();
-                            mPanoramaMetadataInitialized = true;
-                            if (mListener != null) {
-                                mListener.onChange(LocalImage.this, getSupportedOperations());
-                            }
-                        }
-                        });
-                        }
-            }
-            mListener = l;
-        }
+    public void clearCachedPanoramaSupport() {
+        mPanoramaMetadata.clearCachedValues();
     }
 
     @Override
diff --git a/src/com/android/gallery3d/data/MediaObject.java b/src/com/android/gallery3d/data/MediaObject.java
index 14cd524..a41b275 100644
--- a/src/com/android/gallery3d/data/MediaObject.java
+++ b/src/com/android/gallery3d/data/MediaObject.java
@@ -18,8 +18,6 @@
 
 import android.net.Uri;
 
-import com.android.gallery3d.util.ThreadPool;
-
 public abstract class MediaObject {
     @SuppressWarnings("unused")
     private static final String TAG = "MediaObject";
@@ -43,16 +41,8 @@
     public static final int SUPPORT_BACK = 1 << 14;
     public static final int SUPPORT_ACTION = 1 << 15;
     public static final int SUPPORT_CAMERA_SHORTCUT = 1 << 16;
-    // The panorama specific bits are expensive to compute.
-    // Use SupportedOperationsListener to request them.
-    public static final int SUPPORT_PANORAMA = 1 << 30;
-    public static final int SUPPORT_PANORAMA360 = 1 << 31;
     public static final int SUPPORT_ALL = 0xffffffff;
 
-    public static interface SupportedOperationsListener {
-        public void onChange(MediaObject item, int operations);
-    }
-
     // These are the bits returned from getMediaType():
     public static final int MEDIA_TYPE_UNKNOWN = 1;
     public static final int MEDIA_TYPE_IMAGE = 2;
@@ -80,9 +70,9 @@
 
     protected final Path mPath;
 
-    private static ThreadPool sThreadPool = new ThreadPool(1, 1);
-    public static ThreadPool getThreadPool() {
-        return sThreadPool;
+    public interface PanoramaSupportCallback {
+        void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+                boolean isPanorama360);
     }
 
     public MediaObject(Path path, long version) {
@@ -99,12 +89,11 @@
         return 0;
     }
 
-    public int getSupportedOperations(boolean getAll) {
-        return getSupportedOperations();
+    public void getPanoramaSupport(PanoramaSupportCallback callback) {
+        callback.panoramaInfoAvailable(this, false, false);
     }
 
-    public void setSupportedOperationsListener(SupportedOperationsListener l) {
-        // nothing to do
+    public void clearCachedPanoramaSupport() {
     }
 
     public void delete() {
diff --git a/src/com/android/gallery3d/data/PanoramaMetadataJob.java b/src/com/android/gallery3d/data/PanoramaMetadataJob.java
index e0a69c4..ab99d6a 100644
--- a/src/com/android/gallery3d/data/PanoramaMetadataJob.java
+++ b/src/com/android/gallery3d/data/PanoramaMetadataJob.java
@@ -24,7 +24,7 @@
 import com.android.gallery3d.util.ThreadPool.Job;
 import com.android.gallery3d.util.ThreadPool.JobContext;
 
-class PanoramaMetadataJob implements Job<PanoramaMetadata> {
+public class PanoramaMetadataJob implements Job<PanoramaMetadata> {
     Context mContext;
     Uri mUri;
 
diff --git a/src/com/android/gallery3d/data/UriImage.java b/src/com/android/gallery3d/data/UriImage.java
index aaa36a9..e8875b5 100644
--- a/src/com/android/gallery3d/data/UriImage.java
+++ b/src/com/android/gallery3d/data/UriImage.java
@@ -17,7 +17,6 @@
 package com.android.gallery3d.data;
 
 import android.content.ContentResolver;
-import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 import android.graphics.BitmapFactory.Options;
@@ -26,12 +25,9 @@
 import android.os.ParcelFileDescriptor;
 
 import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.PanoramaMetadataSupport;
 import com.android.gallery3d.common.BitmapUtils;
 import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.util.Future;
-import com.android.gallery3d.util.FutureListener;
-import com.android.gallery3d.util.LightCycleHelper;
-import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata;
 import com.android.gallery3d.util.ThreadPool.CancelListener;
 import com.android.gallery3d.util.ThreadPool.Job;
 import com.android.gallery3d.util.ThreadPool.JobContext;
@@ -59,12 +55,7 @@
     private int mWidth;
     private int mHeight;
     private int mRotation;
-
-    private Object mLock = new Object();
-    private Future<PanoramaMetadata> mGetPanoMetadataTask;
-    private boolean mPanoramaMetadataInitialized;
-    private PanoramaMetadata mPanoramaMetadata;
-    private SupportedOperationsListener mListener;
+    private PanoramaMetadataSupport mPanoramaMetadata = new PanoramaMetadataSupport(this);
 
     private GalleryApp mApplication;
 
@@ -225,62 +216,17 @@
         if (BitmapUtils.isSupportedByRegionDecoder(mContentType)) {
             supported |= SUPPORT_FULL_IMAGE;
         }
-        if (mPanoramaMetadata != null && mPanoramaMetadata.mUsePanoramaViewer) {
-            supported |= SUPPORT_PANORAMA;
-            if (mPanoramaMetadata.mIsPanorama360) {
-                supported |= SUPPORT_PANORAMA360;
-                // disable destructive crop for 360 degree panorama
-                supported &= ~SUPPORT_CROP;
-            }
-        }
         return supported;
     }
 
     @Override
-    public int getSupportedOperations(boolean getAll) {
-        synchronized (mLock) {
-            if (getAll && !mPanoramaMetadataInitialized) {
-                if (mGetPanoMetadataTask == null) {
-                    mGetPanoMetadataTask = getThreadPool().submit(
-                            new PanoramaMetadataJob(mApplication.getAndroidContext(),
-                                getContentUri()));
-                }
-                mPanoramaMetadata = mGetPanoMetadataTask.get();
-                mPanoramaMetadataInitialized = true;
-            }
-        }
-        return getSupportedOperations();
+    public void getPanoramaSupport(PanoramaSupportCallback callback) {
+        mPanoramaMetadata.getPanoramaSupport(mApplication, callback);
     }
 
     @Override
-    public void setSupportedOperationsListener(SupportedOperationsListener l) {
-        synchronized (mLock) {
-            if (l != null) {
-                if (mGetPanoMetadataTask != null) {
-                    mGetPanoMetadataTask.cancel();
-                    mGetPanoMetadataTask = null;
-                }
-            } else {
-                if (mGetPanoMetadataTask == null) {
-                    mGetPanoMetadataTask = getThreadPool().submit(
-                            new PanoramaMetadataJob(mApplication.getAndroidContext(),
-                                getContentUri()),
-                            new FutureListener<PanoramaMetadata>() {
-                                @Override
-                        public void onFutureDone(Future<PanoramaMetadata> future) {
-                            mGetPanoMetadataTask = null;
-                            if (future.isCancelled()) return;
-                            mPanoramaMetadata = future.get();
-                            mPanoramaMetadataInitialized = true;
-                            if (mListener != null) {
-                                mListener.onChange(UriImage.this, getSupportedOperations());
-                            }
-                        }
-                        });
-                }
-            }
-            mListener = l;
-        }
+    public void clearCachedPanoramaSupport() {
+        mPanoramaMetadata.clearCachedValues();
     }
 
     private boolean isSharable() {
diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java
index 3384b88..bb195c9 100644
--- a/src/com/android/gallery3d/ui/ActionModeHandler.java
+++ b/src/com/android/gallery3d/ui/ActionModeHandler.java
@@ -38,6 +38,7 @@
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.DataManager;
 import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
 import com.android.gallery3d.data.Path;
 import com.android.gallery3d.ui.MenuExecutor.ProgressListener;
 import com.android.gallery3d.util.Future;
@@ -54,8 +55,7 @@
 
     private static final int SUPPORT_MULTIPLE_MASK = MediaObject.SUPPORT_DELETE
             | MediaObject.SUPPORT_ROTATE | MediaObject.SUPPORT_SHARE
-            | MediaObject.SUPPORT_CACHE | MediaObject.SUPPORT_IMPORT
-            | MediaObject.SUPPORT_PANORAMA | MediaObject.SUPPORT_PANORAMA360;
+            | MediaObject.SUPPORT_CACHE | MediaObject.SUPPORT_IMPORT;
 
     public interface ActionModeListener {
         public boolean onActionItemClicked(MenuItem item);
@@ -76,6 +76,49 @@
     private final Handler mMainHandler;
     private ActionMode mActionMode;
 
+    private static class GetAllPanoramaSupports implements PanoramaSupportCallback {
+        private int mNumInfoRequired;
+        private JobContext mJobContext;
+        public boolean mAllPanoramas = true;
+        public boolean mAllPanorama360 = true;
+        public boolean mHasPanorama360 = false;
+        private Object mLock = new Object();
+
+        public GetAllPanoramaSupports(ArrayList<MediaObject> mediaObjects, JobContext jc) {
+            mJobContext = jc;
+            mNumInfoRequired = mediaObjects.size();
+            for (MediaObject mediaObject : mediaObjects) {
+                mediaObject.getPanoramaSupport(this);
+            }
+        }
+
+        @Override
+        public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+                boolean isPanorama360) {
+            synchronized (mLock) {
+                mNumInfoRequired--;
+                mAllPanoramas = isPanorama && mAllPanoramas;
+                mAllPanorama360 = isPanorama360 && mAllPanorama360;
+                mHasPanorama360 = mHasPanorama360 || isPanorama360;
+                if (mNumInfoRequired == 0 || mJobContext.isCancelled()) {
+                    mLock.notifyAll();
+                }
+            }
+        }
+
+        public void waitForPanoramaSupport() {
+            synchronized (mLock) {
+                while (mNumInfoRequired != 0 && !mJobContext.isCancelled()) {
+                    try {
+                        mLock.wait();
+                    } catch (InterruptedException e) {
+                        // May be a cancelled job context
+                    }
+                }
+            }
+        }
+    }
+
     public ActionModeHandler(
             AbstractGalleryActivity activity, SelectionManager selectionManager) {
         mActivity = Utils.checkNotNull(activity);
@@ -211,28 +254,38 @@
         mSelectionManager.leaveSelectionMode();
     }
 
-    // Menu options are determined by selection set itself.
-    // We cannot expand it because MenuExecuter executes it based on
-    // the selection set instead of the expanded result.
-    // e.g. LocalImage can be rotated but collections of them (LocalAlbum) can't.
-    private int computeMenuOptions(JobContext jc) {
+    private ArrayList<MediaObject> getSelectedMediaObjects(JobContext jc) {
         ArrayList<Path> unexpandedPaths = mSelectionManager.getSelected(false);
         if (unexpandedPaths.isEmpty()) {
             // This happens when starting selection mode from overflow menu
             // (instead of long press a media object)
-            return 0;
+            return null;
         }
-        int operation = MediaObject.SUPPORT_ALL;
+        ArrayList<MediaObject> selected = new ArrayList<MediaObject>();
         DataManager manager = mActivity.getDataManager();
-        int type = 0;
         for (Path path : unexpandedPaths) {
-            if (jc.isCancelled()) return 0;
-            int support = manager.getSupportedOperations(path, true);
-            type |= manager.getMediaType(path);
+            if (jc.isCancelled()) {
+                return null;
+            }
+            selected.add(manager.getMediaObject(path));
+        }
+
+        return selected;
+    }
+    // Menu options are determined by selection set itself.
+    // We cannot expand it because MenuExecuter executes it based on
+    // the selection set instead of the expanded result.
+    // e.g. LocalImage can be rotated but collections of them (LocalAlbum) can't.
+    private int computeMenuOptions(ArrayList<MediaObject> selected) {
+        int operation = MediaObject.SUPPORT_ALL;
+        int type = 0;
+        for (MediaObject mediaObject: selected) {
+            int support = mediaObject.getSupportedOperations();
+            type |= mediaObject.getMediaType();
             operation &= support;
         }
 
-        switch (unexpandedPaths.size()) {
+        switch (selected.size()) {
             case 1:
                 final String mimeType = MenuExecutor.getMimeType(type);
                 if (!GalleryUtils.isEditorAvailable(mActivity, mimeType)) {
@@ -298,7 +351,7 @@
         final Intent intent = new Intent();
         for (Path path : expandedPaths) {
             if (jc.isCancelled()) return null;
-            int support = manager.getSupportedOperations(path, true);
+            int support = manager.getSupportedOperations(path);
             type |= manager.getMediaType(path);
 
             if ((support & MediaObject.SUPPORT_SHARE) != 0) {
@@ -346,21 +399,36 @@
             @Override
             public Void run(final JobContext jc) {
                 // Pass1: Deal with unexpanded media object list for menu operation.
-                final int operation = computeMenuOptions(jc);
+                ArrayList<MediaObject> selected = getSelectedMediaObjects(jc);
+                if (selected == null) {
+                    return null;
+                }
+                final int operation = computeMenuOptions(selected);
+                if (jc.isCancelled()) {
+                    return null;
+                }
+                final GetAllPanoramaSupports supportCallback = new GetAllPanoramaSupports(selected,
+                        jc);
 
                 // Pass2: Deal with expanded media object list for sharing operation.
                 final Intent share_panorama_intent = computePanoramaSharingIntent(jc);
                 final Intent share_intent = computeSharingIntent(jc);
+
+                supportCallback.waitForPanoramaSupport();
+                if (jc.isCancelled()) {
+                    return null;
+                }
                 mMainHandler.post(new Runnable() {
                     @Override
                     public void run() {
                         mMenuTask = null;
                         if (jc.isCancelled()) return;
                         MenuExecutor.updateMenuOperation(mMenu, operation);
+                        MenuExecutor.updateMenuForPanorama(mMenu, supportCallback.mAllPanorama360,
+                                supportCallback.mHasPanorama360);
                         if (mSharePanoramaMenuItem != null) {
                             mSharePanoramaMenuItem.setEnabled(true);
-                            if ((operation & MediaObject.SUPPORT_PANORAMA360) != 0) {
-                                mActivity.invalidateOptionsMenu();
+                            if (supportCallback.mAllPanorama360) {
                                 mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
                                 mShareMenuItem.setTitle(
                                     mActivity.getResources().getString(R.string.share_as_photo));
diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java
index cacf6fc..1d110be 100644
--- a/src/com/android/gallery3d/ui/MenuExecutor.java
+++ b/src/com/android/gallery3d/ui/MenuExecutor.java
@@ -160,7 +160,6 @@
         boolean supportRotate = (supported & MediaObject.SUPPORT_ROTATE) != 0;
         boolean supportCrop = (supported & MediaObject.SUPPORT_CROP) != 0;
         boolean supportTrim = (supported & MediaObject.SUPPORT_TRIM) != 0;
-        boolean supportSharePanorama = (supported & MediaObject.SUPPORT_PANORAMA360) != 0;
         boolean supportShare = (supported & MediaObject.SUPPORT_SHARE) != 0;
         boolean supportSetAs = (supported & MediaObject.SUPPORT_SETAS) != 0;
         boolean supportShowOnMap = (supported & MediaObject.SUPPORT_SHOW_ON_MAP) != 0;
@@ -174,7 +173,8 @@
         setMenuItemVisible(menu, R.id.action_rotate_cw, supportRotate);
         setMenuItemVisible(menu, R.id.action_crop, supportCrop);
         setMenuItemVisible(menu, R.id.action_trim, supportTrim);
-        setMenuItemVisible(menu, R.id.action_share_panorama, supportSharePanorama);
+        // Hide panorama until call to updateMenuForPanorama corrects it
+        setMenuItemVisible(menu, R.id.action_share_panorama, false);
         setMenuItemVisible(menu, R.id.action_share, supportShare);
         setMenuItemVisible(menu, R.id.action_setas, supportSetAs);
         setMenuItemVisible(menu, R.id.action_show_on_map, supportShowOnMap);
@@ -183,6 +183,16 @@
         setMenuItemVisible(menu, R.id.action_import, supportImport);
     }
 
+    public static void updateMenuForPanorama(Menu menu, boolean shareAsPanorama360,
+            boolean disablePanorama360Options) {
+        setMenuItemVisible(menu, R.id.action_share_panorama, shareAsPanorama360);
+        if (disablePanorama360Options) {
+            setMenuItemVisible(menu, R.id.action_rotate_ccw, false);
+            setMenuItemVisible(menu, R.id.action_rotate_cw, false);
+            setMenuItemVisible(menu, R.id.action_crop, false);
+        }
+    }
+
     private static void setMenuItemVisible(Menu menu, int itemId, boolean visible) {
         MenuItem item = menu.findItem(itemId);
         if (item != null) item.setVisible(visible);