summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/gallery3d/app')
-rw-r--r--src/com/android/gallery3d/app/AbstractGalleryActivity.java343
-rw-r--r--src/com/android/gallery3d/app/ActivityState.java276
-rw-r--r--src/com/android/gallery3d/app/AlbumDataLoader.java397
-rw-r--r--src/com/android/gallery3d/app/AlbumPage.java786
-rw-r--r--src/com/android/gallery3d/app/AlbumPicker.java40
-rw-r--r--src/com/android/gallery3d/app/AlbumSetDataLoader.java393
-rw-r--r--src/com/android/gallery3d/app/AlbumSetPage.java764
-rw-r--r--src/com/android/gallery3d/app/AppBridge.java72
-rw-r--r--src/com/android/gallery3d/app/BatchService.java48
-rw-r--r--src/com/android/gallery3d/app/CommonControllerOverlay.java346
-rw-r--r--src/com/android/gallery3d/app/Config.java127
-rw-r--r--src/com/android/gallery3d/app/ControllerOverlay.java56
-rw-r--r--src/com/android/gallery3d/app/DialogPicker.java41
-rw-r--r--src/com/android/gallery3d/app/EyePosition.java226
-rw-r--r--src/com/android/gallery3d/app/FilmstripPage.java21
-rw-r--r--src/com/android/gallery3d/app/FilterUtils.java257
-rw-r--r--src/com/android/gallery3d/app/Gallery.java274
-rw-r--r--src/com/android/gallery3d/app/GalleryActionBar.java438
-rw-r--r--src/com/android/gallery3d/app/GalleryApp.java41
-rw-r--r--src/com/android/gallery3d/app/GalleryAppImpl.java127
-rw-r--r--src/com/android/gallery3d/app/GalleryContext.java34
-rw-r--r--src/com/android/gallery3d/app/LoadingListener.java27
-rw-r--r--src/com/android/gallery3d/app/Log.java53
-rw-r--r--src/com/android/gallery3d/app/ManageCachePage.java419
-rw-r--r--src/com/android/gallery3d/app/MovieActivity.java263
-rw-r--r--src/com/android/gallery3d/app/MovieControllerOverlay.java185
-rw-r--r--src/com/android/gallery3d/app/MoviePlayer.java525
-rw-r--r--src/com/android/gallery3d/app/MuteVideo.java104
-rw-r--r--src/com/android/gallery3d/app/NotificationIds.java22
-rw-r--r--src/com/android/gallery3d/app/OrientationManager.java166
-rw-r--r--src/com/android/gallery3d/app/PackagesMonitor.java71
-rw-r--r--src/com/android/gallery3d/app/PanoramaMetadataSupport.java93
-rw-r--r--src/com/android/gallery3d/app/PhotoDataAdapter.java1133
-rw-r--r--src/com/android/gallery3d/app/PhotoPage.java1571
-rw-r--r--src/com/android/gallery3d/app/PhotoPageBottomControls.java137
-rw-r--r--src/com/android/gallery3d/app/PhotoPageProgressBar.java50
-rw-r--r--src/com/android/gallery3d/app/PickerActivity.java83
-rw-r--r--src/com/android/gallery3d/app/SinglePhotoDataAdapter.java263
-rw-r--r--src/com/android/gallery3d/app/SinglePhotoPage.java21
-rw-r--r--src/com/android/gallery3d/app/SlideshowDataAdapter.java204
-rw-r--r--src/com/android/gallery3d/app/SlideshowPage.java366
-rw-r--r--src/com/android/gallery3d/app/StateManager.java339
-rw-r--r--src/com/android/gallery3d/app/StitchingChangeListener.java27
-rw-r--r--src/com/android/gallery3d/app/TimeBar.java266
-rw-r--r--src/com/android/gallery3d/app/TransitionStore.java46
-rw-r--r--src/com/android/gallery3d/app/TrimControllerOverlay.java111
-rw-r--r--src/com/android/gallery3d/app/TrimTimeBar.java339
-rw-r--r--src/com/android/gallery3d/app/TrimVideo.java337
-rw-r--r--src/com/android/gallery3d/app/VideoUtils.java328
-rw-r--r--src/com/android/gallery3d/app/Wallpaper.java135
50 files changed, 12791 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/app/AbstractGalleryActivity.java b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
new file mode 100644
index 000000000..ac39aa560
--- /dev/null
+++ b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2010 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 android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.Window;
+import android.view.WindowManager;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRootView;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.photos.data.GalleryBitmapPool;
+
+public class AbstractGalleryActivity extends Activity implements GalleryContext {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AbstractGalleryActivity";
+ private GLRootView mGLRootView;
+ private StateManager mStateManager;
+ private GalleryActionBar mActionBar;
+ private OrientationManager mOrientationManager;
+ private TransitionStore mTransitionStore = new TransitionStore();
+ private boolean mDisableToggleStatusBar;
+ private PanoramaViewHelper mPanoramaViewHelper;
+
+ private AlertDialog mAlertDialog = null;
+ private BroadcastReceiver mMountReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (getExternalCacheDir() != null) onStorageReady();
+ }
+ };
+ private IntentFilter mMountFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mOrientationManager = new OrientationManager(this);
+ toggleStatusBarByOrientation();
+ getWindow().setBackgroundDrawable(null);
+ mPanoramaViewHelper = new PanoramaViewHelper(this);
+ mPanoramaViewHelper.onCreate();
+ doBindBatchService();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ mGLRootView.lockRenderThread();
+ try {
+ super.onSaveInstanceState(outState);
+ getStateManager().saveState(outState);
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration config) {
+ super.onConfigurationChanged(config);
+ mStateManager.onConfigurationChange(config);
+ getGalleryActionBar().onConfigurationChanged();
+ invalidateOptionsMenu();
+ toggleStatusBarByOrientation();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ return getStateManager().createOptionsMenu(menu);
+ }
+
+ @Override
+ public Context getAndroidContext() {
+ return this;
+ }
+
+ @Override
+ public DataManager getDataManager() {
+ return ((GalleryApp) getApplication()).getDataManager();
+ }
+
+ @Override
+ public ThreadPool getThreadPool() {
+ return ((GalleryApp) getApplication()).getThreadPool();
+ }
+
+ public synchronized StateManager getStateManager() {
+ if (mStateManager == null) {
+ mStateManager = new StateManager(this);
+ }
+ return mStateManager;
+ }
+
+ public GLRoot getGLRoot() {
+ return mGLRootView;
+ }
+
+ public OrientationManager getOrientationManager() {
+ return mOrientationManager;
+ }
+
+ @Override
+ public void setContentView(int resId) {
+ super.setContentView(resId);
+ mGLRootView = (GLRootView) findViewById(R.id.gl_root_view);
+ }
+
+ protected void onStorageReady() {
+ if (mAlertDialog != null) {
+ mAlertDialog.dismiss();
+ mAlertDialog = null;
+ unregisterReceiver(mMountReceiver);
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (getExternalCacheDir() == null) {
+ OnCancelListener onCancel = new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }
+ };
+ OnClickListener onClick = new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ };
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.no_external_storage_title)
+ .setMessage(R.string.no_external_storage)
+ .setNegativeButton(android.R.string.cancel, onClick)
+ .setOnCancelListener(onCancel);
+ if (ApiHelper.HAS_SET_ICON_ATTRIBUTE) {
+ setAlertDialogIconAttribute(builder);
+ } else {
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ }
+ mAlertDialog = builder.show();
+ registerReceiver(mMountReceiver, mMountFilter);
+ }
+ mPanoramaViewHelper.onStart();
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ private static void setAlertDialogIconAttribute(
+ AlertDialog.Builder builder) {
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mAlertDialog != null) {
+ unregisterReceiver(mMountReceiver);
+ mAlertDialog.dismiss();
+ mAlertDialog = null;
+ }
+ mPanoramaViewHelper.onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mGLRootView.lockRenderThread();
+ try {
+ getStateManager().resume();
+ getDataManager().resume();
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ mGLRootView.onResume();
+ mOrientationManager.resume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mOrientationManager.pause();
+ mGLRootView.onPause();
+ mGLRootView.lockRenderThread();
+ try {
+ getStateManager().pause();
+ getDataManager().pause();
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ GalleryBitmapPool.getInstance().clear();
+ MediaItem.getBytesBufferPool().clear();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mGLRootView.lockRenderThread();
+ try {
+ getStateManager().destroy();
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ doUnbindBatchService();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ mGLRootView.lockRenderThread();
+ try {
+ getStateManager().notifyActivityResult(
+ requestCode, resultCode, data);
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ // send the back event to the top sub-state
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ getStateManager().onBackPressed();
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ public GalleryActionBar getGalleryActionBar() {
+ if (mActionBar == null) {
+ mActionBar = new GalleryActionBar(this);
+ }
+ return mActionBar;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ return getStateManager().itemSelected(item);
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ protected void disableToggleStatusBar() {
+ mDisableToggleStatusBar = true;
+ }
+
+ // Shows status bar in portrait view, hide in landscape view
+ private void toggleStatusBarByOrientation() {
+ if (mDisableToggleStatusBar) return;
+
+ Window win = getWindow();
+ if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+ win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ } else {
+ win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ }
+
+ public TransitionStore getTransitionStore() {
+ return mTransitionStore;
+ }
+
+ public PanoramaViewHelper getPanoramaViewHelper() {
+ return mPanoramaViewHelper;
+ }
+
+ protected boolean isFullscreen() {
+ return (getWindow().getAttributes().flags
+ & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0;
+ }
+
+ private BatchService mBatchService;
+ private boolean mBatchServiceIsBound = false;
+ private ServiceConnection mBatchServiceConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ mBatchService = ((BatchService.LocalBinder)service).getService();
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ mBatchService = null;
+ }
+ };
+
+ private void doBindBatchService() {
+ bindService(new Intent(this, BatchService.class), mBatchServiceConnection, Context.BIND_AUTO_CREATE);
+ mBatchServiceIsBound = true;
+ }
+
+ private void doUnbindBatchService() {
+ if (mBatchServiceIsBound) {
+ // Detach our existing connection.
+ unbindService(mBatchServiceConnection);
+ mBatchServiceIsBound = false;
+ }
+ }
+
+ public ThreadPool getBatchServiceThreadPoolIfAvailable() {
+ if (mBatchServiceIsBound && mBatchService != null) {
+ return mBatchService.getThreadPool();
+ } else {
+ throw new RuntimeException("Batch service unavailable");
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/ActivityState.java b/src/com/android/gallery3d/app/ActivityState.java
new file mode 100644
index 000000000..2f1e0c9d9
--- /dev/null
+++ b/src/com/android/gallery3d/app/ActivityState.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2010 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 android.app.ActionBar;
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.os.BatteryManager;
+import android.os.Bundle;
+import android.view.HapticFeedbackConstants;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.Window;
+import android.view.WindowManager;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.anim.StateTransitionAnimation;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.PreparePageFadeoutTexture;
+import com.android.gallery3d.util.GalleryUtils;
+
+abstract public class ActivityState {
+ protected static final int FLAG_HIDE_ACTION_BAR = 1;
+ protected static final int FLAG_HIDE_STATUS_BAR = 2;
+ protected static final int FLAG_SCREEN_ON_WHEN_PLUGGED = 4;
+ protected static final int FLAG_SCREEN_ON_ALWAYS = 8;
+ protected static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 16;
+ protected static final int FLAG_SHOW_WHEN_LOCKED = 32;
+
+ protected AbstractGalleryActivity mActivity;
+ protected Bundle mData;
+ protected int mFlags;
+
+ protected ResultEntry mReceivedResults;
+ protected ResultEntry mResult;
+
+ protected static class ResultEntry {
+ public int requestCode;
+ public int resultCode = Activity.RESULT_CANCELED;
+ public Intent resultData;
+ }
+
+ private boolean mDestroyed = false;
+ private boolean mPlugged = false;
+ boolean mIsFinishing = false;
+
+ private static final String KEY_TRANSITION_IN = "transition-in";
+
+ private StateTransitionAnimation.Transition mNextTransition =
+ StateTransitionAnimation.Transition.None;
+ private StateTransitionAnimation mIntroAnimation;
+ private GLView mContentPane;
+
+ protected ActivityState() {
+ }
+
+ protected void setContentPane(GLView content) {
+ mContentPane = content;
+ if (mIntroAnimation != null) {
+ mContentPane.setIntroAnimation(mIntroAnimation);
+ mIntroAnimation = null;
+ }
+ mContentPane.setBackgroundColor(getBackgroundColor());
+ mActivity.getGLRoot().setContentPane(mContentPane);
+ }
+
+ void initialize(AbstractGalleryActivity activity, Bundle data) {
+ mActivity = activity;
+ mData = data;
+ }
+
+ public Bundle getData() {
+ return mData;
+ }
+
+ protected void onBackPressed() {
+ mActivity.getStateManager().finishState(this);
+ }
+
+ protected void setStateResult(int resultCode, Intent data) {
+ if (mResult == null) return;
+ mResult.resultCode = resultCode;
+ mResult.resultData = data;
+ }
+
+ protected void onConfigurationChanged(Configuration config) {
+ }
+
+ protected void onSaveState(Bundle outState) {
+ }
+
+ protected void onStateResult(int requestCode, int resultCode, Intent data) {
+ }
+
+ protected float[] mBackgroundColor;
+
+ protected int getBackgroundColorId() {
+ return R.color.default_background;
+ }
+
+ protected float[] getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
+ protected void onCreate(Bundle data, Bundle storedState) {
+ mBackgroundColor = GalleryUtils.intColorToFloatARGBArray(
+ mActivity.getResources().getColor(getBackgroundColorId()));
+ }
+
+ protected void clearStateResult() {
+ }
+
+ BroadcastReceiver mPowerIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
+ boolean plugged = (0 != intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0));
+
+ if (plugged != mPlugged) {
+ mPlugged = plugged;
+ setScreenFlags();
+ }
+ }
+ }
+ };
+
+ private void setScreenFlags() {
+ final Window win = mActivity.getWindow();
+ final WindowManager.LayoutParams params = win.getAttributes();
+ if ((0 != (mFlags & FLAG_SCREEN_ON_ALWAYS)) ||
+ (mPlugged && 0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED))) {
+ params.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+ } else {
+ params.flags &= ~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+ }
+ if (0 != (mFlags & FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)) {
+ params.flags |= WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON;
+ } else {
+ params.flags &= ~WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON;
+ }
+ if (0 != (mFlags & FLAG_SHOW_WHEN_LOCKED)) {
+ params.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
+ } else {
+ params.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
+ }
+ win.setAttributes(params);
+ }
+
+ protected void transitionOnNextPause(Class<? extends ActivityState> outgoing,
+ Class<? extends ActivityState> incoming, StateTransitionAnimation.Transition hint) {
+ if (outgoing == SinglePhotoPage.class && incoming == AlbumPage.class) {
+ mNextTransition = StateTransitionAnimation.Transition.Outgoing;
+ } else if (outgoing == AlbumPage.class && incoming == SinglePhotoPage.class) {
+ mNextTransition = StateTransitionAnimation.Transition.PhotoIncoming;
+ } else {
+ mNextTransition = hint;
+ }
+ }
+
+ protected void performHapticFeedback(int feedbackConstant) {
+ mActivity.getWindow().getDecorView().performHapticFeedback(feedbackConstant,
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+ }
+
+ protected void onPause() {
+ if (0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED)) {
+ ((Activity) mActivity).unregisterReceiver(mPowerIntentReceiver);
+ }
+ if (mNextTransition != StateTransitionAnimation.Transition.None) {
+ mActivity.getTransitionStore().put(KEY_TRANSITION_IN, mNextTransition);
+ PreparePageFadeoutTexture.prepareFadeOutTexture(mActivity, mContentPane);
+ mNextTransition = StateTransitionAnimation.Transition.None;
+ }
+ }
+
+ // should only be called by StateManager
+ void resume() {
+ AbstractGalleryActivity activity = mActivity;
+ ActionBar actionBar = activity.getActionBar();
+ if (actionBar != null) {
+ if ((mFlags & FLAG_HIDE_ACTION_BAR) != 0) {
+ actionBar.hide();
+ } else {
+ actionBar.show();
+ }
+ int stateCount = mActivity.getStateManager().getStateCount();
+ mActivity.getGalleryActionBar().setDisplayOptions(stateCount > 1, true);
+ // Default behavior, this can be overridden in ActivityState's onResume.
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ }
+
+ activity.invalidateOptionsMenu();
+
+ setScreenFlags();
+
+ boolean lightsOut = ((mFlags & FLAG_HIDE_STATUS_BAR) != 0);
+ mActivity.getGLRoot().setLightsOutMode(lightsOut);
+
+ ResultEntry entry = mReceivedResults;
+ if (entry != null) {
+ mReceivedResults = null;
+ onStateResult(entry.requestCode, entry.resultCode, entry.resultData);
+ }
+
+ if (0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED)) {
+ // we need to know whether the device is plugged in to do this correctly
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_BATTERY_CHANGED);
+ activity.registerReceiver(mPowerIntentReceiver, filter);
+ }
+
+ onResume();
+
+ // the transition store should be cleared after resume;
+ mActivity.getTransitionStore().clear();
+ }
+
+ // a subclass of ActivityState should override the method to resume itself
+ protected void onResume() {
+ RawTexture fade = mActivity.getTransitionStore().get(
+ PreparePageFadeoutTexture.KEY_FADE_TEXTURE);
+ mNextTransition = mActivity.getTransitionStore().get(
+ KEY_TRANSITION_IN, StateTransitionAnimation.Transition.None);
+ if (mNextTransition != StateTransitionAnimation.Transition.None) {
+ mIntroAnimation = new StateTransitionAnimation(mNextTransition, fade);
+ mNextTransition = StateTransitionAnimation.Transition.None;
+ }
+ }
+
+ protected boolean onCreateActionBar(Menu menu) {
+ // TODO: we should return false if there is no menu to show
+ // this is a workaround for a bug in system
+ return true;
+ }
+
+ protected boolean onItemSelected(MenuItem item) {
+ return false;
+ }
+
+ protected void onDestroy() {
+ mDestroyed = true;
+ }
+
+ boolean isDestroyed() {
+ return mDestroyed;
+ }
+
+ public boolean isFinishing() {
+ return mIsFinishing;
+ }
+
+ protected MenuInflater getSupportMenuInflater() {
+ return mActivity.getMenuInflater();
+ }
+}
diff --git a/src/com/android/gallery3d/app/AlbumDataLoader.java b/src/com/android/gallery3d/app/AlbumDataLoader.java
new file mode 100644
index 000000000..28a822830
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumDataLoader.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2010 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 android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.SynchronizedHandler;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+public class AlbumDataLoader {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumDataAdapter";
+ private static final int DATA_CACHE_SIZE = 1000;
+
+ private static final int MSG_LOAD_START = 1;
+ private static final int MSG_LOAD_FINISH = 2;
+ private static final int MSG_RUN_OBJECT = 3;
+
+ private static final int MIN_LOAD_COUNT = 32;
+ private static final int MAX_LOAD_COUNT = 64;
+
+ private final MediaItem[] mData;
+ private final long[] mItemVersion;
+ private final long[] mSetVersion;
+
+ public static interface DataListener {
+ public void onContentChanged(int index);
+ public void onSizeChanged(int size);
+ }
+
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ private final MediaSet mSource;
+ private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
+
+ private final Handler mMainHandler;
+ private int mSize = 0;
+
+ private DataListener mDataListener;
+ private MySourceListener mSourceListener = new MySourceListener();
+ private LoadingListener mLoadingListener;
+
+ private ReloadTask mReloadTask;
+ // the data version on which last loading failed
+ private long mFailedVersion = MediaObject.INVALID_DATA_VERSION;
+
+ public AlbumDataLoader(AbstractGalleryActivity context, MediaSet mediaSet) {
+ mSource = mediaSet;
+
+ mData = new MediaItem[DATA_CACHE_SIZE];
+ mItemVersion = new long[DATA_CACHE_SIZE];
+ mSetVersion = new long[DATA_CACHE_SIZE];
+ Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION);
+ Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION);
+
+ mMainHandler = new SynchronizedHandler(context.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_RUN_OBJECT:
+ ((Runnable) message.obj).run();
+ return;
+ case MSG_LOAD_START:
+ if (mLoadingListener != null) mLoadingListener.onLoadingStarted();
+ return;
+ case MSG_LOAD_FINISH:
+ if (mLoadingListener != null) {
+ boolean loadingFailed =
+ (mFailedVersion != MediaObject.INVALID_DATA_VERSION);
+ mLoadingListener.onLoadingFinished(loadingFailed);
+ }
+ return;
+ }
+ }
+ };
+ }
+
+ public void resume() {
+ mSource.addContentListener(mSourceListener);
+ mReloadTask = new ReloadTask();
+ mReloadTask.start();
+ }
+
+ public void pause() {
+ mReloadTask.terminate();
+ mReloadTask = null;
+ mSource.removeContentListener(mSourceListener);
+ }
+
+ public MediaItem get(int index) {
+ if (!isActive(index)) {
+ return mSource.getMediaItem(index, 1).get(0);
+ }
+ return mData[index % mData.length];
+ }
+
+ public int getActiveStart() {
+ return mActiveStart;
+ }
+
+ public boolean isActive(int index) {
+ return index >= mActiveStart && index < mActiveEnd;
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ // Returns the index of the MediaItem with the given path or
+ // -1 if the path is not cached
+ public int findItem(Path id) {
+ for (int i = mContentStart; i < mContentEnd; i++) {
+ MediaItem item = mData[i % DATA_CACHE_SIZE];
+ if (item != null && id == item.getPath()) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void clearSlot(int slotIndex) {
+ mData[slotIndex] = null;
+ mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
+ mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
+ }
+
+ private void setContentWindow(int contentStart, int contentEnd) {
+ if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+ int end = mContentEnd;
+ int start = mContentStart;
+
+ // We need change the content window before calling reloadData(...)
+ synchronized (this) {
+ mContentStart = contentStart;
+ mContentEnd = contentEnd;
+ }
+ long[] itemVersion = mItemVersion;
+ long[] setVersion = mSetVersion;
+ if (contentStart >= end || start >= contentEnd) {
+ for (int i = start, n = end; i < n; ++i) {
+ clearSlot(i % DATA_CACHE_SIZE);
+ }
+ } else {
+ for (int i = start; i < contentStart; ++i) {
+ clearSlot(i % DATA_CACHE_SIZE);
+ }
+ for (int i = contentEnd, n = end; i < n; ++i) {
+ clearSlot(i % DATA_CACHE_SIZE);
+ }
+ }
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+
+ public void setActiveWindow(int start, int end) {
+ if (start == mActiveStart && end == mActiveEnd) return;
+
+ Utils.assertTrue(start <= end
+ && end - start <= mData.length && end <= mSize);
+
+ int length = mData.length;
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ // If no data is visible, keep the cache content
+ if (start == end) return;
+
+ int contentStart = Utils.clamp((start + end) / 2 - length / 2,
+ 0, Math.max(0, mSize - length));
+ int contentEnd = Math.min(contentStart + length, mSize);
+ if (mContentStart > start || mContentEnd < end
+ || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) {
+ setContentWindow(contentStart, contentEnd);
+ }
+ }
+
+ private class MySourceListener implements ContentListener {
+ @Override
+ public void onContentDirty() {
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ public void setDataListener(DataListener listener) {
+ mDataListener = listener;
+ }
+
+ public void setLoadingListener(LoadingListener listener) {
+ mLoadingListener = listener;
+ }
+
+ private <T> T executeAndWait(Callable<T> callable) {
+ FutureTask<T> task = new FutureTask<T>(callable);
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
+ try {
+ return task.get();
+ } catch (InterruptedException e) {
+ return null;
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static class UpdateInfo {
+ public long version;
+ public int reloadStart;
+ public int reloadCount;
+
+ public int size;
+ public ArrayList<MediaItem> items;
+ }
+
+ private class GetUpdateInfo implements Callable<UpdateInfo> {
+ private final long mVersion;
+
+ public GetUpdateInfo(long version) {
+ mVersion = version;
+ }
+
+ @Override
+ public UpdateInfo call() throws Exception {
+ if (mFailedVersion == mVersion) {
+ // previous loading failed, return null to pause loading
+ return null;
+ }
+ UpdateInfo info = new UpdateInfo();
+ long version = mVersion;
+ info.version = mSourceVersion;
+ info.size = mSize;
+ long setVersion[] = mSetVersion;
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ int index = i % DATA_CACHE_SIZE;
+ if (setVersion[index] != version) {
+ info.reloadStart = i;
+ info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i);
+ return info;
+ }
+ }
+ return mSourceVersion == mVersion ? null : info;
+ }
+ }
+
+ private class UpdateContent implements Callable<Void> {
+
+ private UpdateInfo mUpdateInfo;
+
+ public UpdateContent(UpdateInfo info) {
+ mUpdateInfo = info;
+ }
+
+ @Override
+ public Void call() throws Exception {
+ UpdateInfo info = mUpdateInfo;
+ mSourceVersion = info.version;
+ if (mSize != info.size) {
+ mSize = info.size;
+ if (mDataListener != null) mDataListener.onSizeChanged(mSize);
+ if (mContentEnd > mSize) mContentEnd = mSize;
+ if (mActiveEnd > mSize) mActiveEnd = mSize;
+ }
+
+ ArrayList<MediaItem> items = info.items;
+
+ mFailedVersion = MediaObject.INVALID_DATA_VERSION;
+ if ((items == null) || items.isEmpty()) {
+ if (info.reloadCount > 0) {
+ mFailedVersion = info.version;
+ Log.d(TAG, "loading failed: " + mFailedVersion);
+ }
+ return null;
+ }
+ int start = Math.max(info.reloadStart, mContentStart);
+ int end = Math.min(info.reloadStart + items.size(), mContentEnd);
+
+ for (int i = start; i < end; ++i) {
+ int index = i % DATA_CACHE_SIZE;
+ mSetVersion[index] = info.version;
+ MediaItem updateItem = items.get(i - info.reloadStart);
+ long itemVersion = updateItem.getDataVersion();
+ if (mItemVersion[index] != itemVersion) {
+ mItemVersion[index] = itemVersion;
+ mData[index] = updateItem;
+ if (mDataListener != null && i >= mActiveStart && i < mActiveEnd) {
+ mDataListener.onContentChanged(i);
+ }
+ }
+ }
+ return null;
+ }
+ }
+
+ /*
+ * The thread model of ReloadTask
+ * *
+ * [Reload Task] [Main Thread]
+ * | |
+ * getUpdateInfo() --> | (synchronous call)
+ * (wait) <---- getUpdateInfo()
+ * | |
+ * Load Data |
+ * | |
+ * updateContent() --> | (synchronous call)
+ * (wait) updateContent()
+ * | |
+ * | |
+ */
+ private class ReloadTask extends Thread {
+
+ private volatile boolean mActive = true;
+ private volatile boolean mDirty = true;
+ private boolean mIsLoading = false;
+
+ private void updateLoading(boolean loading) {
+ if (mIsLoading == loading) return;
+ mIsLoading = loading;
+ mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
+ }
+
+ @Override
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+ boolean updateComplete = false;
+ while (mActive) {
+ synchronized (this) {
+ if (mActive && !mDirty && updateComplete) {
+ updateLoading(false);
+ if (mFailedVersion != MediaObject.INVALID_DATA_VERSION) {
+ Log.d(TAG, "reload pause");
+ }
+ Utils.waitWithoutInterrupt(this);
+ if (mActive && (mFailedVersion != MediaObject.INVALID_DATA_VERSION)) {
+ Log.d(TAG, "reload resume");
+ }
+ continue;
+ }
+ mDirty = false;
+ }
+ updateLoading(true);
+ long version = mSource.reload();
+ UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
+ updateComplete = info == null;
+ if (updateComplete) continue;
+ if (info.version != version) {
+ info.size = mSource.getMediaItemCount();
+ info.version = version;
+ }
+ if (info.reloadCount > 0) {
+ info.items = mSource.getMediaItem(info.reloadStart, info.reloadCount);
+ }
+ executeAndWait(new UpdateContent(info));
+ }
+ updateLoading(false);
+ }
+
+ public synchronized void notifyDirty() {
+ mDirty = true;
+ notifyAll();
+ }
+
+ public synchronized void terminate() {
+ mActive = false;
+ notifyAll();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java
new file mode 100644
index 000000000..658abbbd4
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -0,0 +1,786 @@
+/*
+ * Copyright (C) 2010 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 android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.view.HapticFeedbackConstants;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.filtershow.crop.CropExtras;
+import com.android.gallery3d.glrenderer.FadeTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.ui.ActionModeHandler;
+import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener;
+import com.android.gallery3d.ui.AlbumSlotRenderer;
+import com.android.gallery3d.ui.DetailsHelper;
+import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.PhotoFallbackEffect;
+import com.android.gallery3d.ui.RelativePosition;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.MediaSetUtils;
+
+
+public class AlbumPage extends ActivityState implements GalleryActionBar.ClusterRunner,
+ SelectionManager.SelectionListener, MediaSet.SyncListener, GalleryActionBar.OnAlbumModeSelectedListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumPage";
+
+ public static final String KEY_MEDIA_PATH = "media-path";
+ public static final String KEY_PARENT_MEDIA_PATH = "parent-media-path";
+ public static final String KEY_SET_CENTER = "set-center";
+ public static final String KEY_AUTO_SELECT_ALL = "auto-select-all";
+ public static final String KEY_SHOW_CLUSTER_MENU = "cluster-menu";
+ public static final String KEY_EMPTY_ALBUM = "empty-album";
+ public static final String KEY_RESUME_ANIMATION = "resume_animation";
+
+ private static final int REQUEST_SLIDESHOW = 1;
+ public static final int REQUEST_PHOTO = 2;
+ private static final int REQUEST_DO_ANIMATION = 3;
+
+ private static final int BIT_LOADING_RELOAD = 1;
+ private static final int BIT_LOADING_SYNC = 2;
+
+ private static final float USER_DISTANCE_METER = 0.3f;
+
+ private boolean mIsActive = false;
+ private AlbumSlotRenderer mAlbumView;
+ private Path mMediaSetPath;
+ private String mParentMediaSetString;
+ private SlotView mSlotView;
+
+ private AlbumDataLoader mAlbumDataAdapter;
+
+ protected SelectionManager mSelectionManager;
+
+ private boolean mGetContent;
+ private boolean mShowClusterMenu;
+
+ private ActionModeHandler mActionModeHandler;
+ private int mFocusIndex = 0;
+ private DetailsHelper mDetailsHelper;
+ private MyDetailsSource mDetailsSource;
+ private MediaSet mMediaSet;
+ private boolean mShowDetails;
+ private float mUserDistance; // in pixel
+ private Future<Integer> mSyncTask = null;
+ private boolean mLaunchedFromPhotoPage;
+ private boolean mInCameraApp;
+ private boolean mInCameraAndWantQuitOnPause;
+
+ private int mLoadingBits = 0;
+ private boolean mInitialSynced = false;
+ private int mSyncResult;
+ private boolean mLoadingFailed;
+ private RelativePosition mOpenCenter = new RelativePosition();
+
+ private Handler mHandler;
+ private static final int MSG_PICK_PHOTO = 0;
+
+ private PhotoFallbackEffect mResumeEffect;
+ private PhotoFallbackEffect.PositionProvider mPositionProvider =
+ new PhotoFallbackEffect.PositionProvider() {
+ @Override
+ public Rect getPosition(int index) {
+ Rect rect = mSlotView.getSlotRect(index);
+ Rect bounds = mSlotView.bounds();
+ rect.offset(bounds.left - mSlotView.getScrollX(),
+ bounds.top - mSlotView.getScrollY());
+ return rect;
+ }
+
+ @Override
+ public int getItemIndex(Path path) {
+ int start = mSlotView.getVisibleStart();
+ int end = mSlotView.getVisibleEnd();
+ for (int i = start; i < end; ++i) {
+ MediaItem item = mAlbumDataAdapter.get(i);
+ if (item != null && item.getPath() == path) return i;
+ }
+ return -1;
+ }
+ };
+
+ @Override
+ protected int getBackgroundColorId() {
+ return R.color.album_background;
+ }
+
+ private final GLView mRootPane = new GLView() {
+ private final float mMatrix[] = new float[16];
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+
+ int slotViewTop = mActivity.getGalleryActionBar().getHeight();
+ int slotViewBottom = bottom - top;
+ int slotViewRight = right - left;
+
+ if (mShowDetails) {
+ mDetailsHelper.layout(left, slotViewTop, right, bottom);
+ } else {
+ mAlbumView.setHighlightItemPath(null);
+ }
+
+ // Set the mSlotView as a reference point to the open animation
+ mOpenCenter.setReferencePosition(0, slotViewTop);
+ mSlotView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
+ GalleryUtils.setViewPointMatrix(mMatrix,
+ (right - left) / 2, (bottom - top) / 2, -mUserDistance);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.multiplyMatrix(mMatrix, 0);
+ super.render(canvas);
+
+ if (mResumeEffect != null) {
+ boolean more = mResumeEffect.draw(canvas);
+ if (!more) {
+ mResumeEffect = null;
+ mAlbumView.setSlotFilter(null);
+ }
+ // We want to render one more time even when no more effect
+ // required. So that the animated thumbnails could be draw
+ // with declarations in super.render().
+ invalidate();
+ }
+ canvas.restore();
+ }
+ };
+
+ // This are the transitions we want:
+ //
+ // +--------+ +------------+ +-------+ +----------+
+ // | Camera |---------->| Fullscreen |--->| Album |--->| AlbumSet |
+ // | View | thumbnail | Photo | up | Page | up | Page |
+ // +--------+ +------------+ +-------+ +----------+
+ // ^ | | ^ |
+ // | | | | | close
+ // +----------back--------+ +----back----+ +--back-> app
+ //
+ @Override
+ protected void onBackPressed() {
+ if (mShowDetails) {
+ hideDetails();
+ } else if (mSelectionManager.inSelectionMode()) {
+ mSelectionManager.leaveSelectionMode();
+ } else {
+ if(mLaunchedFromPhotoPage) {
+ mActivity.getTransitionStore().putIfNotPresent(
+ PhotoPage.KEY_ALBUMPAGE_TRANSITION,
+ PhotoPage.MSG_ALBUMPAGE_RESUMED);
+ }
+ // TODO: fix this regression
+ // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ if (mInCameraApp) {
+ super.onBackPressed();
+ } else {
+ onUpPressed();
+ }
+ }
+ }
+
+ private void onUpPressed() {
+ if (mInCameraApp) {
+ GalleryUtils.startGalleryActivity(mActivity);
+ } else if (mActivity.getStateManager().getStateCount() > 1) {
+ super.onBackPressed();
+ } else if (mParentMediaSetString != null) {
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, mParentMediaSetString);
+ mActivity.getStateManager().switchState(
+ this, AlbumSetPage.class, data);
+ }
+ }
+
+ private void onDown(int index) {
+ mAlbumView.setPressedIndex(index);
+ }
+
+ private void onUp(boolean followedByLongPress) {
+ if (followedByLongPress) {
+ // Avoid showing press-up animations for long-press.
+ mAlbumView.setPressedIndex(-1);
+ } else {
+ mAlbumView.setPressedUp();
+ }
+ }
+
+ private void onSingleTapUp(int slotIndex) {
+ if (!mIsActive) return;
+
+ if (mSelectionManager.inSelectionMode()) {
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) return; // Item not ready yet, ignore the click
+ mSelectionManager.toggle(item.getPath());
+ mSlotView.invalidate();
+ } else {
+ // Render transition in pressed state
+ mAlbumView.setPressedIndex(slotIndex);
+ mAlbumView.setPressedUp();
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_PICK_PHOTO, slotIndex, 0),
+ FadeTexture.DURATION);
+ }
+ }
+
+ private void pickPhoto(int slotIndex) {
+ pickPhoto(slotIndex, false);
+ }
+
+ private void pickPhoto(int slotIndex, boolean startInFilmstrip) {
+ if (!mIsActive) return;
+
+ if (!startInFilmstrip) {
+ // Launch photos in lights out mode
+ mActivity.getGLRoot().setLightsOutMode(true);
+ }
+
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) return; // Item not ready yet, ignore the click
+ if (mGetContent) {
+ onGetContent(item);
+ } else if (mLaunchedFromPhotoPage) {
+ TransitionStore transitions = mActivity.getTransitionStore();
+ transitions.put(
+ PhotoPage.KEY_ALBUMPAGE_TRANSITION,
+ PhotoPage.MSG_ALBUMPAGE_PICKED);
+ transitions.put(PhotoPage.KEY_INDEX_HINT, slotIndex);
+ onBackPressed();
+ } else {
+ // Get into the PhotoPage.
+ // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ Bundle data = new Bundle();
+ data.putInt(PhotoPage.KEY_INDEX_HINT, slotIndex);
+ data.putParcelable(PhotoPage.KEY_OPEN_ANIMATION_RECT,
+ mSlotView.getSlotRect(slotIndex, mRootPane));
+ data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
+ mMediaSetPath.toString());
+ data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH,
+ item.getPath().toString());
+ data.putInt(PhotoPage.KEY_ALBUMPAGE_TRANSITION,
+ PhotoPage.MSG_ALBUMPAGE_STARTED);
+ data.putBoolean(PhotoPage.KEY_START_IN_FILMSTRIP,
+ startInFilmstrip);
+ data.putBoolean(PhotoPage.KEY_IN_CAMERA_ROLL, mMediaSet.isCameraRoll());
+ if (startInFilmstrip) {
+ mActivity.getStateManager().switchState(this, FilmstripPage.class, data);
+ } else {
+ mActivity.getStateManager().startStateForResult(
+ SinglePhotoPage.class, REQUEST_PHOTO, data);
+ }
+ }
+ }
+
+ private void onGetContent(final MediaItem item) {
+ DataManager dm = mActivity.getDataManager();
+ Activity activity = mActivity;
+ if (mData.getString(Gallery.EXTRA_CROP) != null) {
+ Uri uri = dm.getContentUri(item.getPath());
+ Intent intent = new Intent(CropActivity.CROP_ACTION, uri)
+ .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
+ .putExtras(getData());
+ if (mData.getParcelable(MediaStore.EXTRA_OUTPUT) == null) {
+ intent.putExtra(CropExtras.KEY_RETURN_DATA, true);
+ }
+ activity.startActivity(intent);
+ activity.finish();
+ } else {
+ Intent intent = new Intent(null, item.getContentUri())
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ activity.setResult(Activity.RESULT_OK, intent);
+ activity.finish();
+ }
+ }
+
+ public void onLongTap(int slotIndex) {
+ if (mGetContent) return;
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) return;
+ mSelectionManager.setAutoLeaveSelectionMode(true);
+ mSelectionManager.toggle(item.getPath());
+ mSlotView.invalidate();
+ }
+
+ @Override
+ public void doCluster(int clusterType) {
+ String basePath = mMediaSet.getPath().toString();
+ String newPath = FilterUtils.newClusterPath(basePath, clusterType);
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath);
+ if (mShowClusterMenu) {
+ Context context = mActivity.getAndroidContext();
+ data.putString(AlbumSetPage.KEY_SET_TITLE, mMediaSet.getName());
+ data.putString(AlbumSetPage.KEY_SET_SUBTITLE,
+ GalleryActionBar.getClusterByTypeString(context, clusterType));
+ }
+
+ // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ mActivity.getStateManager().startStateForResult(
+ AlbumSetPage.class, REQUEST_DO_ANIMATION, data);
+ }
+
+ @Override
+ protected void onCreate(Bundle data, Bundle restoreState) {
+ super.onCreate(data, restoreState);
+ mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER);
+ initializeViews();
+ initializeData(data);
+ mGetContent = data.getBoolean(Gallery.KEY_GET_CONTENT, false);
+ mShowClusterMenu = data.getBoolean(KEY_SHOW_CLUSTER_MENU, false);
+ mDetailsSource = new MyDetailsSource();
+ Context context = mActivity.getAndroidContext();
+
+ if (data.getBoolean(KEY_AUTO_SELECT_ALL)) {
+ mSelectionManager.selectAll();
+ }
+
+ mLaunchedFromPhotoPage =
+ mActivity.getStateManager().hasStateClass(FilmstripPage.class);
+ mInCameraApp = data.getBoolean(PhotoPage.KEY_APP_BRIDGE, false);
+
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_PICK_PHOTO: {
+ pickPhoto(message.arg1);
+ break;
+ }
+ default:
+ throw new AssertionError(message.what);
+ }
+ }
+ };
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsActive = true;
+
+ mResumeEffect = mActivity.getTransitionStore().get(KEY_RESUME_ANIMATION);
+ if (mResumeEffect != null) {
+ mAlbumView.setSlotFilter(mResumeEffect);
+ mResumeEffect.setPositionProvider(mPositionProvider);
+ mResumeEffect.start();
+ }
+
+ setContentPane(mRootPane);
+
+ boolean enableHomeButton = (mActivity.getStateManager().getStateCount() > 1) |
+ mParentMediaSetString != null;
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ actionBar.setDisplayOptions(enableHomeButton, false);
+ if (!mGetContent) {
+ actionBar.enableAlbumModeMenu(GalleryActionBar.ALBUM_GRID_MODE_SELECTED, this);
+ }
+
+ // Set the reload bit here to prevent it exit this page in clearLoadingBit().
+ setLoadingBit(BIT_LOADING_RELOAD);
+ mLoadingFailed = false;
+ mAlbumDataAdapter.resume();
+
+ mAlbumView.resume();
+ mAlbumView.setPressedIndex(-1);
+ mActionModeHandler.resume();
+ if (!mInitialSynced) {
+ setLoadingBit(BIT_LOADING_SYNC);
+ mSyncTask = mMediaSet.requestSync(this);
+ }
+ mInCameraAndWantQuitOnPause = mInCameraApp;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mIsActive = false;
+
+ if (mSelectionManager.inSelectionMode()) {
+ mSelectionManager.leaveSelectionMode();
+ }
+ mAlbumView.setSlotFilter(null);
+ mActionModeHandler.pause();
+ mAlbumDataAdapter.pause();
+ mAlbumView.pause();
+ DetailsHelper.pause();
+ if (!mGetContent) {
+ mActivity.getGalleryActionBar().disableAlbumModeMenu(true);
+ }
+
+ if (mSyncTask != null) {
+ mSyncTask.cancel();
+ mSyncTask = null;
+ clearLoadingBit(BIT_LOADING_SYNC);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mAlbumDataAdapter != null) {
+ mAlbumDataAdapter.setLoadingListener(null);
+ }
+ mActionModeHandler.destroy();
+ }
+
+ private void initializeViews() {
+ mSelectionManager = new SelectionManager(mActivity, false);
+ mSelectionManager.setSelectionListener(this);
+ Config.AlbumPage config = Config.AlbumPage.get(mActivity);
+ mSlotView = new SlotView(mActivity, config.slotViewSpec);
+ mAlbumView = new AlbumSlotRenderer(mActivity, mSlotView,
+ mSelectionManager, config.placeholderColor);
+ mSlotView.setSlotRenderer(mAlbumView);
+ mRootPane.addComponent(mSlotView);
+ mSlotView.setListener(new SlotView.SimpleListener() {
+ @Override
+ public void onDown(int index) {
+ AlbumPage.this.onDown(index);
+ }
+
+ @Override
+ public void onUp(boolean followedByLongPress) {
+ AlbumPage.this.onUp(followedByLongPress);
+ }
+
+ @Override
+ public void onSingleTapUp(int slotIndex) {
+ AlbumPage.this.onSingleTapUp(slotIndex);
+ }
+
+ @Override
+ public void onLongTap(int slotIndex) {
+ AlbumPage.this.onLongTap(slotIndex);
+ }
+ });
+ mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager);
+ mActionModeHandler.setActionModeListener(new ActionModeListener() {
+ @Override
+ public boolean onActionItemClicked(MenuItem item) {
+ return onItemSelected(item);
+ }
+ });
+ }
+
+ private void initializeData(Bundle data) {
+ mMediaSetPath = Path.fromString(data.getString(KEY_MEDIA_PATH));
+ mParentMediaSetString = data.getString(KEY_PARENT_MEDIA_PATH);
+ mMediaSet = mActivity.getDataManager().getMediaSet(mMediaSetPath);
+ if (mMediaSet == null) {
+ Utils.fail("MediaSet is null. Path = %s", mMediaSetPath);
+ }
+ mSelectionManager.setSourceMediaSet(mMediaSet);
+ mAlbumDataAdapter = new AlbumDataLoader(mActivity, mMediaSet);
+ mAlbumDataAdapter.setLoadingListener(new MyLoadingListener());
+ mAlbumView.setModel(mAlbumDataAdapter);
+ }
+
+ private void showDetails() {
+ mShowDetails = true;
+ if (mDetailsHelper == null) {
+ mDetailsHelper = new DetailsHelper(mActivity, mRootPane, mDetailsSource);
+ mDetailsHelper.setCloseListener(new CloseListener() {
+ @Override
+ public void onClose() {
+ hideDetails();
+ }
+ });
+ }
+ mDetailsHelper.show();
+ }
+
+ private void hideDetails() {
+ mShowDetails = false;
+ mDetailsHelper.hide();
+ mAlbumView.setHighlightItemPath(null);
+ mSlotView.invalidate();
+ }
+
+ @Override
+ protected boolean onCreateActionBar(Menu menu) {
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ MenuInflater inflator = getSupportMenuInflater();
+ if (mGetContent) {
+ inflator.inflate(R.menu.pickup, menu);
+ int typeBits = mData.getInt(Gallery.KEY_TYPE_BITS,
+ DataManager.INCLUDE_IMAGE);
+ actionBar.setTitle(GalleryUtils.getSelectionModePrompt(typeBits));
+ } else {
+ inflator.inflate(R.menu.album, menu);
+ actionBar.setTitle(mMediaSet.getName());
+
+ FilterUtils.setupMenuItems(actionBar, mMediaSetPath, true);
+
+ menu.findItem(R.id.action_group_by).setVisible(mShowClusterMenu);
+ menu.findItem(R.id.action_camera).setVisible(
+ MediaSetUtils.isCameraSource(mMediaSetPath)
+ && GalleryUtils.isCameraAvailable(mActivity));
+
+ }
+ actionBar.setSubtitle(null);
+ return true;
+ }
+
+ private void prepareAnimationBackToFilmstrip(int slotIndex) {
+ if (mAlbumDataAdapter == null || !mAlbumDataAdapter.isActive(slotIndex)) return;
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) return;
+ TransitionStore transitions = mActivity.getTransitionStore();
+ transitions.put(PhotoPage.KEY_INDEX_HINT, slotIndex);
+ transitions.put(PhotoPage.KEY_OPEN_ANIMATION_RECT,
+ mSlotView.getSlotRect(slotIndex, mRootPane));
+ }
+
+ private void switchToFilmstrip() {
+ if (mAlbumDataAdapter.size() < 1) return;
+ int targetPhoto = mSlotView.getVisibleStart();
+ prepareAnimationBackToFilmstrip(targetPhoto);
+ if(mLaunchedFromPhotoPage) {
+ onBackPressed();
+ } else {
+ pickPhoto(targetPhoto, true);
+ }
+ }
+
+ @Override
+ protected boolean onItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home: {
+ onUpPressed();
+ return true;
+ }
+ case R.id.action_cancel:
+ mActivity.getStateManager().finishState(this);
+ return true;
+ case R.id.action_select:
+ mSelectionManager.setAutoLeaveSelectionMode(false);
+ mSelectionManager.enterSelectionMode();
+ return true;
+ case R.id.action_group_by: {
+ mActivity.getGalleryActionBar().showClusterDialog(this);
+ return true;
+ }
+ case R.id.action_slideshow: {
+ mInCameraAndWantQuitOnPause = false;
+ Bundle data = new Bundle();
+ data.putString(SlideshowPage.KEY_SET_PATH,
+ mMediaSetPath.toString());
+ data.putBoolean(SlideshowPage.KEY_REPEAT, true);
+ mActivity.getStateManager().startStateForResult(
+ SlideshowPage.class, REQUEST_SLIDESHOW, data);
+ return true;
+ }
+ case R.id.action_details: {
+ if (mShowDetails) {
+ hideDetails();
+ } else {
+ showDetails();
+ }
+ return true;
+ }
+ case R.id.action_camera: {
+ GalleryUtils.startCameraActivity(mActivity);
+ return true;
+ }
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ protected void onStateResult(int request, int result, Intent data) {
+ switch (request) {
+ case REQUEST_SLIDESHOW: {
+ // data could be null, if there is no images in the album
+ if (data == null) return;
+ mFocusIndex = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
+ mSlotView.setCenterIndex(mFocusIndex);
+ break;
+ }
+ case REQUEST_PHOTO: {
+ if (data == null) return;
+ mFocusIndex = data.getIntExtra(PhotoPage.KEY_RETURN_INDEX_HINT, 0);
+ mSlotView.makeSlotVisible(mFocusIndex);
+ break;
+ }
+ case REQUEST_DO_ANIMATION: {
+ mSlotView.startRisingAnimation();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onSelectionModeChange(int mode) {
+ switch (mode) {
+ case SelectionManager.ENTER_SELECTION_MODE: {
+ mActionModeHandler.startActionMode();
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ break;
+ }
+ case SelectionManager.LEAVE_SELECTION_MODE: {
+ mActionModeHandler.finishActionMode();
+ mRootPane.invalidate();
+ break;
+ }
+ case SelectionManager.SELECT_ALL_MODE: {
+ mActionModeHandler.updateSupportedOperation();
+ mRootPane.invalidate();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onSelectionChange(Path path, boolean selected) {
+ int count = mSelectionManager.getSelectedCount();
+ String format = mActivity.getResources().getQuantityString(
+ R.plurals.number_of_items_selected, count);
+ mActionModeHandler.setTitle(String.format(format, count));
+ 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() {
+ GLRoot root = mActivity.getGLRoot();
+ root.lockRenderThread();
+ mSyncResult = resultCode;
+ try {
+ if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) {
+ mInitialSynced = true;
+ }
+ clearLoadingBit(BIT_LOADING_SYNC);
+ showSyncErrorIfNecessary(mLoadingFailed);
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+ });
+ }
+
+ // Show sync error toast when all the following conditions are met:
+ // (1) both loading and sync are done,
+ // (2) sync result is error,
+ // (3) the page is still active, and
+ // (4) no photo is shown or loading fails.
+ private void showSyncErrorIfNecessary(boolean loadingFailed) {
+ if ((mLoadingBits == 0) && (mSyncResult == MediaSet.SYNC_RESULT_ERROR) && mIsActive
+ && (loadingFailed || (mAlbumDataAdapter.size() == 0))) {
+ Toast.makeText(mActivity, R.string.sync_album_error,
+ Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private void setLoadingBit(int loadTaskBit) {
+ mLoadingBits |= loadTaskBit;
+ }
+
+ private void clearLoadingBit(int loadTaskBit) {
+ mLoadingBits &= ~loadTaskBit;
+ if (mLoadingBits == 0 && mIsActive) {
+ if (mAlbumDataAdapter.size() == 0) {
+ Intent result = new Intent();
+ result.putExtra(KEY_EMPTY_ALBUM, true);
+ setStateResult(Activity.RESULT_OK, result);
+ mActivity.getStateManager().finishState(this);
+ }
+ }
+ }
+
+ private class MyLoadingListener implements LoadingListener {
+ @Override
+ public void onLoadingStarted() {
+ setLoadingBit(BIT_LOADING_RELOAD);
+ mLoadingFailed = false;
+ }
+
+ @Override
+ public void onLoadingFinished(boolean loadingFailed) {
+ clearLoadingBit(BIT_LOADING_RELOAD);
+ mLoadingFailed = loadingFailed;
+ showSyncErrorIfNecessary(loadingFailed);
+ }
+ }
+
+ private class MyDetailsSource implements DetailsHelper.DetailsSource {
+ private int mIndex;
+
+ @Override
+ public int size() {
+ return mAlbumDataAdapter.size();
+ }
+
+ @Override
+ public int setIndex() {
+ Path id = mSelectionManager.getSelected(false).get(0);
+ mIndex = mAlbumDataAdapter.findItem(id);
+ return mIndex;
+ }
+
+ @Override
+ public MediaDetails getDetails() {
+ // this relies on setIndex() being called beforehand
+ MediaObject item = mAlbumDataAdapter.get(mIndex);
+ if (item != null) {
+ mAlbumView.setHighlightItemPath(item.getPath());
+ return item.getDetails();
+ } else {
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public void onAlbumModeSelected(int mode) {
+ if (mode == GalleryActionBar.ALBUM_FILMSTRIP_MODE_SELECTED) {
+ switchToFilmstrip();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/AlbumPicker.java b/src/com/android/gallery3d/app/AlbumPicker.java
new file mode 100644
index 000000000..65eb77291
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumPicker.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 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 android.content.Intent;
+import android.os.Bundle;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.DataManager;
+
+public class AlbumPicker extends PickerActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.select_album);
+ Intent intent = getIntent();
+ Bundle extras = intent.getExtras();
+ Bundle data = extras == null ? new Bundle() : new Bundle(extras);
+
+ data.putBoolean(Gallery.KEY_GET_ALBUM, true);
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+ getDataManager().getTopSetPath(DataManager.INCLUDE_IMAGE));
+ getStateManager().startState(AlbumSetPage.class, data);
+ }
+}
diff --git a/src/com/android/gallery3d/app/AlbumSetDataLoader.java b/src/com/android/gallery3d/app/AlbumSetDataLoader.java
new file mode 100644
index 000000000..cf380f812
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumSetDataLoader.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2010 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 android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.SynchronizedHandler;
+
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+public class AlbumSetDataLoader {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumSetDataAdapter";
+
+ private static final int INDEX_NONE = -1;
+
+ private static final int MIN_LOAD_COUNT = 4;
+
+ private static final int MSG_LOAD_START = 1;
+ private static final int MSG_LOAD_FINISH = 2;
+ private static final int MSG_RUN_OBJECT = 3;
+
+ public static interface DataListener {
+ public void onContentChanged(int index);
+ public void onSizeChanged(int size);
+ }
+
+ private final MediaSet[] mData;
+ private final MediaItem[] mCoverItem;
+ private final int[] mTotalCount;
+ private final long[] mItemVersion;
+ private final long[] mSetVersion;
+
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ private final MediaSet mSource;
+ private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
+ private int mSize;
+
+ private DataListener mDataListener;
+ private LoadingListener mLoadingListener;
+ private ReloadTask mReloadTask;
+
+ private final Handler mMainHandler;
+
+ private final MySourceListener mSourceListener = new MySourceListener();
+
+ public AlbumSetDataLoader(AbstractGalleryActivity activity, MediaSet albumSet, int cacheSize) {
+ mSource = Utils.checkNotNull(albumSet);
+ mCoverItem = new MediaItem[cacheSize];
+ mData = new MediaSet[cacheSize];
+ mTotalCount = new int[cacheSize];
+ mItemVersion = new long[cacheSize];
+ mSetVersion = new long[cacheSize];
+ Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION);
+ Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION);
+
+ mMainHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_RUN_OBJECT:
+ ((Runnable) message.obj).run();
+ return;
+ case MSG_LOAD_START:
+ if (mLoadingListener != null) mLoadingListener.onLoadingStarted();
+ return;
+ case MSG_LOAD_FINISH:
+ if (mLoadingListener != null) mLoadingListener.onLoadingFinished(false);
+ return;
+ }
+ }
+ };
+ }
+
+ public void pause() {
+ mReloadTask.terminate();
+ mReloadTask = null;
+ mSource.removeContentListener(mSourceListener);
+ }
+
+ public void resume() {
+ mSource.addContentListener(mSourceListener);
+ mReloadTask = new ReloadTask();
+ mReloadTask.start();
+ }
+
+ private void assertIsActive(int index) {
+ if (index < mActiveStart && index >= mActiveEnd) {
+ throw new IllegalArgumentException(String.format(
+ "%s not in (%s, %s)", index, mActiveStart, mActiveEnd));
+ }
+ }
+
+ public MediaSet getMediaSet(int index) {
+ assertIsActive(index);
+ return mData[index % mData.length];
+ }
+
+ public MediaItem getCoverItem(int index) {
+ assertIsActive(index);
+ return mCoverItem[index % mCoverItem.length];
+ }
+
+ public int getTotalCount(int index) {
+ assertIsActive(index);
+ return mTotalCount[index % mTotalCount.length];
+ }
+
+ public int getActiveStart() {
+ return mActiveStart;
+ }
+
+ public boolean isActive(int index) {
+ return index >= mActiveStart && index < mActiveEnd;
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ // Returns the index of the MediaSet with the given path or
+ // -1 if the path is not cached
+ public int findSet(Path id) {
+ int length = mData.length;
+ for (int i = mContentStart; i < mContentEnd; i++) {
+ MediaSet set = mData[i % length];
+ if (set != null && id == set.getPath()) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void clearSlot(int slotIndex) {
+ mData[slotIndex] = null;
+ mCoverItem[slotIndex] = null;
+ mTotalCount[slotIndex] = 0;
+ mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
+ mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
+ }
+
+ private void setContentWindow(int contentStart, int contentEnd) {
+ if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+ int length = mCoverItem.length;
+
+ int start = this.mContentStart;
+ int end = this.mContentEnd;
+
+ mContentStart = contentStart;
+ mContentEnd = contentEnd;
+
+ if (contentStart >= end || start >= contentEnd) {
+ for (int i = start, n = end; i < n; ++i) {
+ clearSlot(i % length);
+ }
+ } else {
+ for (int i = start; i < contentStart; ++i) {
+ clearSlot(i % length);
+ }
+ for (int i = contentEnd, n = end; i < n; ++i) {
+ clearSlot(i % length);
+ }
+ }
+ mReloadTask.notifyDirty();
+ }
+
+ public void setActiveWindow(int start, int end) {
+ if (start == mActiveStart && end == mActiveEnd) return;
+
+ Utils.assertTrue(start <= end
+ && end - start <= mCoverItem.length && end <= mSize);
+
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ int length = mCoverItem.length;
+ // If no data is visible, keep the cache content
+ if (start == end) return;
+
+ int contentStart = Utils.clamp((start + end) / 2 - length / 2,
+ 0, Math.max(0, mSize - length));
+ int contentEnd = Math.min(contentStart + length, mSize);
+ if (mContentStart > start || mContentEnd < end
+ || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) {
+ setContentWindow(contentStart, contentEnd);
+ }
+ }
+
+ private class MySourceListener implements ContentListener {
+ @Override
+ public void onContentDirty() {
+ mReloadTask.notifyDirty();
+ }
+ }
+
+ public void setModelListener(DataListener listener) {
+ mDataListener = listener;
+ }
+
+ public void setLoadingListener(LoadingListener listener) {
+ mLoadingListener = listener;
+ }
+
+ private static class UpdateInfo {
+ public long version;
+ public int index;
+
+ public int size;
+ public MediaSet item;
+ public MediaItem cover;
+ public int totalCount;
+ }
+
+ private class GetUpdateInfo implements Callable<UpdateInfo> {
+
+ private final long mVersion;
+
+ public GetUpdateInfo(long version) {
+ mVersion = version;
+ }
+
+ private int getInvalidIndex(long version) {
+ long setVersion[] = mSetVersion;
+ int length = setVersion.length;
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ int index = i % length;
+ if (setVersion[i % length] != version) return i;
+ }
+ return INDEX_NONE;
+ }
+
+ @Override
+ public UpdateInfo call() throws Exception {
+ int index = getInvalidIndex(mVersion);
+ if (index == INDEX_NONE && mSourceVersion == mVersion) return null;
+ UpdateInfo info = new UpdateInfo();
+ info.version = mSourceVersion;
+ info.index = index;
+ info.size = mSize;
+ return info;
+ }
+ }
+
+ private class UpdateContent implements Callable<Void> {
+ private final UpdateInfo mUpdateInfo;
+
+ public UpdateContent(UpdateInfo info) {
+ mUpdateInfo = info;
+ }
+
+ @Override
+ public Void call() {
+ // Avoid notifying listeners of status change after pause
+ // Otherwise gallery will be in inconsistent state after resume.
+ if (mReloadTask == null) return null;
+ UpdateInfo info = mUpdateInfo;
+ mSourceVersion = info.version;
+ if (mSize != info.size) {
+ mSize = info.size;
+ if (mDataListener != null) mDataListener.onSizeChanged(mSize);
+ if (mContentEnd > mSize) mContentEnd = mSize;
+ if (mActiveEnd > mSize) mActiveEnd = mSize;
+ }
+ // Note: info.index could be INDEX_NONE, i.e., -1
+ if (info.index >= mContentStart && info.index < mContentEnd) {
+ int pos = info.index % mCoverItem.length;
+ mSetVersion[pos] = info.version;
+ long itemVersion = info.item.getDataVersion();
+ if (mItemVersion[pos] == itemVersion) return null;
+ mItemVersion[pos] = itemVersion;
+ mData[pos] = info.item;
+ mCoverItem[pos] = info.cover;
+ mTotalCount[pos] = info.totalCount;
+ if (mDataListener != null
+ && info.index >= mActiveStart && info.index < mActiveEnd) {
+ mDataListener.onContentChanged(info.index);
+ }
+ }
+ return null;
+ }
+ }
+
+ private <T> T executeAndWait(Callable<T> callable) {
+ FutureTask<T> task = new FutureTask<T>(callable);
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
+ try {
+ return task.get();
+ } catch (InterruptedException e) {
+ return null;
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // TODO: load active range first
+ private class ReloadTask extends Thread {
+ private volatile boolean mActive = true;
+ private volatile boolean mDirty = true;
+ private volatile boolean mIsLoading = false;
+
+ private void updateLoading(boolean loading) {
+ if (mIsLoading == loading) return;
+ mIsLoading = loading;
+ mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
+ }
+
+ @Override
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+ boolean updateComplete = false;
+ while (mActive) {
+ synchronized (this) {
+ if (mActive && !mDirty && updateComplete) {
+ if (!mSource.isLoading()) updateLoading(false);
+ Utils.waitWithoutInterrupt(this);
+ continue;
+ }
+ }
+ mDirty = false;
+ updateLoading(true);
+
+ long version = mSource.reload();
+ UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
+ updateComplete = info == null;
+ if (updateComplete) continue;
+ if (info.version != version) {
+ info.version = version;
+ info.size = mSource.getSubMediaSetCount();
+
+ // If the size becomes smaller after reload(), we may
+ // receive from GetUpdateInfo an index which is too
+ // big. Because the main thread is not aware of the size
+ // change until we call UpdateContent.
+ if (info.index >= info.size) {
+ info.index = INDEX_NONE;
+ }
+ }
+ if (info.index != INDEX_NONE) {
+ info.item = mSource.getSubMediaSet(info.index);
+ if (info.item == null) continue;
+ info.cover = info.item.getCoverMediaItem();
+ info.totalCount = info.item.getTotalMediaItemCount();
+ }
+ executeAndWait(new UpdateContent(info));
+ }
+ updateLoading(false);
+ }
+
+ public synchronized void notifyDirty() {
+ mDirty = true;
+ notifyAll();
+ }
+
+ public synchronized void terminate() {
+ mActive = false;
+ notifyAll();
+ }
+ }
+}
+
+
diff --git a/src/com/android/gallery3d/app/AlbumSetPage.java b/src/com/android/gallery3d/app/AlbumSetPage.java
new file mode 100644
index 000000000..dd9d8ec41
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumSetPage.java
@@ -0,0 +1,764 @@
+/*
+ * Copyright (C) 2010 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 android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.HapticFeedbackConstants;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.RelativeLayout;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.FadeTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.settings.GallerySettings;
+import com.android.gallery3d.ui.ActionModeHandler;
+import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener;
+import com.android.gallery3d.ui.AlbumSetSlotRenderer;
+import com.android.gallery3d.ui.DetailsHelper;
+import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.HelpUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+public class AlbumSetPage extends ActivityState implements
+ SelectionManager.SelectionListener, GalleryActionBar.ClusterRunner,
+ EyePosition.EyePositionListener, MediaSet.SyncListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumSetPage";
+
+ private static final int MSG_PICK_ALBUM = 1;
+
+ public static final String KEY_MEDIA_PATH = "media-path";
+ public static final String KEY_SET_TITLE = "set-title";
+ public static final String KEY_SET_SUBTITLE = "set-subtitle";
+ public static final String KEY_SELECTED_CLUSTER_TYPE = "selected-cluster";
+
+ private static final int DATA_CACHE_SIZE = 256;
+ private static final int REQUEST_DO_ANIMATION = 1;
+
+ private static final int BIT_LOADING_RELOAD = 1;
+ private static final int BIT_LOADING_SYNC = 2;
+
+ private boolean mIsActive = false;
+ private SlotView mSlotView;
+ private AlbumSetSlotRenderer mAlbumSetView;
+ private Config.AlbumSetPage mConfig;
+
+ private MediaSet mMediaSet;
+ private String mTitle;
+ private String mSubtitle;
+ private boolean mShowClusterMenu;
+ private GalleryActionBar mActionBar;
+ private int mSelectedAction;
+
+ protected SelectionManager mSelectionManager;
+ private AlbumSetDataLoader mAlbumSetDataAdapter;
+
+ private boolean mGetContent;
+ private boolean mGetAlbum;
+ private ActionModeHandler mActionModeHandler;
+ private DetailsHelper mDetailsHelper;
+ private MyDetailsSource mDetailsSource;
+ private boolean mShowDetails;
+ private EyePosition mEyePosition;
+ private Handler mHandler;
+
+ // The eyes' position of the user, the origin is at the center of the
+ // device and the unit is in pixels.
+ private float mX;
+ private float mY;
+ private float mZ;
+
+ private Future<Integer> mSyncTask = null;
+
+ private int mLoadingBits = 0;
+ private boolean mInitialSynced = false;
+
+ private Button mCameraButton;
+ private boolean mShowedEmptyToastForSelf = false;
+
+ @Override
+ protected int getBackgroundColorId() {
+ return R.color.albumset_background;
+ }
+
+ private final GLView mRootPane = new GLView() {
+ private final float mMatrix[] = new float[16];
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ mEyePosition.resetPosition();
+
+ int slotViewTop = mActionBar.getHeight() + mConfig.paddingTop;
+ int slotViewBottom = bottom - top - mConfig.paddingBottom;
+ int slotViewRight = right - left;
+
+ if (mShowDetails) {
+ mDetailsHelper.layout(left, slotViewTop, right, bottom);
+ } else {
+ mAlbumSetView.setHighlightItemPath(null);
+ }
+
+ mSlotView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ GalleryUtils.setViewPointMatrix(mMatrix,
+ getWidth() / 2 + mX, getHeight() / 2 + mY, mZ);
+ canvas.multiplyMatrix(mMatrix, 0);
+ super.render(canvas);
+ canvas.restore();
+ }
+ };
+
+ @Override
+ public void onEyePositionChanged(float x, float y, float z) {
+ mRootPane.lockRendering();
+ mX = x;
+ mY = y;
+ mZ = z;
+ mRootPane.unlockRendering();
+ mRootPane.invalidate();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mShowDetails) {
+ hideDetails();
+ } else if (mSelectionManager.inSelectionMode()) {
+ mSelectionManager.leaveSelectionMode();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ private void getSlotCenter(int slotIndex, int center[]) {
+ Rect offset = new Rect();
+ mRootPane.getBoundsOf(mSlotView, offset);
+ Rect r = mSlotView.getSlotRect(slotIndex);
+ int scrollX = mSlotView.getScrollX();
+ int scrollY = mSlotView.getScrollY();
+ center[0] = offset.left + (r.left + r.right) / 2 - scrollX;
+ center[1] = offset.top + (r.top + r.bottom) / 2 - scrollY;
+ }
+
+ public void onSingleTapUp(int slotIndex) {
+ if (!mIsActive) return;
+
+ if (mSelectionManager.inSelectionMode()) {
+ MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+ if (targetSet == null) return; // Content is dirty, we shall reload soon
+ mSelectionManager.toggle(targetSet.getPath());
+ mSlotView.invalidate();
+ } else {
+ // Show pressed-up animation for the single-tap.
+ mAlbumSetView.setPressedIndex(slotIndex);
+ mAlbumSetView.setPressedUp();
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_PICK_ALBUM, slotIndex, 0),
+ FadeTexture.DURATION);
+ }
+ }
+
+ private static boolean albumShouldOpenInFilmstrip(MediaSet album) {
+ int itemCount = album.getMediaItemCount();
+ ArrayList<MediaItem> list = (itemCount == 1) ? album.getMediaItem(0, 1) : null;
+ // open in film strip only if there's one item in the album and the item exists
+ return (list != null && !list.isEmpty());
+ }
+
+ WeakReference<Toast> mEmptyAlbumToast = null;
+
+ private void showEmptyAlbumToast(int toastLength) {
+ Toast toast;
+ if (mEmptyAlbumToast != null) {
+ toast = mEmptyAlbumToast.get();
+ if (toast != null) {
+ toast.show();
+ return;
+ }
+ }
+ toast = Toast.makeText(mActivity, R.string.empty_album, toastLength);
+ mEmptyAlbumToast = new WeakReference<Toast>(toast);
+ toast.show();
+ }
+
+ private void hideEmptyAlbumToast() {
+ if (mEmptyAlbumToast != null) {
+ Toast toast = mEmptyAlbumToast.get();
+ if (toast != null) toast.cancel();
+ }
+ }
+
+ private void pickAlbum(int slotIndex) {
+ if (!mIsActive) return;
+
+ MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+ if (targetSet == null) return; // Content is dirty, we shall reload soon
+ if (targetSet.getTotalMediaItemCount() == 0) {
+ showEmptyAlbumToast(Toast.LENGTH_SHORT);
+ return;
+ }
+ hideEmptyAlbumToast();
+
+ String mediaPath = targetSet.getPath().toString();
+
+ Bundle data = new Bundle(getData());
+ int[] center = new int[2];
+ getSlotCenter(slotIndex, center);
+ data.putIntArray(AlbumPage.KEY_SET_CENTER, center);
+ if (mGetAlbum && targetSet.isLeafAlbum()) {
+ Activity activity = mActivity;
+ Intent result = new Intent()
+ .putExtra(AlbumPicker.KEY_ALBUM_PATH, targetSet.getPath().toString());
+ activity.setResult(Activity.RESULT_OK, result);
+ activity.finish();
+ } else if (targetSet.getSubMediaSetCount() > 0) {
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, mediaPath);
+ mActivity.getStateManager().startStateForResult(
+ AlbumSetPage.class, REQUEST_DO_ANIMATION, data);
+ } else {
+ if (!mGetContent && albumShouldOpenInFilmstrip(targetSet)) {
+ data.putParcelable(PhotoPage.KEY_OPEN_ANIMATION_RECT,
+ mSlotView.getSlotRect(slotIndex, mRootPane));
+ data.putInt(PhotoPage.KEY_INDEX_HINT, 0);
+ data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
+ mediaPath);
+ data.putBoolean(PhotoPage.KEY_START_IN_FILMSTRIP, true);
+ data.putBoolean(PhotoPage.KEY_IN_CAMERA_ROLL, targetSet.isCameraRoll());
+ mActivity.getStateManager().startStateForResult(
+ FilmstripPage.class, AlbumPage.REQUEST_PHOTO, data);
+ return;
+ }
+ data.putString(AlbumPage.KEY_MEDIA_PATH, mediaPath);
+
+ // We only show cluster menu in the first AlbumPage in stack
+ boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
+ data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum);
+ mActivity.getStateManager().startStateForResult(
+ AlbumPage.class, REQUEST_DO_ANIMATION, data);
+ }
+ }
+
+ private void onDown(int index) {
+ mAlbumSetView.setPressedIndex(index);
+ }
+
+ private void onUp(boolean followedByLongPress) {
+ if (followedByLongPress) {
+ // Avoid showing press-up animations for long-press.
+ mAlbumSetView.setPressedIndex(-1);
+ } else {
+ mAlbumSetView.setPressedUp();
+ }
+ }
+
+ public void onLongTap(int slotIndex) {
+ if (mGetContent || mGetAlbum) return;
+ MediaSet set = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+ if (set == null) return;
+ mSelectionManager.setAutoLeaveSelectionMode(true);
+ mSelectionManager.toggle(set.getPath());
+ mSlotView.invalidate();
+ }
+
+ @Override
+ public void doCluster(int clusterType) {
+ String basePath = mMediaSet.getPath().toString();
+ String newPath = FilterUtils.switchClusterPath(basePath, clusterType);
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath);
+ data.putInt(KEY_SELECTED_CLUSTER_TYPE, clusterType);
+ mActivity.getStateManager().switchState(this, AlbumSetPage.class, data);
+ }
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ super.onCreate(data, restoreState);
+ initializeViews();
+ initializeData(data);
+ Context context = mActivity.getAndroidContext();
+ mGetContent = data.getBoolean(Gallery.KEY_GET_CONTENT, false);
+ mGetAlbum = data.getBoolean(Gallery.KEY_GET_ALBUM, false);
+ mTitle = data.getString(AlbumSetPage.KEY_SET_TITLE);
+ mSubtitle = data.getString(AlbumSetPage.KEY_SET_SUBTITLE);
+ mEyePosition = new EyePosition(context, this);
+ mDetailsSource = new MyDetailsSource();
+ mActionBar = mActivity.getGalleryActionBar();
+ mSelectedAction = data.getInt(AlbumSetPage.KEY_SELECTED_CLUSTER_TYPE,
+ FilterUtils.CLUSTER_BY_ALBUM);
+
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_PICK_ALBUM: {
+ pickAlbum(message.arg1);
+ break;
+ }
+ default: throw new AssertionError(message.what);
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ cleanupCameraButton();
+ mActionModeHandler.destroy();
+ }
+
+ private boolean setupCameraButton() {
+ if (!GalleryUtils.isCameraAvailable(mActivity)) return false;
+ RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity)
+ .findViewById(R.id.gallery_root);
+ if (galleryRoot == null) return false;
+
+ mCameraButton = new Button(mActivity);
+ mCameraButton.setText(R.string.camera_label);
+ mCameraButton.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.frame_overlay_gallery_camera, 0, 0);
+ mCameraButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ GalleryUtils.startCameraActivity(mActivity);
+ }
+ });
+ RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.WRAP_CONTENT,
+ RelativeLayout.LayoutParams.WRAP_CONTENT);
+ lp.addRule(RelativeLayout.CENTER_IN_PARENT);
+ galleryRoot.addView(mCameraButton, lp);
+ return true;
+ }
+
+ private void cleanupCameraButton() {
+ if (mCameraButton == null) return;
+ RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity)
+ .findViewById(R.id.gallery_root);
+ if (galleryRoot == null) return;
+ galleryRoot.removeView(mCameraButton);
+ mCameraButton = null;
+ }
+
+ private void showCameraButton() {
+ if (mCameraButton == null && !setupCameraButton()) return;
+ mCameraButton.setVisibility(View.VISIBLE);
+ }
+
+ private void hideCameraButton() {
+ if (mCameraButton == null) return;
+ mCameraButton.setVisibility(View.GONE);
+ }
+
+ private void clearLoadingBit(int loadingBit) {
+ mLoadingBits &= ~loadingBit;
+ if (mLoadingBits == 0 && mIsActive) {
+ if (mAlbumSetDataAdapter.size() == 0) {
+ // If this is not the top of the gallery folder hierarchy,
+ // tell the parent AlbumSetPage instance to handle displaying
+ // the empty album toast, otherwise show it within this
+ // instance
+ if (mActivity.getStateManager().getStateCount() > 1) {
+ Intent result = new Intent();
+ result.putExtra(AlbumPage.KEY_EMPTY_ALBUM, true);
+ setStateResult(Activity.RESULT_OK, result);
+ mActivity.getStateManager().finishState(this);
+ } else {
+ mShowedEmptyToastForSelf = true;
+ showEmptyAlbumToast(Toast.LENGTH_LONG);
+ mSlotView.invalidate();
+ showCameraButton();
+ }
+ return;
+ }
+ }
+ // Hide the empty album toast if we are in the root instance of
+ // AlbumSetPage and the album is no longer empty (for instance,
+ // after a sync is completed and web albums have been synced)
+ if (mShowedEmptyToastForSelf) {
+ mShowedEmptyToastForSelf = false;
+ hideEmptyAlbumToast();
+ hideCameraButton();
+ }
+ }
+
+ private void setLoadingBit(int loadingBit) {
+ mLoadingBits |= loadingBit;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mIsActive = false;
+ mAlbumSetDataAdapter.pause();
+ mAlbumSetView.pause();
+ mActionModeHandler.pause();
+ mEyePosition.pause();
+ DetailsHelper.pause();
+ // Call disableClusterMenu to avoid receiving callback after paused.
+ // Don't hide menu here otherwise the list menu will disappear earlier than
+ // the action bar, which is janky and unwanted behavior.
+ mActionBar.disableClusterMenu(false);
+ if (mSyncTask != null) {
+ mSyncTask.cancel();
+ mSyncTask = null;
+ clearLoadingBit(BIT_LOADING_SYNC);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mIsActive = true;
+ setContentPane(mRootPane);
+
+ // Set the reload bit here to prevent it exit this page in clearLoadingBit().
+ setLoadingBit(BIT_LOADING_RELOAD);
+ mAlbumSetDataAdapter.resume();
+
+ mAlbumSetView.resume();
+ mEyePosition.resume();
+ mActionModeHandler.resume();
+ if (mShowClusterMenu) {
+ mActionBar.enableClusterMenu(mSelectedAction, this);
+ }
+ if (!mInitialSynced) {
+ setLoadingBit(BIT_LOADING_SYNC);
+ mSyncTask = mMediaSet.requestSync(AlbumSetPage.this);
+ }
+ }
+
+ private void initializeData(Bundle data) {
+ String mediaPath = data.getString(AlbumSetPage.KEY_MEDIA_PATH);
+ mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
+ mSelectionManager.setSourceMediaSet(mMediaSet);
+ mAlbumSetDataAdapter = new AlbumSetDataLoader(
+ mActivity, mMediaSet, DATA_CACHE_SIZE);
+ mAlbumSetDataAdapter.setLoadingListener(new MyLoadingListener());
+ mAlbumSetView.setModel(mAlbumSetDataAdapter);
+ }
+
+ private void initializeViews() {
+ mSelectionManager = new SelectionManager(mActivity, true);
+ mSelectionManager.setSelectionListener(this);
+
+ mConfig = Config.AlbumSetPage.get(mActivity);
+ mSlotView = new SlotView(mActivity, mConfig.slotViewSpec);
+ mAlbumSetView = new AlbumSetSlotRenderer(
+ mActivity, mSelectionManager, mSlotView, mConfig.labelSpec,
+ mConfig.placeholderColor);
+ mSlotView.setSlotRenderer(mAlbumSetView);
+ mSlotView.setListener(new SlotView.SimpleListener() {
+ @Override
+ public void onDown(int index) {
+ AlbumSetPage.this.onDown(index);
+ }
+
+ @Override
+ public void onUp(boolean followedByLongPress) {
+ AlbumSetPage.this.onUp(followedByLongPress);
+ }
+
+ @Override
+ public void onSingleTapUp(int slotIndex) {
+ AlbumSetPage.this.onSingleTapUp(slotIndex);
+ }
+
+ @Override
+ public void onLongTap(int slotIndex) {
+ AlbumSetPage.this.onLongTap(slotIndex);
+ }
+ });
+
+ mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager);
+ mActionModeHandler.setActionModeListener(new ActionModeListener() {
+ @Override
+ public boolean onActionItemClicked(MenuItem item) {
+ return onItemSelected(item);
+ }
+ });
+ mRootPane.addComponent(mSlotView);
+ }
+
+ @Override
+ protected boolean onCreateActionBar(Menu menu) {
+ Activity activity = mActivity;
+ final boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
+ MenuInflater inflater = getSupportMenuInflater();
+
+ if (mGetContent) {
+ inflater.inflate(R.menu.pickup, menu);
+ int typeBits = mData.getInt(
+ Gallery.KEY_TYPE_BITS, DataManager.INCLUDE_IMAGE);
+ mActionBar.setTitle(GalleryUtils.getSelectionModePrompt(typeBits));
+ } else if (mGetAlbum) {
+ inflater.inflate(R.menu.pickup, menu);
+ mActionBar.setTitle(R.string.select_album);
+ } else {
+ inflater.inflate(R.menu.albumset, menu);
+ boolean wasShowingClusterMenu = mShowClusterMenu;
+ mShowClusterMenu = !inAlbum;
+ boolean selectAlbums = !inAlbum &&
+ mActionBar.getClusterTypeAction() == FilterUtils.CLUSTER_BY_ALBUM;
+ MenuItem selectItem = menu.findItem(R.id.action_select);
+ selectItem.setTitle(activity.getString(
+ selectAlbums ? R.string.select_album : R.string.select_group));
+
+ MenuItem cameraItem = menu.findItem(R.id.action_camera);
+ cameraItem.setVisible(GalleryUtils.isCameraAvailable(activity));
+
+ FilterUtils.setupMenuItems(mActionBar, mMediaSet.getPath(), false);
+
+ Intent helpIntent = HelpUtils.getHelpIntent(activity);
+
+ MenuItem helpItem = menu.findItem(R.id.action_general_help);
+ helpItem.setVisible(helpIntent != null);
+ if (helpIntent != null) helpItem.setIntent(helpIntent);
+
+ mActionBar.setTitle(mTitle);
+ mActionBar.setSubtitle(mSubtitle);
+ if (mShowClusterMenu != wasShowingClusterMenu) {
+ if (mShowClusterMenu) {
+ mActionBar.enableClusterMenu(mSelectedAction, this);
+ } else {
+ mActionBar.disableClusterMenu(true);
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean onItemSelected(MenuItem item) {
+ Activity activity = mActivity;
+ switch (item.getItemId()) {
+ case R.id.action_cancel:
+ activity.setResult(Activity.RESULT_CANCELED);
+ activity.finish();
+ return true;
+ case R.id.action_select:
+ mSelectionManager.setAutoLeaveSelectionMode(false);
+ mSelectionManager.enterSelectionMode();
+ return true;
+ case R.id.action_details:
+ if (mAlbumSetDataAdapter.size() != 0) {
+ if (mShowDetails) {
+ hideDetails();
+ } else {
+ showDetails();
+ }
+ } else {
+ Toast.makeText(activity,
+ activity.getText(R.string.no_albums_alert),
+ Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ case R.id.action_camera: {
+ GalleryUtils.startCameraActivity(activity);
+ return true;
+ }
+ case R.id.action_manage_offline: {
+ Bundle data = new Bundle();
+ String mediaPath = mActivity.getDataManager().getTopSetPath(
+ DataManager.INCLUDE_ALL);
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, mediaPath);
+ mActivity.getStateManager().startState(ManageCachePage.class, data);
+ return true;
+ }
+ case R.id.action_sync_picasa_albums: {
+ PicasaSource.requestSync(activity);
+ return true;
+ }
+ case R.id.action_settings: {
+ activity.startActivity(new Intent(activity, GallerySettings.class));
+ return true;
+ }
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ protected void onStateResult(int requestCode, int resultCode, Intent data) {
+ if (data != null && data.getBooleanExtra(AlbumPage.KEY_EMPTY_ALBUM, false)) {
+ showEmptyAlbumToast(Toast.LENGTH_SHORT);
+ }
+ switch (requestCode) {
+ case REQUEST_DO_ANIMATION: {
+ mSlotView.startRisingAnimation();
+ }
+ }
+ }
+
+ private String getSelectedString() {
+ int count = mSelectionManager.getSelectedCount();
+ int action = mActionBar.getClusterTypeAction();
+ int string = action == FilterUtils.CLUSTER_BY_ALBUM
+ ? R.plurals.number_of_albums_selected
+ : R.plurals.number_of_groups_selected;
+ String format = mActivity.getResources().getQuantityString(string, count);
+ return String.format(format, count);
+ }
+
+ @Override
+ public void onSelectionModeChange(int mode) {
+ switch (mode) {
+ case SelectionManager.ENTER_SELECTION_MODE: {
+ mActionBar.disableClusterMenu(true);
+ mActionModeHandler.startActionMode();
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ break;
+ }
+ case SelectionManager.LEAVE_SELECTION_MODE: {
+ mActionModeHandler.finishActionMode();
+ if (mShowClusterMenu) {
+ mActionBar.enableClusterMenu(mSelectedAction, this);
+ }
+ mRootPane.invalidate();
+ break;
+ }
+ case SelectionManager.SELECT_ALL_MODE: {
+ mActionModeHandler.updateSupportedOperation();
+ mRootPane.invalidate();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onSelectionChange(Path path, boolean selected) {
+ mActionModeHandler.setTitle(getSelectedString());
+ mActionModeHandler.updateSupportedOperation(path, selected);
+ }
+
+ private void hideDetails() {
+ mShowDetails = false;
+ mDetailsHelper.hide();
+ mAlbumSetView.setHighlightItemPath(null);
+ mSlotView.invalidate();
+ }
+
+ private void showDetails() {
+ mShowDetails = true;
+ if (mDetailsHelper == null) {
+ mDetailsHelper = new DetailsHelper(mActivity, mRootPane, mDetailsSource);
+ mDetailsHelper.setCloseListener(new CloseListener() {
+ @Override
+ public void onClose() {
+ hideDetails();
+ }
+ });
+ }
+ 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() {
+ GLRoot root = mActivity.getGLRoot();
+ root.lockRenderThread();
+ try {
+ if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) {
+ mInitialSynced = true;
+ }
+ clearLoadingBit(BIT_LOADING_SYNC);
+ if (resultCode == MediaSet.SYNC_RESULT_ERROR && mIsActive) {
+ Log.w(TAG, "failed to load album set");
+ }
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+ });
+ }
+
+ private class MyLoadingListener implements LoadingListener {
+ @Override
+ public void onLoadingStarted() {
+ setLoadingBit(BIT_LOADING_RELOAD);
+ }
+
+ @Override
+ public void onLoadingFinished(boolean loadingFailed) {
+ clearLoadingBit(BIT_LOADING_RELOAD);
+ }
+ }
+
+ private class MyDetailsSource implements DetailsHelper.DetailsSource {
+ private int mIndex;
+
+ @Override
+ public int size() {
+ return mAlbumSetDataAdapter.size();
+ }
+
+ @Override
+ public int setIndex() {
+ Path id = mSelectionManager.getSelected(false).get(0);
+ mIndex = mAlbumSetDataAdapter.findSet(id);
+ return mIndex;
+ }
+
+ @Override
+ public MediaDetails getDetails() {
+ MediaObject item = mAlbumSetDataAdapter.getMediaSet(mIndex);
+ if (item != null) {
+ mAlbumSetView.setHighlightItemPath(item.getPath());
+ return item.getDetails();
+ } else {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/AppBridge.java b/src/com/android/gallery3d/app/AppBridge.java
new file mode 100644
index 000000000..ee55fa6db
--- /dev/null
+++ b/src/com/android/gallery3d/app/AppBridge.java
@@ -0,0 +1,72 @@
+/*
+ * 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 android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.gallery3d.ui.ScreenNail;
+
+// This is the bridge to connect a PhotoPage to the external environment.
+public abstract class AppBridge implements Parcelable {
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ // These are requests sent from PhotoPage to the app
+ //////////////////////////////////////////////////////////////////////////
+
+ public abstract boolean isPanorama();
+ public abstract boolean isStaticCamera();
+ public abstract ScreenNail attachScreenNail();
+ public abstract void detachScreenNail();
+
+ // Return true if the tap is consumed.
+ public abstract boolean onSingleTapUp(int x, int y);
+
+ // This is used to notify that the screen nail will be drawn in full screen
+ // or not in next draw() call.
+ public abstract void onFullScreenChanged(boolean full);
+
+ //////////////////////////////////////////////////////////////////////////
+ // These are requests send from app to PhotoPage
+ //////////////////////////////////////////////////////////////////////////
+
+ public interface Server {
+ // Set the camera frame relative to GLRootView.
+ public void setCameraRelativeFrame(Rect frame);
+ // Switch to the previous or next picture using the capture animation.
+ // The offset is -1 to switch to the previous picture, 1 to switch to
+ // the next picture.
+ public boolean switchWithCaptureAnimation(int offset);
+ // Enable or disable the swiping gestures (the default is enabled).
+ public void setSwipingEnabled(boolean enabled);
+ // Notify that the ScreenNail is changed.
+ public void notifyScreenNailChanged();
+ // Add a new media item to the secure album.
+ public void addSecureAlbumItem(boolean isVideo, int id);
+ }
+
+ // If server is null, the services are not available.
+ public abstract void setServer(Server server);
+}
diff --git a/src/com/android/gallery3d/app/BatchService.java b/src/com/android/gallery3d/app/BatchService.java
new file mode 100644
index 000000000..564001d5b
--- /dev/null
+++ b/src/com/android/gallery3d/app/BatchService.java
@@ -0,0 +1,48 @@
+/*
+ * 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 android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import com.android.gallery3d.util.ThreadPool;
+
+public class BatchService extends Service {
+
+ public class LocalBinder extends Binder {
+ BatchService getService() {
+ return BatchService.this;
+ }
+ }
+
+ private final IBinder mBinder = new LocalBinder();
+ private ThreadPool mThreadPool = new ThreadPool(1, 1);
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ // The threadpool returned by getThreadPool must have only 1 thread
+ // running at a time, as MenuExecutor (atrociously) depends on this
+ // guarantee for synchronization.
+ public ThreadPool getThreadPool() {
+ return mThreadPool;
+ }
+}
diff --git a/src/com/android/gallery3d/app/CommonControllerOverlay.java b/src/com/android/gallery3d/app/CommonControllerOverlay.java
new file mode 100644
index 000000000..9adb4e7a8
--- /dev/null
+++ b/src/com/android/gallery3d/app/CommonControllerOverlay.java
@@ -0,0 +1,346 @@
+/*
+ * 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 android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+
+/**
+ * The common playback controller for the Movie Player or Video Trimming.
+ */
+public abstract class CommonControllerOverlay extends FrameLayout implements
+ ControllerOverlay,
+ OnClickListener,
+ TimeBar.Listener {
+
+ protected enum State {
+ PLAYING,
+ PAUSED,
+ ENDED,
+ ERROR,
+ LOADING
+ }
+
+ private static final float ERROR_MESSAGE_RELATIVE_PADDING = 1.0f / 6;
+
+ protected Listener mListener;
+
+ protected final View mBackground;
+ protected TimeBar mTimeBar;
+
+ protected View mMainView;
+ protected final LinearLayout mLoadingView;
+ protected final TextView mErrorView;
+ protected final ImageView mPlayPauseReplayView;
+
+ protected State mState;
+
+ protected boolean mCanReplay = true;
+
+ public void setSeekable(boolean canSeek) {
+ mTimeBar.setSeekable(canSeek);
+ }
+
+ public CommonControllerOverlay(Context context) {
+ super(context);
+
+ mState = State.LOADING;
+ // TODO: Move the following layout code into xml file.
+ LayoutParams wrapContent =
+ new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ LayoutParams matchParent =
+ new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+
+ mBackground = new View(context);
+ mBackground.setBackgroundColor(context.getResources().getColor(R.color.darker_transparent));
+ addView(mBackground, matchParent);
+
+ // Depending on the usage, the timeBar can show a single scrubber, or
+ // multiple ones for trimming.
+ createTimeBar(context);
+ addView(mTimeBar, wrapContent);
+ mTimeBar.setContentDescription(
+ context.getResources().getString(R.string.accessibility_time_bar));
+ mLoadingView = new LinearLayout(context);
+ mLoadingView.setOrientation(LinearLayout.VERTICAL);
+ mLoadingView.setGravity(Gravity.CENTER_HORIZONTAL);
+ ProgressBar spinner = new ProgressBar(context);
+ spinner.setIndeterminate(true);
+ mLoadingView.addView(spinner, wrapContent);
+ TextView loadingText = createOverlayTextView(context);
+ loadingText.setText(R.string.loading_video);
+ mLoadingView.addView(loadingText, wrapContent);
+ addView(mLoadingView, wrapContent);
+
+ mPlayPauseReplayView = new ImageView(context);
+ mPlayPauseReplayView.setImageResource(R.drawable.ic_vidcontrol_play);
+ mPlayPauseReplayView.setContentDescription(
+ context.getResources().getString(R.string.accessibility_play_video));
+ mPlayPauseReplayView.setBackgroundResource(R.drawable.bg_vidcontrol);
+ mPlayPauseReplayView.setScaleType(ScaleType.CENTER);
+ mPlayPauseReplayView.setFocusable(true);
+ mPlayPauseReplayView.setClickable(true);
+ mPlayPauseReplayView.setOnClickListener(this);
+ addView(mPlayPauseReplayView, wrapContent);
+
+ mErrorView = createOverlayTextView(context);
+ addView(mErrorView, matchParent);
+
+ RelativeLayout.LayoutParams params =
+ new RelativeLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ setLayoutParams(params);
+ hide();
+ }
+
+ abstract protected void createTimeBar(Context context);
+
+ private TextView createOverlayTextView(Context context) {
+ TextView view = new TextView(context);
+ view.setGravity(Gravity.CENTER);
+ view.setTextColor(0xFFFFFFFF);
+ view.setPadding(0, 15, 0, 15);
+ return view;
+ }
+
+ @Override
+ public void setListener(Listener listener) {
+ this.mListener = listener;
+ }
+
+ @Override
+ public void setCanReplay(boolean canReplay) {
+ this.mCanReplay = canReplay;
+ }
+
+ @Override
+ public View getView() {
+ return this;
+ }
+
+ @Override
+ public void showPlaying() {
+ mState = State.PLAYING;
+ showMainView(mPlayPauseReplayView);
+ }
+
+ @Override
+ public void showPaused() {
+ mState = State.PAUSED;
+ showMainView(mPlayPauseReplayView);
+ }
+
+ @Override
+ public void showEnded() {
+ mState = State.ENDED;
+ if (mCanReplay) showMainView(mPlayPauseReplayView);
+ }
+
+ @Override
+ public void showLoading() {
+ mState = State.LOADING;
+ showMainView(mLoadingView);
+ }
+
+ @Override
+ public void showErrorMessage(String message) {
+ mState = State.ERROR;
+ int padding = (int) (getMeasuredWidth() * ERROR_MESSAGE_RELATIVE_PADDING);
+ mErrorView.setPadding(
+ padding, mErrorView.getPaddingTop(), padding, mErrorView.getPaddingBottom());
+ mErrorView.setText(message);
+ showMainView(mErrorView);
+ }
+
+ @Override
+ public void setTimes(int currentTime, int totalTime,
+ int trimStartTime, int trimEndTime) {
+ mTimeBar.setTime(currentTime, totalTime, trimStartTime, trimEndTime);
+ }
+
+ public void hide() {
+ mPlayPauseReplayView.setVisibility(View.INVISIBLE);
+ mLoadingView.setVisibility(View.INVISIBLE);
+ mBackground.setVisibility(View.INVISIBLE);
+ mTimeBar.setVisibility(View.INVISIBLE);
+ setVisibility(View.INVISIBLE);
+ setFocusable(true);
+ requestFocus();
+ }
+
+ private void showMainView(View view) {
+ mMainView = view;
+ mErrorView.setVisibility(mMainView == mErrorView ? View.VISIBLE : View.INVISIBLE);
+ mLoadingView.setVisibility(mMainView == mLoadingView ? View.VISIBLE : View.INVISIBLE);
+ mPlayPauseReplayView.setVisibility(
+ mMainView == mPlayPauseReplayView ? View.VISIBLE : View.INVISIBLE);
+ show();
+ }
+
+ @Override
+ public void show() {
+ updateViews();
+ setVisibility(View.VISIBLE);
+ setFocusable(false);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (mListener != null) {
+ if (view == mPlayPauseReplayView) {
+ if (mState == State.ENDED) {
+ if (mCanReplay) {
+ mListener.onReplay();
+ }
+ } else if (mState == State.PAUSED || mState == State.PLAYING) {
+ mListener.onPlayPause();
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (super.onTouchEvent(event)) {
+ return true;
+ }
+ return false;
+ }
+
+ // The paddings of 4 sides which covered by system components. E.g.
+ // +-----------------+\
+ // | Action Bar | insets.top
+ // +-----------------+/
+ // | |
+ // | Content Area | insets.right = insets.left = 0
+ // | |
+ // +-----------------+\
+ // | Navigation Bar | insets.bottom
+ // +-----------------+/
+ // Please see View.fitSystemWindows() for more details.
+ private final Rect mWindowInsets = new Rect();
+
+ @Override
+ protected boolean fitSystemWindows(Rect insets) {
+ // We don't set the paddings of this View, otherwise,
+ // the content will get cropped outside window
+ mWindowInsets.set(insets);
+ return true;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ Rect insets = mWindowInsets;
+ int pl = insets.left; // the left paddings
+ int pr = insets.right;
+ int pt = insets.top;
+ int pb = insets.bottom;
+
+ int h = bottom - top;
+ int w = right - left;
+ boolean error = mErrorView.getVisibility() == View.VISIBLE;
+
+ int y = h - pb;
+ // Put both TimeBar and Background just above the bottom system
+ // component.
+ // But extend the background to the width of the screen, since we don't
+ // care if it will be covered by a system component and it looks better.
+ mBackground.layout(0, y - mTimeBar.getBarHeight(), w, y);
+ mTimeBar.layout(pl, y - mTimeBar.getPreferredHeight(), w - pr, y);
+
+ // Put the play/pause/next/ previous button in the center of the screen
+ layoutCenteredView(mPlayPauseReplayView, 0, 0, w, h);
+
+ if (mMainView != null) {
+ layoutCenteredView(mMainView, 0, 0, w, h);
+ }
+ }
+
+ private void layoutCenteredView(View view, int l, int t, int r, int b) {
+ int cw = view.getMeasuredWidth();
+ int ch = view.getMeasuredHeight();
+ int cl = (r - l - cw) / 2;
+ int ct = (b - t - ch) / 2;
+ view.layout(cl, ct, cl + cw, ct + ch);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ measureChildren(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ protected void updateViews() {
+ mBackground.setVisibility(View.VISIBLE);
+ mTimeBar.setVisibility(View.VISIBLE);
+ Resources resources = getContext().getResources();
+ int imageResource = R.drawable.ic_vidcontrol_reload;
+ String contentDescription = resources.getString(R.string.accessibility_reload_video);
+ if (mState == State.PAUSED) {
+ imageResource = R.drawable.ic_vidcontrol_play;
+ contentDescription = resources.getString(R.string.accessibility_play_video);
+ } else if (mState == State.PLAYING) {
+ imageResource = R.drawable.ic_vidcontrol_pause;
+ contentDescription = resources.getString(R.string.accessibility_pause_video);
+ }
+
+ mPlayPauseReplayView.setImageResource(imageResource);
+ mPlayPauseReplayView.setContentDescription(contentDescription);
+ mPlayPauseReplayView.setVisibility(
+ (mState != State.LOADING && mState != State.ERROR &&
+ !(mState == State.ENDED && !mCanReplay))
+ ? View.VISIBLE : View.GONE);
+ requestLayout();
+ }
+
+ // TimeBar listener
+
+ @Override
+ public void onScrubbingStart() {
+ mListener.onSeekStart();
+ }
+
+ @Override
+ public void onScrubbingMove(int time) {
+ mListener.onSeekMove(time);
+ }
+
+ @Override
+ public void onScrubbingEnd(int time, int trimStartTime, int trimEndTime) {
+ mListener.onSeekEnd(time, trimStartTime, trimEndTime);
+ }
+}
diff --git a/src/com/android/gallery3d/app/Config.java b/src/com/android/gallery3d/app/Config.java
new file mode 100644
index 000000000..7183acc33
--- /dev/null
+++ b/src/com/android/gallery3d/app/Config.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2010 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 android.content.Context;
+import android.content.res.Resources;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ui.AlbumSetSlotRenderer;
+import com.android.gallery3d.ui.SlotView;
+
+final class Config {
+ public static class AlbumSetPage {
+ private static AlbumSetPage sInstance;
+
+ public SlotView.Spec slotViewSpec;
+ public AlbumSetSlotRenderer.LabelSpec labelSpec;
+ public int paddingTop;
+ public int paddingBottom;
+ public int placeholderColor;
+
+ public static synchronized AlbumSetPage get(Context context) {
+ if (sInstance == null) {
+ sInstance = new AlbumSetPage(context);
+ }
+ return sInstance;
+ }
+
+ private AlbumSetPage(Context context) {
+ Resources r = context.getResources();
+
+ placeholderColor = r.getColor(R.color.albumset_placeholder);
+
+ slotViewSpec = new SlotView.Spec();
+ slotViewSpec.rowsLand = r.getInteger(R.integer.albumset_rows_land);
+ slotViewSpec.rowsPort = r.getInteger(R.integer.albumset_rows_port);
+ slotViewSpec.slotGap = r.getDimensionPixelSize(R.dimen.albumset_slot_gap);
+ slotViewSpec.slotHeightAdditional = 0;
+
+ paddingTop = r.getDimensionPixelSize(R.dimen.albumset_padding_top);
+ paddingBottom = r.getDimensionPixelSize(R.dimen.albumset_padding_bottom);
+
+ labelSpec = new AlbumSetSlotRenderer.LabelSpec();
+ labelSpec.labelBackgroundHeight = r.getDimensionPixelSize(
+ R.dimen.albumset_label_background_height);
+ labelSpec.titleOffset = r.getDimensionPixelSize(
+ R.dimen.albumset_title_offset);
+ labelSpec.countOffset = r.getDimensionPixelSize(
+ R.dimen.albumset_count_offset);
+ labelSpec.titleFontSize = r.getDimensionPixelSize(
+ R.dimen.albumset_title_font_size);
+ labelSpec.countFontSize = r.getDimensionPixelSize(
+ R.dimen.albumset_count_font_size);
+ labelSpec.leftMargin = r.getDimensionPixelSize(
+ R.dimen.albumset_left_margin);
+ labelSpec.titleRightMargin = r.getDimensionPixelSize(
+ R.dimen.albumset_title_right_margin);
+ labelSpec.iconSize = r.getDimensionPixelSize(
+ R.dimen.albumset_icon_size);
+ labelSpec.backgroundColor = r.getColor(
+ R.color.albumset_label_background);
+ labelSpec.titleColor = r.getColor(R.color.albumset_label_title);
+ labelSpec.countColor = r.getColor(R.color.albumset_label_count);
+ }
+ }
+
+ public static class AlbumPage {
+ private static AlbumPage sInstance;
+
+ public SlotView.Spec slotViewSpec;
+ public int placeholderColor;
+
+ public static synchronized AlbumPage get(Context context) {
+ if (sInstance == null) {
+ sInstance = new AlbumPage(context);
+ }
+ return sInstance;
+ }
+
+ private AlbumPage(Context context) {
+ Resources r = context.getResources();
+
+ placeholderColor = r.getColor(R.color.album_placeholder);
+
+ slotViewSpec = new SlotView.Spec();
+ slotViewSpec.rowsLand = r.getInteger(R.integer.album_rows_land);
+ slotViewSpec.rowsPort = r.getInteger(R.integer.album_rows_port);
+ slotViewSpec.slotGap = r.getDimensionPixelSize(R.dimen.album_slot_gap);
+ }
+ }
+
+ public static class ManageCachePage extends AlbumSetPage {
+ private static ManageCachePage sInstance;
+
+ public final int cachePinSize;
+ public final int cachePinMargin;
+
+ public static synchronized ManageCachePage get(Context context) {
+ if (sInstance == null) {
+ sInstance = new ManageCachePage(context);
+ }
+ return sInstance;
+ }
+
+ public ManageCachePage(Context context) {
+ super(context);
+ Resources r = context.getResources();
+ cachePinSize = r.getDimensionPixelSize(R.dimen.cache_pin_size);
+ cachePinMargin = r.getDimensionPixelSize(R.dimen.cache_pin_margin);
+ }
+ }
+}
+
diff --git a/src/com/android/gallery3d/app/ControllerOverlay.java b/src/com/android/gallery3d/app/ControllerOverlay.java
new file mode 100644
index 000000000..078f59e28
--- /dev/null
+++ b/src/com/android/gallery3d/app/ControllerOverlay.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 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 android.view.View;
+
+public interface ControllerOverlay {
+
+ interface Listener {
+ void onPlayPause();
+ void onSeekStart();
+ void onSeekMove(int time);
+ void onSeekEnd(int time, int trimStartTime, int trimEndTime);
+ void onShown();
+ void onHidden();
+ void onReplay();
+ }
+
+ void setListener(Listener listener);
+
+ void setCanReplay(boolean canReplay);
+
+ /**
+ * @return The overlay view that should be added to the player.
+ */
+ View getView();
+
+ void show();
+
+ void showPlaying();
+
+ void showPaused();
+
+ void showEnded();
+
+ void showLoading();
+
+ void showErrorMessage(String message);
+
+ void setTimes(int currentTime, int totalTime,
+ int trimStartTime, int trimEndTime);
+}
diff --git a/src/com/android/gallery3d/app/DialogPicker.java b/src/com/android/gallery3d/app/DialogPicker.java
new file mode 100644
index 000000000..7ca86e5b4
--- /dev/null
+++ b/src/com/android/gallery3d/app/DialogPicker.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 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 android.content.Intent;
+import android.os.Bundle;
+
+import com.android.gallery3d.util.GalleryUtils;
+
+public class DialogPicker extends PickerActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ int typeBits = GalleryUtils.determineTypeBits(this, getIntent());
+ setTitle(GalleryUtils.getSelectionModePrompt(typeBits));
+ Intent intent = getIntent();
+ Bundle extras = intent.getExtras();
+ Bundle data = extras == null ? new Bundle() : new Bundle(extras);
+
+ data.putBoolean(Gallery.KEY_GET_CONTENT, true);
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+ getDataManager().getTopSetPath(typeBits));
+ getStateManager().startState(AlbumSetPage.class, data);
+ }
+}
diff --git a/src/com/android/gallery3d/app/EyePosition.java b/src/com/android/gallery3d/app/EyePosition.java
new file mode 100644
index 000000000..d99d97b0e
--- /dev/null
+++ b/src/com/android/gallery3d/app/EyePosition.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2010 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 android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.SystemClock;
+import android.util.FloatMath;
+import android.view.Display;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+
+public class EyePosition {
+ @SuppressWarnings("unused")
+ private static final String TAG = "EyePosition";
+
+ public interface EyePositionListener {
+ public void onEyePositionChanged(float x, float y, float z);
+ }
+
+ private static final float GYROSCOPE_THRESHOLD = 0.15f;
+ private static final float GYROSCOPE_LIMIT = 10f;
+ private static final int GYROSCOPE_SETTLE_DOWN = 15;
+ private static final float GYROSCOPE_RESTORE_FACTOR = 0.995f;
+
+ private static final float USER_ANGEL = (float) Math.toRadians(10);
+ private static final float USER_ANGEL_COS = FloatMath.cos(USER_ANGEL);
+ private static final float USER_ANGEL_SIN = FloatMath.sin(USER_ANGEL);
+ private static final float MAX_VIEW_RANGE = (float) 0.5;
+ private static final int NOT_STARTED = -1;
+
+ private static final float USER_DISTANCE_METER = 0.3f;
+
+ private Context mContext;
+ private EyePositionListener mListener;
+ private Display mDisplay;
+ // The eyes' position of the user, the origin is at the center of the
+ // device and the unit is in pixels.
+ private float mX;
+ private float mY;
+ private float mZ;
+
+ private final float mUserDistance; // in pixel
+ private final float mLimit;
+ private long mStartTime = NOT_STARTED;
+ private Sensor mSensor;
+ private PositionListener mPositionListener = new PositionListener();
+
+ private int mGyroscopeCountdown = 0;
+
+ public EyePosition(Context context, EyePositionListener listener) {
+ mContext = context;
+ mListener = listener;
+ mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER);
+ mLimit = mUserDistance * MAX_VIEW_RANGE;
+
+ WindowManager wManager = (WindowManager) mContext
+ .getSystemService(Context.WINDOW_SERVICE);
+ mDisplay = wManager.getDefaultDisplay();
+
+ // The 3D effect where the photo albums fan out in 3D based on angle
+ // of device tilt is currently disabled.
+/*
+ SensorManager sManager = (SensorManager) mContext
+ .getSystemService(Context.SENSOR_SERVICE);
+ mSensor = sManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
+ if (mSensor == null) {
+ Log.w(TAG, "no gyroscope, use accelerometer instead");
+ mSensor = sManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ }
+ if (mSensor == null) {
+ Log.w(TAG, "no sensor available");
+ }
+*/
+ }
+
+ public void resetPosition() {
+ mStartTime = NOT_STARTED;
+ mX = mY = 0;
+ mZ = -mUserDistance;
+ mListener.onEyePositionChanged(mX, mY, mZ);
+ }
+
+ /*
+ * We assume the user is at the following position
+ *
+ * /|\ user's eye
+ * | /
+ * -G(gravity) | /
+ * |_/
+ * / |/_____\ -Y (-y direction of device)
+ * user angel
+ */
+ private void onAccelerometerChanged(float gx, float gy, float gz) {
+
+ float x = gx, y = gy, z = gz;
+
+ switch (mDisplay.getRotation()) {
+ case Surface.ROTATION_90: x = -gy; y= gx; break;
+ case Surface.ROTATION_180: x = -gx; y = -gy; break;
+ case Surface.ROTATION_270: x = gy; y = -gx; break;
+ }
+
+ float temp = x * x + y * y + z * z;
+ float t = -y /temp;
+
+ float tx = t * x;
+ float ty = -1 + t * y;
+ float tz = t * z;
+
+ float length = FloatMath.sqrt(tx * tx + ty * ty + tz * tz);
+ float glength = FloatMath.sqrt(temp);
+
+ mX = Utils.clamp((x * USER_ANGEL_COS / glength
+ + tx * USER_ANGEL_SIN / length) * mUserDistance,
+ -mLimit, mLimit);
+ mY = -Utils.clamp((y * USER_ANGEL_COS / glength
+ + ty * USER_ANGEL_SIN / length) * mUserDistance,
+ -mLimit, mLimit);
+ mZ = -FloatMath.sqrt(
+ mUserDistance * mUserDistance - mX * mX - mY * mY);
+ mListener.onEyePositionChanged(mX, mY, mZ);
+ }
+
+ private void onGyroscopeChanged(float gx, float gy, float gz) {
+ long now = SystemClock.elapsedRealtime();
+ float distance = (gx > 0 ? gx : -gx) + (gy > 0 ? gy : - gy);
+ if (distance < GYROSCOPE_THRESHOLD
+ || distance > GYROSCOPE_LIMIT || mGyroscopeCountdown > 0) {
+ --mGyroscopeCountdown;
+ mStartTime = now;
+ float limit = mUserDistance / 20f;
+ if (mX > limit || mX < -limit || mY > limit || mY < -limit) {
+ mX *= GYROSCOPE_RESTORE_FACTOR;
+ mY *= GYROSCOPE_RESTORE_FACTOR;
+ mZ = (float) -Math.sqrt(
+ mUserDistance * mUserDistance - mX * mX - mY * mY);
+ mListener.onEyePositionChanged(mX, mY, mZ);
+ }
+ return;
+ }
+
+ float t = (now - mStartTime) / 1000f * mUserDistance * (-mZ);
+ mStartTime = now;
+
+ float x = -gy, y = -gx;
+ switch (mDisplay.getRotation()) {
+ case Surface.ROTATION_90: x = -gx; y= gy; break;
+ case Surface.ROTATION_180: x = gy; y = gx; break;
+ case Surface.ROTATION_270: x = gx; y = -gy; break;
+ }
+
+ mX = Utils.clamp((float) (mX + x * t / Math.hypot(mZ, mX)),
+ -mLimit, mLimit) * GYROSCOPE_RESTORE_FACTOR;
+ mY = Utils.clamp((float) (mY + y * t / Math.hypot(mZ, mY)),
+ -mLimit, mLimit) * GYROSCOPE_RESTORE_FACTOR;
+
+ mZ = -FloatMath.sqrt(
+ mUserDistance * mUserDistance - mX * mX - mY * mY);
+ mListener.onEyePositionChanged(mX, mY, mZ);
+ }
+
+ private class PositionListener implements SensorEventListener {
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ switch (event.sensor.getType()) {
+ case Sensor.TYPE_GYROSCOPE: {
+ onGyroscopeChanged(
+ event.values[0], event.values[1], event.values[2]);
+ break;
+ }
+ case Sensor.TYPE_ACCELEROMETER: {
+ onAccelerometerChanged(
+ event.values[0], event.values[1], event.values[2]);
+ }
+ }
+ }
+ }
+
+ public void pause() {
+ if (mSensor != null) {
+ SensorManager sManager = (SensorManager) mContext
+ .getSystemService(Context.SENSOR_SERVICE);
+ sManager.unregisterListener(mPositionListener);
+ }
+ }
+
+ public void resume() {
+ if (mSensor != null) {
+ SensorManager sManager = (SensorManager) mContext
+ .getSystemService(Context.SENSOR_SERVICE);
+ sManager.registerListener(mPositionListener,
+ mSensor, SensorManager.SENSOR_DELAY_GAME);
+ }
+
+ mStartTime = NOT_STARTED;
+ mGyroscopeCountdown = GYROSCOPE_SETTLE_DOWN;
+ mX = mY = 0;
+ mZ = -mUserDistance;
+ mListener.onEyePositionChanged(mX, mY, mZ);
+ }
+}
diff --git a/src/com/android/gallery3d/app/FilmstripPage.java b/src/com/android/gallery3d/app/FilmstripPage.java
new file mode 100644
index 000000000..a9726cdc9
--- /dev/null
+++ b/src/com/android/gallery3d/app/FilmstripPage.java
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+public class FilmstripPage extends PhotoPage {
+
+}
diff --git a/src/com/android/gallery3d/app/FilterUtils.java b/src/com/android/gallery3d/app/FilterUtils.java
new file mode 100644
index 000000000..bc28a9cc1
--- /dev/null
+++ b/src/com/android/gallery3d/app/FilterUtils.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2010 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.R;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+
+// This class handles filtering and clustering.
+//
+// We allow at most only one filter operation at a time (Currently it
+// doesn't make sense to use more than one). Also each clustering operation
+// can be applied at most once. In addition, there is one more constraint
+// ("fixed set constraint") described below.
+//
+// A clustered album (not including album set) and its base sets are fixed.
+// For example,
+//
+// /cluster/{base_set}/time/7
+//
+// This set and all sets inside base_set (recursively) are fixed because
+// 1. We can not change this set to use another clustering condition (like
+// changing "time" to "location").
+// 2. Neither can we change any set in the base_set.
+// The reason is in both cases the 7th set may not exist in the new clustering.
+// ---------------------
+// newPath operation: create a new path based on a source path and put an extra
+// condition on top of it:
+//
+// T = newFilterPath(S, filterType);
+// T = newClusterPath(S, clusterType);
+//
+// Similar functions can be used to replace the current condition (if there is one).
+//
+// T = switchFilterPath(S, filterType);
+// T = switchClusterPath(S, clusterType);
+//
+// For all fixed set in the path defined above, if some clusterType and
+// filterType are already used, they cannot not be used as parameter for these
+// functions. setupMenuItems() makes sure those types cannot be selected.
+//
+public class FilterUtils {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilterUtils";
+
+ public static final int CLUSTER_BY_ALBUM = 1;
+ public static final int CLUSTER_BY_TIME = 2;
+ public static final int CLUSTER_BY_LOCATION = 4;
+ public static final int CLUSTER_BY_TAG = 8;
+ public static final int CLUSTER_BY_SIZE = 16;
+ public static final int CLUSTER_BY_FACE = 32;
+
+ public static final int FILTER_IMAGE_ONLY = 1;
+ public static final int FILTER_VIDEO_ONLY = 2;
+ public static final int FILTER_ALL = 4;
+
+ // These are indices of the return values of getAppliedFilters().
+ // The _F suffix means "fixed".
+ private static final int CLUSTER_TYPE = 0;
+ private static final int FILTER_TYPE = 1;
+ private static final int CLUSTER_TYPE_F = 2;
+ private static final int FILTER_TYPE_F = 3;
+ private static final int CLUSTER_CURRENT_TYPE = 4;
+ private static final int FILTER_CURRENT_TYPE = 5;
+
+ public static void setupMenuItems(GalleryActionBar actionBar, Path path, boolean inAlbum) {
+ int[] result = new int[6];
+ getAppliedFilters(path, result);
+ int ctype = result[CLUSTER_TYPE];
+ int ftype = result[FILTER_TYPE];
+ int ftypef = result[FILTER_TYPE_F];
+ int ccurrent = result[CLUSTER_CURRENT_TYPE];
+ int fcurrent = result[FILTER_CURRENT_TYPE];
+
+ setMenuItemApplied(actionBar, CLUSTER_BY_TIME,
+ (ctype & CLUSTER_BY_TIME) != 0, (ccurrent & CLUSTER_BY_TIME) != 0);
+ setMenuItemApplied(actionBar, CLUSTER_BY_LOCATION,
+ (ctype & CLUSTER_BY_LOCATION) != 0, (ccurrent & CLUSTER_BY_LOCATION) != 0);
+ setMenuItemApplied(actionBar, CLUSTER_BY_TAG,
+ (ctype & CLUSTER_BY_TAG) != 0, (ccurrent & CLUSTER_BY_TAG) != 0);
+ setMenuItemApplied(actionBar, CLUSTER_BY_FACE,
+ (ctype & CLUSTER_BY_FACE) != 0, (ccurrent & CLUSTER_BY_FACE) != 0);
+
+ actionBar.setClusterItemVisibility(CLUSTER_BY_ALBUM, !inAlbum || ctype == 0);
+
+ setMenuItemApplied(actionBar, R.id.action_cluster_album, ctype == 0,
+ ccurrent == 0);
+
+ // A filtering is available if it's not applied, and the old filtering
+ // (if any) is not fixed.
+ setMenuItemAppliedEnabled(actionBar, R.string.show_images_only,
+ (ftype & FILTER_IMAGE_ONLY) != 0,
+ (ftype & FILTER_IMAGE_ONLY) == 0 && ftypef == 0,
+ (fcurrent & FILTER_IMAGE_ONLY) != 0);
+ setMenuItemAppliedEnabled(actionBar, R.string.show_videos_only,
+ (ftype & FILTER_VIDEO_ONLY) != 0,
+ (ftype & FILTER_VIDEO_ONLY) == 0 && ftypef == 0,
+ (fcurrent & FILTER_VIDEO_ONLY) != 0);
+ setMenuItemAppliedEnabled(actionBar, R.string.show_all,
+ ftype == 0, ftype != 0 && ftypef == 0, fcurrent == 0);
+ }
+
+ // Gets the filters applied in the path.
+ private static void getAppliedFilters(Path path, int[] result) {
+ getAppliedFilters(path, result, false);
+ }
+
+ private static void getAppliedFilters(Path path, int[] result, boolean underCluster) {
+ String[] segments = path.split();
+ // Recurse into sub media sets.
+ for (int i = 0; i < segments.length; i++) {
+ if (segments[i].startsWith("{")) {
+ String[] sets = Path.splitSequence(segments[i]);
+ for (int j = 0; j < sets.length; j++) {
+ Path sub = Path.fromString(sets[j]);
+ getAppliedFilters(sub, result, underCluster);
+ }
+ }
+ }
+
+ // update current selection
+ if (segments[0].equals("cluster")) {
+ // if this is a clustered album, set underCluster to true.
+ if (segments.length == 4) {
+ underCluster = true;
+ }
+
+ int ctype = toClusterType(segments[2]);
+ result[CLUSTER_TYPE] |= ctype;
+ result[CLUSTER_CURRENT_TYPE] = ctype;
+ if (underCluster) {
+ result[CLUSTER_TYPE_F] |= ctype;
+ }
+ }
+ }
+
+ private static int toClusterType(String s) {
+ if (s.equals("time")) {
+ return CLUSTER_BY_TIME;
+ } else if (s.equals("location")) {
+ return CLUSTER_BY_LOCATION;
+ } else if (s.equals("tag")) {
+ return CLUSTER_BY_TAG;
+ } else if (s.equals("size")) {
+ return CLUSTER_BY_SIZE;
+ } else if (s.equals("face")) {
+ return CLUSTER_BY_FACE;
+ }
+ return 0;
+ }
+
+ private static void setMenuItemApplied(
+ GalleryActionBar model, int id, boolean applied, boolean updateTitle) {
+ model.setClusterItemEnabled(id, !applied);
+ }
+
+ private static void setMenuItemAppliedEnabled(GalleryActionBar model, int id, boolean applied, boolean enabled, boolean updateTitle) {
+ model.setClusterItemEnabled(id, enabled);
+ }
+
+ // Add a specified filter to the path.
+ public static String newFilterPath(String base, int filterType) {
+ int mediaType;
+ switch (filterType) {
+ case FILTER_IMAGE_ONLY:
+ mediaType = MediaObject.MEDIA_TYPE_IMAGE;
+ break;
+ case FILTER_VIDEO_ONLY:
+ mediaType = MediaObject.MEDIA_TYPE_VIDEO;
+ break;
+ default: /* FILTER_ALL */
+ return base;
+ }
+
+ return "/filter/mediatype/" + mediaType + "/{" + base + "}";
+ }
+
+ // Add a specified clustering to the path.
+ public static String newClusterPath(String base, int clusterType) {
+ String kind;
+ switch (clusterType) {
+ case CLUSTER_BY_TIME:
+ kind = "time";
+ break;
+ case CLUSTER_BY_LOCATION:
+ kind = "location";
+ break;
+ case CLUSTER_BY_TAG:
+ kind = "tag";
+ break;
+ case CLUSTER_BY_SIZE:
+ kind = "size";
+ break;
+ case CLUSTER_BY_FACE:
+ kind = "face";
+ break;
+ default: /* CLUSTER_BY_ALBUM */
+ return base;
+ }
+
+ return "/cluster/{" + base + "}/" + kind;
+ }
+
+ // Change the topmost clustering to the specified type.
+ public static String switchClusterPath(String base, int clusterType) {
+ return newClusterPath(removeOneClusterFromPath(base), clusterType);
+ }
+
+ // Remove the topmost clustering (if any) from the path.
+ private static String removeOneClusterFromPath(String base) {
+ boolean[] done = new boolean[1];
+ return removeOneClusterFromPath(base, done);
+ }
+
+ private static String removeOneClusterFromPath(String base, boolean[] done) {
+ if (done[0]) return base;
+
+ String[] segments = Path.split(base);
+ if (segments[0].equals("cluster")) {
+ done[0] = true;
+ return Path.splitSequence(segments[1])[0];
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < segments.length; i++) {
+ sb.append("/");
+ if (segments[i].startsWith("{")) {
+ sb.append("{");
+ String[] sets = Path.splitSequence(segments[i]);
+ for (int j = 0; j < sets.length; j++) {
+ if (j > 0) {
+ sb.append(",");
+ }
+ sb.append(removeOneClusterFromPath(sets[j], done));
+ }
+ sb.append("}");
+ } else {
+ sb.append(segments[i]);
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/com/android/gallery3d/app/Gallery.java b/src/com/android/gallery3d/app/Gallery.java
new file mode 100644
index 000000000..baef56b44
--- /dev/null
+++ b/src/com/android/gallery3d/app/Gallery.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2009 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 android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.util.GalleryUtils;
+
+public final class Gallery extends AbstractGalleryActivity implements OnCancelListener {
+ public static final String EXTRA_SLIDESHOW = "slideshow";
+ public static final String EXTRA_DREAM = "dream";
+ public static final String EXTRA_CROP = "crop";
+
+ public static final String ACTION_REVIEW = "com.android.camera.action.REVIEW";
+ public static final String KEY_GET_CONTENT = "get-content";
+ public static final String KEY_GET_ALBUM = "get-album";
+ public static final String KEY_TYPE_BITS = "type-bits";
+ public static final String KEY_MEDIA_TYPES = "mediaTypes";
+ public static final String KEY_DISMISS_KEYGUARD = "dismiss-keyguard";
+
+ private static final String TAG = "Gallery";
+ private Dialog mVersionCheckDialog;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+
+ if (getIntent().getBooleanExtra(KEY_DISMISS_KEYGUARD, false)) {
+ getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ }
+
+ setContentView(R.layout.main);
+
+ if (savedInstanceState != null) {
+ getStateManager().restoreFromState(savedInstanceState);
+ } else {
+ initializeByIntent();
+ }
+ }
+
+ private void initializeByIntent() {
+ Intent intent = getIntent();
+ String action = intent.getAction();
+
+ if (Intent.ACTION_GET_CONTENT.equalsIgnoreCase(action)) {
+ startGetContent(intent);
+ } else if (Intent.ACTION_PICK.equalsIgnoreCase(action)) {
+ // We do NOT really support the PICK intent. Handle it as
+ // the GET_CONTENT. However, we need to translate the type
+ // in the intent here.
+ Log.w(TAG, "action PICK is not supported");
+ String type = Utils.ensureNotNull(intent.getType());
+ if (type.startsWith("vnd.android.cursor.dir/")) {
+ if (type.endsWith("/image")) intent.setType("image/*");
+ if (type.endsWith("/video")) intent.setType("video/*");
+ }
+ startGetContent(intent);
+ } else if (Intent.ACTION_VIEW.equalsIgnoreCase(action)
+ || ACTION_REVIEW.equalsIgnoreCase(action)){
+ startViewAction(intent);
+ } else {
+ startDefaultPage();
+ }
+ }
+
+ public void startDefaultPage() {
+ PicasaSource.showSignInReminder(this);
+ Bundle data = new Bundle();
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+ getDataManager().getTopSetPath(DataManager.INCLUDE_ALL));
+ getStateManager().startState(AlbumSetPage.class, data);
+ mVersionCheckDialog = PicasaSource.getVersionCheckDialog(this);
+ if (mVersionCheckDialog != null) {
+ mVersionCheckDialog.setOnCancelListener(this);
+ }
+ }
+
+ private void startGetContent(Intent intent) {
+ Bundle data = intent.getExtras() != null
+ ? new Bundle(intent.getExtras())
+ : new Bundle();
+ data.putBoolean(KEY_GET_CONTENT, true);
+ int typeBits = GalleryUtils.determineTypeBits(this, intent);
+ data.putInt(KEY_TYPE_BITS, typeBits);
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+ getDataManager().getTopSetPath(typeBits));
+ getStateManager().startState(AlbumSetPage.class, data);
+ }
+
+ private String getContentType(Intent intent) {
+ String type = intent.getType();
+ if (type != null) {
+ return GalleryUtils.MIME_TYPE_PANORAMA360.equals(type)
+ ? MediaItem.MIME_TYPE_JPEG : type;
+ }
+
+ Uri uri = intent.getData();
+ try {
+ return getContentResolver().getType(uri);
+ } catch (Throwable t) {
+ Log.w(TAG, "get type fail", t);
+ return null;
+ }
+ }
+
+ private void startViewAction(Intent intent) {
+ Boolean slideshow = intent.getBooleanExtra(EXTRA_SLIDESHOW, false);
+ if (slideshow) {
+ getActionBar().hide();
+ DataManager manager = getDataManager();
+ Path path = manager.findPathByUri(intent.getData(), intent.getType());
+ if (path == null || manager.getMediaObject(path)
+ instanceof MediaItem) {
+ path = Path.fromString(
+ manager.getTopSetPath(DataManager.INCLUDE_IMAGE));
+ }
+ Bundle data = new Bundle();
+ data.putString(SlideshowPage.KEY_SET_PATH, path.toString());
+ data.putBoolean(SlideshowPage.KEY_RANDOM_ORDER, true);
+ data.putBoolean(SlideshowPage.KEY_REPEAT, true);
+ if (intent.getBooleanExtra(EXTRA_DREAM, false)) {
+ data.putBoolean(SlideshowPage.KEY_DREAM, true);
+ }
+ getStateManager().startState(SlideshowPage.class, data);
+ } else {
+ Bundle data = new Bundle();
+ DataManager dm = getDataManager();
+ Uri uri = intent.getData();
+ String contentType = getContentType(intent);
+ if (contentType == null) {
+ Toast.makeText(this,
+ R.string.no_such_item, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+ if (uri == null) {
+ int typeBits = GalleryUtils.determineTypeBits(this, intent);
+ data.putInt(KEY_TYPE_BITS, typeBits);
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+ getDataManager().getTopSetPath(typeBits));
+ getStateManager().startState(AlbumSetPage.class, data);
+ } else if (contentType.startsWith(
+ ContentResolver.CURSOR_DIR_BASE_TYPE)) {
+ int mediaType = intent.getIntExtra(KEY_MEDIA_TYPES, 0);
+ if (mediaType != 0) {
+ uri = uri.buildUpon().appendQueryParameter(
+ KEY_MEDIA_TYPES, String.valueOf(mediaType))
+ .build();
+ }
+ Path setPath = dm.findPathByUri(uri, null);
+ MediaSet mediaSet = null;
+ if (setPath != null) {
+ mediaSet = (MediaSet) dm.getMediaObject(setPath);
+ }
+ if (mediaSet != null) {
+ if (mediaSet.isLeafAlbum()) {
+ data.putString(AlbumPage.KEY_MEDIA_PATH, setPath.toString());
+ data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
+ dm.getTopSetPath(DataManager.INCLUDE_ALL));
+ getStateManager().startState(AlbumPage.class, data);
+ } else {
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, setPath.toString());
+ getStateManager().startState(AlbumSetPage.class, data);
+ }
+ } else {
+ startDefaultPage();
+ }
+ } else {
+ Path itemPath = dm.findPathByUri(uri, contentType);
+ Path albumPath = dm.getDefaultSetOf(itemPath);
+
+ data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, itemPath.toString());
+
+ // TODO: Make the parameter "SingleItemOnly" public so other
+ // activities can reference it.
+ boolean singleItemOnly = (albumPath == null)
+ || intent.getBooleanExtra("SingleItemOnly", false);
+ if (!singleItemOnly) {
+ data.putString(PhotoPage.KEY_MEDIA_SET_PATH, albumPath.toString());
+ // when FLAG_ACTIVITY_NEW_TASK is set, (e.g. when intent is fired
+ // from notification), back button should behave the same as up button
+ // rather than taking users back to the home screen
+ if (intent.getBooleanExtra(PhotoPage.KEY_TREAT_BACK_AS_UP, false)
+ || ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0)) {
+ data.putBoolean(PhotoPage.KEY_TREAT_BACK_AS_UP, true);
+ }
+ }
+
+ getStateManager().startState(SinglePhotoPage.class, data);
+ }
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ Utils.assertTrue(getStateManager().getStateCount() > 0);
+ super.onResume();
+ if (mVersionCheckDialog != null) {
+ mVersionCheckDialog.show();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (mVersionCheckDialog != null) {
+ mVersionCheckDialog.dismiss();
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (dialog == mVersionCheckDialog) {
+ mVersionCheckDialog = null;
+ }
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ final boolean isTouchPad = (event.getSource()
+ & InputDevice.SOURCE_CLASS_POSITION) != 0;
+ if (isTouchPad) {
+ float maxX = event.getDevice().getMotionRange(MotionEvent.AXIS_X).getMax();
+ float maxY = event.getDevice().getMotionRange(MotionEvent.AXIS_Y).getMax();
+ View decor = getWindow().getDecorView();
+ float scaleX = decor.getWidth() / maxX;
+ float scaleY = decor.getHeight() / maxY;
+ float x = event.getX() * scaleX;
+ //x = decor.getWidth() - x; // invert x
+ float y = event.getY() * scaleY;
+ //y = decor.getHeight() - y; // invert y
+ MotionEvent touchEvent = MotionEvent.obtain(event.getDownTime(),
+ event.getEventTime(), event.getAction(), x, y, event.getMetaState());
+ return dispatchTouchEvent(touchEvent);
+ }
+ return super.onGenericMotionEvent(event);
+ }
+}
diff --git a/src/com/android/gallery3d/app/GalleryActionBar.java b/src/com/android/gallery3d/app/GalleryActionBar.java
new file mode 100644
index 000000000..588f5842a
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryActionBar.java
@@ -0,0 +1,438 @@
+/*
+ * Copyright (C) 2011 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 android.annotation.TargetApi;
+import android.app.ActionBar;
+import android.app.ActionBar.OnMenuVisibilityListener;
+import android.app.ActionBar.OnNavigationListener;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ShareActionProvider;
+import android.widget.TextView;
+import android.widget.TwoLineListItem;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.ArrayList;
+
+public class GalleryActionBar implements OnNavigationListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "GalleryActionBar";
+
+ private ClusterRunner mClusterRunner;
+ private CharSequence[] mTitles;
+ private ArrayList<Integer> mActions;
+ private Context mContext;
+ private LayoutInflater mInflater;
+ private AbstractGalleryActivity mActivity;
+ private ActionBar mActionBar;
+ private int mCurrentIndex;
+ private ClusterAdapter mAdapter = new ClusterAdapter();
+
+ private AlbumModeAdapter mAlbumModeAdapter;
+ private OnAlbumModeSelectedListener mAlbumModeListener;
+ private int mLastAlbumModeSelected;
+ private CharSequence [] mAlbumModes;
+ public static final int ALBUM_FILMSTRIP_MODE_SELECTED = 0;
+ public static final int ALBUM_GRID_MODE_SELECTED = 1;
+
+ public interface ClusterRunner {
+ public void doCluster(int id);
+ }
+
+ public interface OnAlbumModeSelectedListener {
+ public void onAlbumModeSelected(int mode);
+ }
+
+ private static class ActionItem {
+ public int action;
+ public boolean enabled;
+ public boolean visible;
+ public int spinnerTitle;
+ public int dialogTitle;
+ public int clusterBy;
+
+ public ActionItem(int action, boolean applied, boolean enabled, int title,
+ int clusterBy) {
+ this(action, applied, enabled, title, title, clusterBy);
+ }
+
+ public ActionItem(int action, boolean applied, boolean enabled, int spinnerTitle,
+ int dialogTitle, int clusterBy) {
+ this.action = action;
+ this.enabled = enabled;
+ this.spinnerTitle = spinnerTitle;
+ this.dialogTitle = dialogTitle;
+ this.clusterBy = clusterBy;
+ this.visible = true;
+ }
+ }
+
+ private static final ActionItem[] sClusterItems = new ActionItem[] {
+ new ActionItem(FilterUtils.CLUSTER_BY_ALBUM, true, false, R.string.albums,
+ R.string.group_by_album),
+ new ActionItem(FilterUtils.CLUSTER_BY_LOCATION, true, false,
+ R.string.locations, R.string.location, R.string.group_by_location),
+ new ActionItem(FilterUtils.CLUSTER_BY_TIME, true, false, R.string.times,
+ R.string.time, R.string.group_by_time),
+ new ActionItem(FilterUtils.CLUSTER_BY_FACE, true, false, R.string.people,
+ R.string.group_by_faces),
+ new ActionItem(FilterUtils.CLUSTER_BY_TAG, true, false, R.string.tags,
+ R.string.group_by_tags)
+ };
+
+ private class ClusterAdapter extends BaseAdapter {
+
+ @Override
+ public int getCount() {
+ return sClusterItems.length;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return sClusterItems[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return sClusterItems[position].action;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.action_bar_text,
+ parent, false);
+ }
+ TextView view = (TextView) convertView;
+ view.setText(sClusterItems[position].spinnerTitle);
+ return convertView;
+ }
+ }
+
+ private class AlbumModeAdapter extends BaseAdapter {
+ @Override
+ public int getCount() {
+ return mAlbumModes.length;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mAlbumModes[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.action_bar_two_line_text,
+ parent, false);
+ }
+ TwoLineListItem view = (TwoLineListItem) convertView;
+ view.getText1().setText(mActionBar.getTitle());
+ view.getText2().setText((CharSequence) getItem(position));
+ return convertView;
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.action_bar_text,
+ parent, false);
+ }
+ TextView view = (TextView) convertView;
+ view.setText((CharSequence) getItem(position));
+ return convertView;
+ }
+ }
+
+ public static String getClusterByTypeString(Context context, int type) {
+ for (ActionItem item : sClusterItems) {
+ if (item.action == type) {
+ return context.getString(item.clusterBy);
+ }
+ }
+ return null;
+ }
+
+ public GalleryActionBar(AbstractGalleryActivity activity) {
+ mActionBar = activity.getActionBar();
+ mContext = activity.getAndroidContext();
+ mActivity = activity;
+ mInflater = ((Activity) mActivity).getLayoutInflater();
+ mCurrentIndex = 0;
+ }
+
+ private void createDialogData() {
+ ArrayList<CharSequence> titles = new ArrayList<CharSequence>();
+ mActions = new ArrayList<Integer>();
+ for (ActionItem item : sClusterItems) {
+ if (item.enabled && item.visible) {
+ titles.add(mContext.getString(item.dialogTitle));
+ mActions.add(item.action);
+ }
+ }
+ mTitles = new CharSequence[titles.size()];
+ titles.toArray(mTitles);
+ }
+
+ public int getHeight() {
+ return mActionBar != null ? mActionBar.getHeight() : 0;
+ }
+
+ public void setClusterItemEnabled(int id, boolean enabled) {
+ for (ActionItem item : sClusterItems) {
+ if (item.action == id) {
+ item.enabled = enabled;
+ return;
+ }
+ }
+ }
+
+ public void setClusterItemVisibility(int id, boolean visible) {
+ for (ActionItem item : sClusterItems) {
+ if (item.action == id) {
+ item.visible = visible;
+ return;
+ }
+ }
+ }
+
+ public int getClusterTypeAction() {
+ return sClusterItems[mCurrentIndex].action;
+ }
+
+ public void enableClusterMenu(int action, ClusterRunner runner) {
+ if (mActionBar != null) {
+ // Don't set cluster runner until action bar is ready.
+ mClusterRunner = null;
+ mActionBar.setListNavigationCallbacks(mAdapter, this);
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+ setSelectedAction(action);
+ mClusterRunner = runner;
+ }
+ }
+
+ // The only use case not to hideMenu in this method is to ensure
+ // all elements disappear at the same time when exiting gallery.
+ // hideMenu should always be true in all other cases.
+ public void disableClusterMenu(boolean hideMenu) {
+ if (mActionBar != null) {
+ mClusterRunner = null;
+ if (hideMenu) {
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ }
+ }
+ }
+
+ public void onConfigurationChanged() {
+ if (mActionBar != null && mAlbumModeListener != null) {
+ OnAlbumModeSelectedListener listener = mAlbumModeListener;
+ enableAlbumModeMenu(mLastAlbumModeSelected, listener);
+ }
+ }
+
+ public void enableAlbumModeMenu(int selected, OnAlbumModeSelectedListener listener) {
+ if (mActionBar != null) {
+ if (mAlbumModeAdapter == null) {
+ // Initialize the album mode options if they haven't been already
+ Resources res = mActivity.getResources();
+ mAlbumModes = new CharSequence[] {
+ res.getString(R.string.switch_photo_filmstrip),
+ res.getString(R.string.switch_photo_grid)};
+ mAlbumModeAdapter = new AlbumModeAdapter();
+ }
+ mAlbumModeListener = null;
+ mLastAlbumModeSelected = selected;
+ mActionBar.setListNavigationCallbacks(mAlbumModeAdapter, this);
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+ mActionBar.setSelectedNavigationItem(selected);
+ mAlbumModeListener = listener;
+ }
+ }
+
+ public void disableAlbumModeMenu(boolean hideMenu) {
+ if (mActionBar != null) {
+ mAlbumModeListener = null;
+ if (hideMenu) {
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ }
+ }
+ }
+
+ public void showClusterDialog(final ClusterRunner clusterRunner) {
+ createDialogData();
+ final ArrayList<Integer> actions = mActions;
+ new AlertDialog.Builder(mContext).setTitle(R.string.group_by).setItems(
+ mTitles, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // Need to lock rendering when operations invoked by system UI (main thread) are
+ // modifying slot data used in GL thread for rendering.
+ mActivity.getGLRoot().lockRenderThread();
+ try {
+ clusterRunner.doCluster(actions.get(which).intValue());
+ } finally {
+ mActivity.getGLRoot().unlockRenderThread();
+ }
+ }
+ }).create().show();
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void setHomeButtonEnabled(boolean enabled) {
+ if (mActionBar != null) mActionBar.setHomeButtonEnabled(enabled);
+ }
+
+ public void setDisplayOptions(boolean displayHomeAsUp, boolean showTitle) {
+ if (mActionBar == null) return;
+ int options = 0;
+ if (displayHomeAsUp) options |= ActionBar.DISPLAY_HOME_AS_UP;
+ if (showTitle) options |= ActionBar.DISPLAY_SHOW_TITLE;
+
+ mActionBar.setDisplayOptions(options,
+ ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE);
+ mActionBar.setHomeButtonEnabled(displayHomeAsUp);
+ }
+
+ public void setTitle(String title) {
+ if (mActionBar != null) mActionBar.setTitle(title);
+ }
+
+ public void setTitle(int titleId) {
+ if (mActionBar != null) {
+ mActionBar.setTitle(mContext.getString(titleId));
+ }
+ }
+
+ public void setSubtitle(String title) {
+ if (mActionBar != null) mActionBar.setSubtitle(title);
+ }
+
+ public void show() {
+ if (mActionBar != null) mActionBar.show();
+ }
+
+ public void hide() {
+ if (mActionBar != null) mActionBar.hide();
+ }
+
+ public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+ if (mActionBar != null) mActionBar.addOnMenuVisibilityListener(listener);
+ }
+
+ public void removeOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+ if (mActionBar != null) mActionBar.removeOnMenuVisibilityListener(listener);
+ }
+
+ public boolean setSelectedAction(int type) {
+ if (mActionBar == null) return false;
+
+ for (int i = 0, n = sClusterItems.length; i < n; i++) {
+ ActionItem item = sClusterItems[i];
+ if (item.action == type) {
+ mActionBar.setSelectedNavigationItem(i);
+ mCurrentIndex = i;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onNavigationItemSelected(int itemPosition, long itemId) {
+ if (itemPosition != mCurrentIndex && mClusterRunner != null
+ || mAlbumModeListener != null) {
+ // Need to lock rendering when operations invoked by system UI (main thread) are
+ // modifying slot data used in GL thread for rendering.
+ mActivity.getGLRoot().lockRenderThread();
+ try {
+ if (mAlbumModeListener != null) {
+ mAlbumModeListener.onAlbumModeSelected(itemPosition);
+ } else {
+ mClusterRunner.doCluster(sClusterItems[itemPosition].action);
+ }
+ } finally {
+ mActivity.getGLRoot().unlockRenderThread();
+ }
+ }
+ return false;
+ }
+
+ private Menu mActionBarMenu;
+ private ShareActionProvider mSharePanoramaActionProvider;
+ private ShareActionProvider mShareActionProvider;
+ private Intent mSharePanoramaIntent;
+ private Intent mShareIntent;
+
+ public void createActionBarMenu(int menuRes, Menu menu) {
+ mActivity.getMenuInflater().inflate(menuRes, menu);
+ mActionBarMenu = menu;
+
+ MenuItem item = menu.findItem(R.id.action_share_panorama);
+ if (item != null) {
+ mSharePanoramaActionProvider = (ShareActionProvider)
+ item.getActionProvider();
+ mSharePanoramaActionProvider
+ .setShareHistoryFileName("panorama_share_history.xml");
+ mSharePanoramaActionProvider.setShareIntent(mSharePanoramaIntent);
+ }
+
+ item = menu.findItem(R.id.action_share);
+ if (item != null) {
+ mShareActionProvider = (ShareActionProvider)
+ item.getActionProvider();
+ mShareActionProvider
+ .setShareHistoryFileName("share_history.xml");
+ mShareActionProvider.setShareIntent(mShareIntent);
+ }
+ }
+
+ public Menu getMenu() {
+ return mActionBarMenu;
+ }
+
+ public void setShareIntents(Intent sharePanoramaIntent, Intent shareIntent,
+ ShareActionProvider.OnShareTargetSelectedListener onShareListener) {
+ mSharePanoramaIntent = sharePanoramaIntent;
+ if (mSharePanoramaActionProvider != null) {
+ mSharePanoramaActionProvider.setShareIntent(sharePanoramaIntent);
+ }
+ mShareIntent = shareIntent;
+ if (mShareActionProvider != null) {
+ mShareActionProvider.setShareIntent(shareIntent);
+ mShareActionProvider.setOnShareTargetSelectedListener(
+ onShareListener);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/GalleryApp.java b/src/com/android/gallery3d/app/GalleryApp.java
new file mode 100644
index 000000000..b56b8a82c
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryApp.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 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 android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Looper;
+
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.DownloadCache;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.util.ThreadPool;
+
+public interface GalleryApp {
+ public DataManager getDataManager();
+
+ public StitchingProgressManager getStitchingProgressManager();
+ public ImageCacheService getImageCacheService();
+ public DownloadCache getDownloadCache();
+ public ThreadPool getThreadPool();
+
+ public Context getAndroidContext();
+ public Looper getMainLooper();
+ public ContentResolver getContentResolver();
+ public Resources getResources();
+}
diff --git a/src/com/android/gallery3d/app/GalleryAppImpl.java b/src/com/android/gallery3d/app/GalleryAppImpl.java
new file mode 100644
index 000000000..2abdaa0c1
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryAppImpl.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2010 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 android.app.Application;
+import android.content.Context;
+import android.os.AsyncTask;
+
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.DownloadCache;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.gadget.WidgetUtils;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.LightCycleHelper;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.UsageStatistics;
+import com.android.photos.data.MediaCache;
+
+import java.io.File;
+
+public class GalleryAppImpl extends Application implements GalleryApp {
+
+ private static final String DOWNLOAD_FOLDER = "download";
+ private static final long DOWNLOAD_CAPACITY = 64 * 1024 * 1024; // 64M
+
+ private ImageCacheService mImageCacheService;
+ private Object mLock = new Object();
+ private DataManager mDataManager;
+ private ThreadPool mThreadPool;
+ private DownloadCache mDownloadCache;
+ private StitchingProgressManager mStitchingProgressManager;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ com.android.camera.Util.initialize(this);
+ initializeAsyncTask();
+ GalleryUtils.initialize(this);
+ WidgetUtils.initialize(this);
+ PicasaSource.initialize(this);
+ UsageStatistics.initialize(this);
+ MediaCache.initialize(this);
+
+ mStitchingProgressManager = LightCycleHelper.createStitchingManagerInstance(this);
+ if (mStitchingProgressManager != null) {
+ mStitchingProgressManager.addChangeListener(getDataManager());
+ }
+ }
+
+ @Override
+ public Context getAndroidContext() {
+ return this;
+ }
+
+ @Override
+ public synchronized DataManager getDataManager() {
+ if (mDataManager == null) {
+ mDataManager = new DataManager(this);
+ mDataManager.initializeSourceMap();
+ }
+ return mDataManager;
+ }
+
+ @Override
+ public StitchingProgressManager getStitchingProgressManager() {
+ return mStitchingProgressManager;
+ }
+
+ @Override
+ public ImageCacheService getImageCacheService() {
+ // This method may block on file I/O so a dedicated lock is needed here.
+ synchronized (mLock) {
+ if (mImageCacheService == null) {
+ mImageCacheService = new ImageCacheService(getAndroidContext());
+ }
+ return mImageCacheService;
+ }
+ }
+
+ @Override
+ public synchronized ThreadPool getThreadPool() {
+ if (mThreadPool == null) {
+ mThreadPool = new ThreadPool();
+ }
+ return mThreadPool;
+ }
+
+ @Override
+ public synchronized DownloadCache getDownloadCache() {
+ if (mDownloadCache == null) {
+ File cacheDir = new File(getExternalCacheDir(), DOWNLOAD_FOLDER);
+
+ if (!cacheDir.isDirectory()) cacheDir.mkdirs();
+
+ if (!cacheDir.isDirectory()) {
+ throw new RuntimeException(
+ "fail to create: " + cacheDir.getAbsolutePath());
+ }
+ mDownloadCache = new DownloadCache(this, cacheDir, DOWNLOAD_CAPACITY);
+ }
+ return mDownloadCache;
+ }
+
+ private void initializeAsyncTask() {
+ // AsyncTask class needs to be loaded in UI thread.
+ // So we load it here to comply the rule.
+ try {
+ Class.forName(AsyncTask.class.getName());
+ } catch (ClassNotFoundException e) {
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/GalleryContext.java b/src/com/android/gallery3d/app/GalleryContext.java
new file mode 100644
index 000000000..06f4fe4d1
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryContext.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2010 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 android.content.Context;
+import android.content.res.Resources;
+import android.os.Looper;
+
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.util.ThreadPool;
+
+public interface GalleryContext {
+ public DataManager getDataManager();
+
+ public Context getAndroidContext();
+
+ public Looper getMainLooper();
+ public Resources getResources();
+ public ThreadPool getThreadPool();
+}
diff --git a/src/com/android/gallery3d/app/LoadingListener.java b/src/com/android/gallery3d/app/LoadingListener.java
new file mode 100644
index 000000000..e94df9307
--- /dev/null
+++ b/src/com/android/gallery3d/app/LoadingListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2010 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;
+
+public interface LoadingListener {
+ public void onLoadingStarted();
+ /**
+ * Called when loading is complete or no further progress can be made.
+ *
+ * @param loadingFailed true if data source cannot provide requested data
+ */
+ public void onLoadingFinished(boolean loadingFailed);
+}
diff --git a/src/com/android/gallery3d/app/Log.java b/src/com/android/gallery3d/app/Log.java
new file mode 100644
index 000000000..07a8ea588
--- /dev/null
+++ b/src/com/android/gallery3d/app/Log.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 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;
+
+public class Log {
+ public static int v(String tag, String msg) {
+ return android.util.Log.v(tag, msg);
+ }
+ public static int v(String tag, String msg, Throwable tr) {
+ return android.util.Log.v(tag, msg, tr);
+ }
+ public static int d(String tag, String msg) {
+ return android.util.Log.d(tag, msg);
+ }
+ public static int d(String tag, String msg, Throwable tr) {
+ return android.util.Log.d(tag, msg, tr);
+ }
+ public static int i(String tag, String msg) {
+ return android.util.Log.i(tag, msg);
+ }
+ public static int i(String tag, String msg, Throwable tr) {
+ return android.util.Log.i(tag, msg, tr);
+ }
+ public static int w(String tag, String msg) {
+ return android.util.Log.w(tag, msg);
+ }
+ public static int w(String tag, String msg, Throwable tr) {
+ return android.util.Log.w(tag, msg, tr);
+ }
+ public static int w(String tag, Throwable tr) {
+ return android.util.Log.w(tag, tr);
+ }
+ public static int e(String tag, String msg) {
+ return android.util.Log.e(tag, msg);
+ }
+ public static int e(String tag, String msg, Throwable tr) {
+ return android.util.Log.e(tag, msg, tr);
+ }
+}
diff --git a/src/com/android/gallery3d/app/ManageCachePage.java b/src/com/android/gallery3d/app/ManageCachePage.java
new file mode 100644
index 000000000..4f5c35819
--- /dev/null
+++ b/src/com/android/gallery3d/app/ManageCachePage.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2010 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 android.app.Activity;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.format.Formatter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.FrameLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.ui.CacheStorageUsageInfo;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.ManageCacheDrawer;
+import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+
+public class ManageCachePage extends ActivityState implements
+ SelectionManager.SelectionListener, MenuExecutor.ProgressListener,
+ EyePosition.EyePositionListener, OnClickListener {
+ public static final String KEY_MEDIA_PATH = "media-path";
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "ManageCachePage";
+
+ private static final int DATA_CACHE_SIZE = 256;
+ private static final int MSG_REFRESH_STORAGE_INFO = 1;
+ private static final int MSG_REQUEST_LAYOUT = 2;
+ private static final int PROGRESS_BAR_MAX = 10000;
+
+ private SlotView mSlotView;
+ private MediaSet mMediaSet;
+
+ protected SelectionManager mSelectionManager;
+ protected ManageCacheDrawer mSelectionDrawer;
+ private AlbumSetDataLoader mAlbumSetDataAdapter;
+
+ private EyePosition mEyePosition;
+
+ // The eyes' position of the user, the origin is at the center of the
+ // device and the unit is in pixels.
+ private float mX;
+ private float mY;
+ private float mZ;
+
+ private int mAlbumCountToMakeAvailableOffline;
+ private View mFooterContent;
+ private CacheStorageUsageInfo mCacheStorageInfo;
+ private Future<Void> mUpdateStorageInfo;
+ private Handler mHandler;
+ private boolean mLayoutReady = false;
+
+ @Override
+ protected int getBackgroundColorId() {
+ return R.color.cache_background;
+ }
+
+ private GLView mRootPane = new GLView() {
+ private float mMatrix[] = new float[16];
+
+ @Override
+ protected void renderBackground(GLCanvas view) {
+ view.clearBuffer(getBackgroundColor());
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ // Hack: our layout depends on other components on the screen.
+ // We assume the other components will complete before we get a change
+ // to run a message in main thread.
+ if (!mLayoutReady) {
+ mHandler.sendEmptyMessage(MSG_REQUEST_LAYOUT);
+ return;
+ }
+ mLayoutReady = false;
+
+ mEyePosition.resetPosition();
+ int slotViewTop = mActivity.getGalleryActionBar().getHeight();
+ int slotViewBottom = bottom - top;
+
+ View footer = mActivity.findViewById(R.id.footer);
+ if (footer != null) {
+ int location[] = {0, 0};
+ footer.getLocationOnScreen(location);
+ slotViewBottom = location[1];
+ }
+
+ mSlotView.layout(0, slotViewTop, right - left, slotViewBottom);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ GalleryUtils.setViewPointMatrix(mMatrix,
+ getWidth() / 2 + mX, getHeight() / 2 + mY, mZ);
+ canvas.multiplyMatrix(mMatrix, 0);
+ super.render(canvas);
+ canvas.restore();
+ }
+ };
+
+ @Override
+ public void onEyePositionChanged(float x, float y, float z) {
+ mRootPane.lockRendering();
+ mX = x;
+ mY = y;
+ mZ = z;
+ mRootPane.unlockRendering();
+ mRootPane.invalidate();
+ }
+
+ private void onDown(int index) {
+ mSelectionDrawer.setPressedIndex(index);
+ }
+
+ private void onUp() {
+ mSelectionDrawer.setPressedIndex(-1);
+ }
+
+ public void onSingleTapUp(int slotIndex) {
+ MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+ if (targetSet == null) return; // Content is dirty, we shall reload soon
+
+ // ignore selection action if the target set does not support cache
+ // operation (like a local album).
+ if ((targetSet.getSupportedOperations()
+ & MediaSet.SUPPORT_CACHE) == 0) {
+ showToastForLocalAlbum();
+ return;
+ }
+
+ Path path = targetSet.getPath();
+ boolean isFullyCached =
+ (targetSet.getCacheFlag() == MediaObject.CACHE_FLAG_FULL);
+ boolean isSelected = mSelectionManager.isItemSelected(path);
+
+ if (!isFullyCached) {
+ // We only count the media sets that will be made available offline
+ // in this session.
+ if (isSelected) {
+ --mAlbumCountToMakeAvailableOffline;
+ } else {
+ ++mAlbumCountToMakeAvailableOffline;
+ }
+ }
+
+ long sizeOfTarget = targetSet.getCacheSize();
+ mCacheStorageInfo.increaseTargetCacheSize(
+ (isFullyCached ^ isSelected) ? -sizeOfTarget : sizeOfTarget);
+ refreshCacheStorageInfo();
+
+ mSelectionManager.toggle(path);
+ mSlotView.invalidate();
+ }
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ super.onCreate(data, restoreState);
+ mCacheStorageInfo = new CacheStorageUsageInfo(mActivity);
+ initializeViews();
+ initializeData(data);
+ mEyePosition = new EyePosition(mActivity.getAndroidContext(), this);
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_REFRESH_STORAGE_INFO:
+ refreshCacheStorageInfo();
+ break;
+ case MSG_REQUEST_LAYOUT: {
+ mLayoutReady = true;
+ removeMessages(MSG_REQUEST_LAYOUT);
+ mRootPane.requestLayout();
+ break;
+ }
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration config) {
+ // We use different layout resources for different configs
+ initializeFooterViews();
+ FrameLayout layout = (FrameLayout) ((Activity) mActivity).findViewById(R.id.footer);
+ if (layout.getVisibility() == View.VISIBLE) {
+ layout.removeAllViews();
+ layout.addView(mFooterContent);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mAlbumSetDataAdapter.pause();
+ mSelectionDrawer.pause();
+ mEyePosition.pause();
+
+ if (mUpdateStorageInfo != null) {
+ mUpdateStorageInfo.cancel();
+ mUpdateStorageInfo = null;
+ }
+ mHandler.removeMessages(MSG_REFRESH_STORAGE_INFO);
+
+ FrameLayout layout = (FrameLayout) ((Activity) mActivity).findViewById(R.id.footer);
+ layout.removeAllViews();
+ layout.setVisibility(View.INVISIBLE);
+ }
+
+ private Job<Void> mUpdateStorageInfoJob = new Job<Void>() {
+ @Override
+ public Void run(JobContext jc) {
+ mCacheStorageInfo.loadStorageInfo(jc);
+ if (!jc.isCancelled()) {
+ mHandler.sendEmptyMessage(MSG_REFRESH_STORAGE_INFO);
+ }
+ return null;
+ }
+ };
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ setContentPane(mRootPane);
+ mAlbumSetDataAdapter.resume();
+ mSelectionDrawer.resume();
+ mEyePosition.resume();
+ mUpdateStorageInfo = mActivity.getThreadPool().submit(mUpdateStorageInfoJob);
+ FrameLayout layout = (FrameLayout) ((Activity) mActivity).findViewById(R.id.footer);
+ layout.addView(mFooterContent);
+ layout.setVisibility(View.VISIBLE);
+ }
+
+ private void initializeData(Bundle data) {
+ String mediaPath = data.getString(ManageCachePage.KEY_MEDIA_PATH);
+ mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
+ mSelectionManager.setSourceMediaSet(mMediaSet);
+
+ // We will always be in selection mode in this page.
+ mSelectionManager.setAutoLeaveSelectionMode(false);
+ mSelectionManager.enterSelectionMode();
+
+ mAlbumSetDataAdapter = new AlbumSetDataLoader(
+ mActivity, mMediaSet, DATA_CACHE_SIZE);
+ mSelectionDrawer.setModel(mAlbumSetDataAdapter);
+ }
+
+ private void initializeViews() {
+ Activity activity = mActivity;
+
+ mSelectionManager = new SelectionManager(mActivity, true);
+ mSelectionManager.setSelectionListener(this);
+
+ Config.ManageCachePage config = Config.ManageCachePage.get(activity);
+ mSlotView = new SlotView(mActivity, config.slotViewSpec);
+ mSelectionDrawer = new ManageCacheDrawer(mActivity, mSelectionManager, mSlotView,
+ config.labelSpec, config.cachePinSize, config.cachePinMargin);
+ mSlotView.setSlotRenderer(mSelectionDrawer);
+ mSlotView.setListener(new SlotView.SimpleListener() {
+ @Override
+ public void onDown(int index) {
+ ManageCachePage.this.onDown(index);
+ }
+
+ @Override
+ public void onUp(boolean followedByLongPress) {
+ ManageCachePage.this.onUp();
+ }
+
+ @Override
+ public void onSingleTapUp(int slotIndex) {
+ ManageCachePage.this.onSingleTapUp(slotIndex);
+ }
+ });
+ mRootPane.addComponent(mSlotView);
+ initializeFooterViews();
+ }
+
+ private void initializeFooterViews() {
+ Activity activity = mActivity;
+
+ LayoutInflater inflater = activity.getLayoutInflater();
+ mFooterContent = inflater.inflate(R.layout.manage_offline_bar, null);
+
+ mFooterContent.findViewById(R.id.done).setOnClickListener(this);
+ refreshCacheStorageInfo();
+ }
+
+ @Override
+ public void onClick(View view) {
+ Utils.assertTrue(view.getId() == R.id.done);
+ GLRoot root = mActivity.getGLRoot();
+ root.lockRenderThread();
+ try {
+ ArrayList<Path> ids = mSelectionManager.getSelected(false);
+ if (ids.size() == 0) {
+ onBackPressed();
+ return;
+ }
+ showToast();
+
+ MenuExecutor menuExecutor = new MenuExecutor(mActivity, mSelectionManager);
+ menuExecutor.startAction(R.id.action_toggle_full_caching,
+ R.string.process_caching_requests, this);
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ private void showToast() {
+ if (mAlbumCountToMakeAvailableOffline > 0) {
+ Activity activity = mActivity;
+ Toast.makeText(activity, activity.getResources().getQuantityString(
+ R.plurals.make_albums_available_offline,
+ mAlbumCountToMakeAvailableOffline),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void showToastForLocalAlbum() {
+ Activity activity = mActivity;
+ Toast.makeText(activity, activity.getResources().getString(
+ R.string.try_to_set_local_album_available_offline),
+ Toast.LENGTH_SHORT).show();
+ }
+
+ private void refreshCacheStorageInfo() {
+ ProgressBar progressBar = (ProgressBar) mFooterContent.findViewById(R.id.progress);
+ TextView status = (TextView) mFooterContent.findViewById(R.id.status);
+ progressBar.setMax(PROGRESS_BAR_MAX);
+ long totalBytes = mCacheStorageInfo.getTotalBytes();
+ long usedBytes = mCacheStorageInfo.getUsedBytes();
+ long expectedBytes = mCacheStorageInfo.getExpectedUsedBytes();
+ long freeBytes = mCacheStorageInfo.getFreeBytes();
+
+ Activity activity = mActivity;
+ if (totalBytes == 0) {
+ progressBar.setProgress(0);
+ progressBar.setSecondaryProgress(0);
+
+ // TODO: get the string translated
+ String label = activity.getString(R.string.free_space_format, "-");
+ status.setText(label);
+ } else {
+ progressBar.setProgress((int) (usedBytes * PROGRESS_BAR_MAX / totalBytes));
+ progressBar.setSecondaryProgress(
+ (int) (expectedBytes * PROGRESS_BAR_MAX / totalBytes));
+ String label = activity.getString(R.string.free_space_format,
+ Formatter.formatFileSize(activity, freeBytes));
+ status.setText(label);
+ }
+ }
+
+ @Override
+ public void onProgressComplete(int result) {
+ onBackPressed();
+ }
+
+ @Override
+ public void onProgressUpdate(int index) {
+ }
+
+ @Override
+ public void onSelectionModeChange(int mode) {
+ }
+
+ @Override
+ public void onSelectionChange(Path path, boolean selected) {
+ }
+
+ @Override
+ public void onConfirmDialogDismissed(boolean confirmed) {
+ }
+
+ @Override
+ public void onConfirmDialogShown() {
+ }
+
+ @Override
+ public void onProgressStart() {
+ }
+}
diff --git a/src/com/android/gallery3d/app/MovieActivity.java b/src/com/android/gallery3d/app/MovieActivity.java
new file mode 100644
index 000000000..40edbbe4d
--- /dev/null
+++ b/src/com/android/gallery3d/app/MovieActivity.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2007 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 android.annotation.TargetApi;
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.provider.OpenableColumns;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.ShareActionProvider;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+
+/**
+ * This activity plays a video from a specified URI.
+ *
+ * The client of this activity can pass a logo bitmap in the intent (KEY_LOGO_BITMAP)
+ * to set the action bar logo so the playback process looks more seamlessly integrated with
+ * the original activity.
+ */
+public class MovieActivity extends Activity {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MovieActivity";
+ public static final String KEY_LOGO_BITMAP = "logo-bitmap";
+ public static final String KEY_TREAT_UP_AS_BACK = "treat-up-as-back";
+
+ private MoviePlayer mPlayer;
+ private boolean mFinishOnCompletion;
+ private Uri mUri;
+ private boolean mTreatUpAsBack;
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private void setSystemUiVisibility(View rootView) {
+ if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) {
+ rootView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ requestWindowFeature(Window.FEATURE_ACTION_BAR);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+
+ setContentView(R.layout.movie_view);
+ View rootView = findViewById(R.id.movie_view_root);
+
+ setSystemUiVisibility(rootView);
+
+ Intent intent = getIntent();
+ initializeActionBar(intent);
+ mFinishOnCompletion = intent.getBooleanExtra(
+ MediaStore.EXTRA_FINISH_ON_COMPLETION, true);
+ mTreatUpAsBack = intent.getBooleanExtra(KEY_TREAT_UP_AS_BACK, false);
+ mPlayer = new MoviePlayer(rootView, this, intent.getData(), savedInstanceState,
+ !mFinishOnCompletion) {
+ @Override
+ public void onCompletion() {
+ if (mFinishOnCompletion) {
+ finish();
+ }
+ }
+ };
+ if (intent.hasExtra(MediaStore.EXTRA_SCREEN_ORIENTATION)) {
+ int orientation = intent.getIntExtra(
+ MediaStore.EXTRA_SCREEN_ORIENTATION,
+ ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+ if (orientation != getRequestedOrientation()) {
+ setRequestedOrientation(orientation);
+ }
+ }
+ Window win = getWindow();
+ WindowManager.LayoutParams winParams = win.getAttributes();
+ winParams.buttonBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF;
+ winParams.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
+ win.setAttributes(winParams);
+
+ // We set the background in the theme to have the launching animation.
+ // But for the performance (and battery), we remove the background here.
+ win.setBackgroundDrawable(null);
+ }
+
+ private void setActionBarLogoFromIntent(Intent intent) {
+ Bitmap logo = intent.getParcelableExtra(KEY_LOGO_BITMAP);
+ if (logo != null) {
+ getActionBar().setLogo(
+ new BitmapDrawable(getResources(), logo));
+ }
+ }
+
+ private void initializeActionBar(Intent intent) {
+ mUri = intent.getData();
+ final ActionBar actionBar = getActionBar();
+ if (actionBar == null) {
+ return;
+ }
+ setActionBarLogoFromIntent(intent);
+ actionBar.setDisplayOptions(
+ ActionBar.DISPLAY_HOME_AS_UP,
+ ActionBar.DISPLAY_HOME_AS_UP);
+
+ String title = intent.getStringExtra(Intent.EXTRA_TITLE);
+ if (title != null) {
+ actionBar.setTitle(title);
+ } else {
+ // Displays the filename as title, reading the filename from the
+ // interface: {@link android.provider.OpenableColumns#DISPLAY_NAME}.
+ AsyncQueryHandler queryHandler =
+ new AsyncQueryHandler(getContentResolver()) {
+ @Override
+ protected void onQueryComplete(int token, Object cookie,
+ Cursor cursor) {
+ try {
+ if ((cursor != null) && cursor.moveToFirst()) {
+ String displayName = cursor.getString(0);
+
+ // Just show empty title if other apps don't set
+ // DISPLAY_NAME
+ actionBar.setTitle((displayName == null) ? "" :
+ displayName);
+ }
+ } finally {
+ Utils.closeSilently(cursor);
+ }
+ }
+ };
+ queryHandler.startQuery(0, null, mUri,
+ new String[] {OpenableColumns.DISPLAY_NAME}, null, null,
+ null);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.movie, menu);
+
+ // Document says EXTRA_STREAM should be a content: Uri
+ // So, we only share the video if it's "content:".
+ MenuItem shareItem = menu.findItem(R.id.action_share);
+ if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme())) {
+ shareItem.setVisible(true);
+ ((ShareActionProvider) shareItem.getActionProvider())
+ .setShareIntent(createShareIntent());
+ } else {
+ shareItem.setVisible(false);
+ }
+ return true;
+ }
+
+ private Intent createShareIntent() {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("video/*");
+ intent.putExtra(Intent.EXTRA_STREAM, mUri);
+ return intent;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ if (mTreatUpAsBack) {
+ finish();
+ } else {
+ startActivity(new Intent(this, Gallery.class));
+ finish();
+ }
+ return true;
+ } else if (id == R.id.action_share) {
+ startActivity(Intent.createChooser(createShareIntent(),
+ getString(R.string.share)));
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onStart() {
+ ((AudioManager) getSystemService(AUDIO_SERVICE))
+ .requestAudioFocus(null, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ super.onStart();
+ }
+
+ @Override
+ protected void onStop() {
+ ((AudioManager) getSystemService(AUDIO_SERVICE))
+ .abandonAudioFocus(null);
+ super.onStop();
+ }
+
+ @Override
+ public void onPause() {
+ mPlayer.onPause();
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ mPlayer.onResume();
+ super.onResume();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mPlayer.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onDestroy() {
+ mPlayer.onDestroy();
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return mPlayer.onKeyDown(keyCode, event)
+ || super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return mPlayer.onKeyUp(keyCode, event)
+ || super.onKeyUp(keyCode, event);
+ }
+}
diff --git a/src/com/android/gallery3d/app/MovieControllerOverlay.java b/src/com/android/gallery3d/app/MovieControllerOverlay.java
new file mode 100644
index 000000000..f01e619c6
--- /dev/null
+++ b/src/com/android/gallery3d/app/MovieControllerOverlay.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2011 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 android.content.Context;
+import android.os.Handler;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.AnimationUtils;
+import com.android.gallery3d.R;
+
+/**
+ * The playback controller for the Movie Player.
+ */
+public class MovieControllerOverlay extends CommonControllerOverlay implements
+ AnimationListener {
+
+ private boolean hidden;
+
+ private final Handler handler;
+ private final Runnable startHidingRunnable;
+ private final Animation hideAnimation;
+
+ public MovieControllerOverlay(Context context) {
+ super(context);
+
+ handler = new Handler();
+ startHidingRunnable = new Runnable() {
+ @Override
+ public void run() {
+ startHiding();
+ }
+ };
+
+ hideAnimation = AnimationUtils.loadAnimation(context, R.anim.player_out);
+ hideAnimation.setAnimationListener(this);
+
+ hide();
+ }
+
+ @Override
+ protected void createTimeBar(Context context) {
+ mTimeBar = new TimeBar(context, this);
+ }
+
+ @Override
+ public void hide() {
+ boolean wasHidden = hidden;
+ hidden = true;
+ super.hide();
+ if (mListener != null && wasHidden != hidden) {
+ mListener.onHidden();
+ }
+ }
+
+
+ @Override
+ public void show() {
+ boolean wasHidden = hidden;
+ hidden = false;
+ super.show();
+ if (mListener != null && wasHidden != hidden) {
+ mListener.onShown();
+ }
+ maybeStartHiding();
+ }
+
+ private void maybeStartHiding() {
+ cancelHiding();
+ if (mState == State.PLAYING) {
+ handler.postDelayed(startHidingRunnable, 2500);
+ }
+ }
+
+ private void startHiding() {
+ startHideAnimation(mBackground);
+ startHideAnimation(mTimeBar);
+ startHideAnimation(mPlayPauseReplayView);
+ }
+
+ private void startHideAnimation(View view) {
+ if (view.getVisibility() == View.VISIBLE) {
+ view.startAnimation(hideAnimation);
+ }
+ }
+
+ private void cancelHiding() {
+ handler.removeCallbacks(startHidingRunnable);
+ mBackground.setAnimation(null);
+ mTimeBar.setAnimation(null);
+ mPlayPauseReplayView.setAnimation(null);
+ }
+
+ @Override
+ public void onAnimationStart(Animation animation) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ hide();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (hidden) {
+ show();
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (super.onTouchEvent(event)) {
+ return true;
+ }
+
+ if (hidden) {
+ show();
+ return true;
+ }
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ cancelHiding();
+ if (mState == State.PLAYING || mState == State.PAUSED) {
+ mListener.onPlayPause();
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ maybeStartHiding();
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ protected void updateViews() {
+ if (hidden) {
+ return;
+ }
+ super.updateViews();
+ }
+
+ // TimeBar listener
+
+ @Override
+ public void onScrubbingStart() {
+ cancelHiding();
+ super.onScrubbingStart();
+ }
+
+ @Override
+ public void onScrubbingMove(int time) {
+ cancelHiding();
+ super.onScrubbingMove(time);
+ }
+
+ @Override
+ public void onScrubbingEnd(int time, int trimStartTime, int trimEndTime) {
+ maybeStartHiding();
+ super.onScrubbingEnd(time, trimStartTime, trimEndTime);
+ }
+}
diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java
new file mode 100644
index 000000000..ce9183483
--- /dev/null
+++ b/src/com/android/gallery3d/app/MoviePlayer.java
@@ -0,0 +1,525 @@
+/*
+ * Copyright (C) 2009 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 android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.VideoView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.BlobCache;
+import com.android.gallery3d.util.CacheManager;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+
+public class MoviePlayer implements
+ MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener,
+ ControllerOverlay.Listener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MoviePlayer";
+
+ private static final String KEY_VIDEO_POSITION = "video-position";
+ private static final String KEY_RESUMEABLE_TIME = "resumeable-timeout";
+
+ // These are constants in KeyEvent, appearing on API level 11.
+ private static final int KEYCODE_MEDIA_PLAY = 126;
+ private static final int KEYCODE_MEDIA_PAUSE = 127;
+
+ // Copied from MediaPlaybackService in the Music Player app.
+ private static final String SERVICECMD = "com.android.music.musicservicecommand";
+ private static final String CMDNAME = "command";
+ private static final String CMDPAUSE = "pause";
+
+ private static final long BLACK_TIMEOUT = 500;
+
+ // If we resume the acitivty with in RESUMEABLE_TIMEOUT, we will keep playing.
+ // Otherwise, we pause the player.
+ private static final long RESUMEABLE_TIMEOUT = 3 * 60 * 1000; // 3 mins
+
+ private Context mContext;
+ private final VideoView mVideoView;
+ private final View mRootView;
+ private final Bookmarker mBookmarker;
+ private final Uri mUri;
+ private final Handler mHandler = new Handler();
+ private final AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver;
+ private final MovieControllerOverlay mController;
+
+ private long mResumeableTime = Long.MAX_VALUE;
+ private int mVideoPosition = 0;
+ private boolean mHasPaused = false;
+ private int mLastSystemUiVis = 0;
+
+ // If the time bar is being dragged.
+ private boolean mDragging;
+
+ // If the time bar is visible.
+ private boolean mShowing;
+
+ private final Runnable mPlayingChecker = new Runnable() {
+ @Override
+ public void run() {
+ if (mVideoView.isPlaying()) {
+ mController.showPlaying();
+ } else {
+ mHandler.postDelayed(mPlayingChecker, 250);
+ }
+ }
+ };
+
+ private final Runnable mProgressChecker = new Runnable() {
+ @Override
+ public void run() {
+ int pos = setProgress();
+ mHandler.postDelayed(mProgressChecker, 1000 - (pos % 1000));
+ }
+ };
+
+ public MoviePlayer(View rootView, final MovieActivity movieActivity,
+ Uri videoUri, Bundle savedInstance, boolean canReplay) {
+ mContext = movieActivity.getApplicationContext();
+ mRootView = rootView;
+ mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
+ mBookmarker = new Bookmarker(movieActivity);
+ mUri = videoUri;
+
+ mController = new MovieControllerOverlay(mContext);
+ ((ViewGroup)rootView).addView(mController.getView());
+ mController.setListener(this);
+ mController.setCanReplay(canReplay);
+
+ mVideoView.setOnErrorListener(this);
+ mVideoView.setOnCompletionListener(this);
+ mVideoView.setVideoURI(mUri);
+ mVideoView.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ mController.show();
+ return true;
+ }
+ });
+ mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+ @Override
+ public void onPrepared(MediaPlayer player) {
+ if (!mVideoView.canSeekForward() || !mVideoView.canSeekBackward()) {
+ mController.setSeekable(false);
+ } else {
+ mController.setSeekable(true);
+ }
+ setProgress();
+ }
+ });
+
+ // The SurfaceView is transparent before drawing the first frame.
+ // This makes the UI flashing when open a video. (black -> old screen
+ // -> video) However, we have no way to know the timing of the first
+ // frame. So, we hide the VideoView for a while to make sure the
+ // video has been drawn on it.
+ mVideoView.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mVideoView.setVisibility(View.VISIBLE);
+ }
+ }, BLACK_TIMEOUT);
+
+ setOnSystemUiVisibilityChangeListener();
+ // Hide system UI by default
+ showSystemUi(false);
+
+ mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver();
+ mAudioBecomingNoisyReceiver.register();
+
+ Intent i = new Intent(SERVICECMD);
+ i.putExtra(CMDNAME, CMDPAUSE);
+ movieActivity.sendBroadcast(i);
+
+ if (savedInstance != null) { // this is a resumed activity
+ mVideoPosition = savedInstance.getInt(KEY_VIDEO_POSITION, 0);
+ mResumeableTime = savedInstance.getLong(KEY_RESUMEABLE_TIME, Long.MAX_VALUE);
+ mVideoView.start();
+ mVideoView.suspend();
+ mHasPaused = true;
+ } else {
+ final Integer bookmark = mBookmarker.getBookmark(mUri);
+ if (bookmark != null) {
+ showResumeDialog(movieActivity, bookmark);
+ } else {
+ startVideo();
+ }
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private void setOnSystemUiVisibilityChangeListener() {
+ if (!ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_HIDE_NAVIGATION) return;
+
+ // When the user touches the screen or uses some hard key, the framework
+ // will change system ui visibility from invisible to visible. We show
+ // the media control and enable system UI (e.g. ActionBar) to be visible at this point
+ mVideoView.setOnSystemUiVisibilityChangeListener(
+ new View.OnSystemUiVisibilityChangeListener() {
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ int diff = mLastSystemUiVis ^ visibility;
+ mLastSystemUiVis = visibility;
+ if ((diff & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
+ && (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
+ mController.show();
+ }
+ }
+ });
+ }
+
+ @SuppressWarnings("deprecation")
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private void showSystemUi(boolean visible) {
+ if (!ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) return;
+
+ int flag = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
+ if (!visible) {
+ // We used the deprecated "STATUS_BAR_HIDDEN" for unbundling
+ flag |= View.STATUS_BAR_HIDDEN | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+ }
+ mVideoView.setSystemUiVisibility(flag);
+ }
+
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putInt(KEY_VIDEO_POSITION, mVideoPosition);
+ outState.putLong(KEY_RESUMEABLE_TIME, mResumeableTime);
+ }
+
+ private void showResumeDialog(Context context, final int bookmark) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.resume_playing_title);
+ builder.setMessage(String.format(
+ context.getString(R.string.resume_playing_message),
+ GalleryUtils.formatDuration(context, bookmark / 1000)));
+ builder.setOnCancelListener(new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ onCompletion();
+ }
+ });
+ builder.setPositiveButton(
+ R.string.resume_playing_resume, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mVideoView.seekTo(bookmark);
+ startVideo();
+ }
+ });
+ builder.setNegativeButton(
+ R.string.resume_playing_restart, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ startVideo();
+ }
+ });
+ builder.show();
+ }
+
+ public void onPause() {
+ mHasPaused = true;
+ mHandler.removeCallbacksAndMessages(null);
+ mVideoPosition = mVideoView.getCurrentPosition();
+ mBookmarker.setBookmark(mUri, mVideoPosition, mVideoView.getDuration());
+ mVideoView.suspend();
+ mResumeableTime = System.currentTimeMillis() + RESUMEABLE_TIMEOUT;
+ }
+
+ public void onResume() {
+ if (mHasPaused) {
+ mVideoView.seekTo(mVideoPosition);
+ mVideoView.resume();
+
+ // If we have slept for too long, pause the play
+ if (System.currentTimeMillis() > mResumeableTime) {
+ pauseVideo();
+ }
+ }
+ mHandler.post(mProgressChecker);
+ }
+
+ public void onDestroy() {
+ mVideoView.stopPlayback();
+ mAudioBecomingNoisyReceiver.unregister();
+ }
+
+ // This updates the time bar display (if necessary). It is called every
+ // second by mProgressChecker and also from places where the time bar needs
+ // to be updated immediately.
+ private int setProgress() {
+ if (mDragging || !mShowing) {
+ return 0;
+ }
+ int position = mVideoView.getCurrentPosition();
+ int duration = mVideoView.getDuration();
+ mController.setTimes(position, duration, 0, 0);
+ return position;
+ }
+
+ private void startVideo() {
+ // For streams that we expect to be slow to start up, show a
+ // progress spinner until playback starts.
+ String scheme = mUri.getScheme();
+ if ("http".equalsIgnoreCase(scheme) || "rtsp".equalsIgnoreCase(scheme)) {
+ mController.showLoading();
+ mHandler.removeCallbacks(mPlayingChecker);
+ mHandler.postDelayed(mPlayingChecker, 250);
+ } else {
+ mController.showPlaying();
+ mController.hide();
+ }
+
+ mVideoView.start();
+ setProgress();
+ }
+
+ private void playVideo() {
+ mVideoView.start();
+ mController.showPlaying();
+ setProgress();
+ }
+
+ private void pauseVideo() {
+ mVideoView.pause();
+ mController.showPaused();
+ }
+
+ // Below are notifications from VideoView
+ @Override
+ public boolean onError(MediaPlayer player, int arg1, int arg2) {
+ mHandler.removeCallbacksAndMessages(null);
+ // VideoView will show an error dialog if we return false, so no need
+ // to show more message.
+ mController.showErrorMessage("");
+ return false;
+ }
+
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ mController.showEnded();
+ onCompletion();
+ }
+
+ public void onCompletion() {
+ }
+
+ // Below are notifications from ControllerOverlay
+ @Override
+ public void onPlayPause() {
+ if (mVideoView.isPlaying()) {
+ pauseVideo();
+ } else {
+ playVideo();
+ }
+ }
+
+ @Override
+ public void onSeekStart() {
+ mDragging = true;
+ }
+
+ @Override
+ public void onSeekMove(int time) {
+ mVideoView.seekTo(time);
+ }
+
+ @Override
+ public void onSeekEnd(int time, int start, int end) {
+ mDragging = false;
+ mVideoView.seekTo(time);
+ setProgress();
+ }
+
+ @Override
+ public void onShown() {
+ mShowing = true;
+ setProgress();
+ showSystemUi(true);
+ }
+
+ @Override
+ public void onHidden() {
+ mShowing = false;
+ showSystemUi(false);
+ }
+
+ @Override
+ public void onReplay() {
+ startVideo();
+ }
+
+ // Below are key events passed from MovieActivity.
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+
+ // Some headsets will fire off 7-10 events on a single click
+ if (event.getRepeatCount() > 0) {
+ return isMediaKey(keyCode);
+ }
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ if (mVideoView.isPlaying()) {
+ pauseVideo();
+ } else {
+ playVideo();
+ }
+ return true;
+ case KEYCODE_MEDIA_PAUSE:
+ if (mVideoView.isPlaying()) {
+ pauseVideo();
+ }
+ return true;
+ case KEYCODE_MEDIA_PLAY:
+ if (!mVideoView.isPlaying()) {
+ playVideo();
+ }
+ return true;
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ // TODO: Handle next / previous accordingly, for now we're
+ // just consuming the events.
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return isMediaKey(keyCode);
+ }
+
+ private static boolean isMediaKey(int keyCode) {
+ return keyCode == KeyEvent.KEYCODE_HEADSETHOOK
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS
+ || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE;
+ }
+
+ // We want to pause when the headset is unplugged.
+ private class AudioBecomingNoisyReceiver extends BroadcastReceiver {
+
+ public void register() {
+ mContext.registerReceiver(this,
+ new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
+ }
+
+ public void unregister() {
+ mContext.unregisterReceiver(this);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mVideoView.isPlaying()) pauseVideo();
+ }
+ }
+}
+
+class Bookmarker {
+ private static final String TAG = "Bookmarker";
+
+ private static final String BOOKMARK_CACHE_FILE = "bookmark";
+ private static final int BOOKMARK_CACHE_MAX_ENTRIES = 100;
+ private static final int BOOKMARK_CACHE_MAX_BYTES = 10 * 1024;
+ private static final int BOOKMARK_CACHE_VERSION = 1;
+
+ private static final int HALF_MINUTE = 30 * 1000;
+ private static final int TWO_MINUTES = 4 * HALF_MINUTE;
+
+ private final Context mContext;
+
+ public Bookmarker(Context context) {
+ mContext = context;
+ }
+
+ public void setBookmark(Uri uri, int bookmark, int duration) {
+ try {
+ BlobCache cache = CacheManager.getCache(mContext,
+ BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
+ BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(bos);
+ dos.writeUTF(uri.toString());
+ dos.writeInt(bookmark);
+ dos.writeInt(duration);
+ dos.flush();
+ cache.insert(uri.hashCode(), bos.toByteArray());
+ } catch (Throwable t) {
+ Log.w(TAG, "setBookmark failed", t);
+ }
+ }
+
+ public Integer getBookmark(Uri uri) {
+ try {
+ BlobCache cache = CacheManager.getCache(mContext,
+ BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
+ BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
+
+ byte[] data = cache.lookup(uri.hashCode());
+ if (data == null) return null;
+
+ DataInputStream dis = new DataInputStream(
+ new ByteArrayInputStream(data));
+
+ String uriString = DataInputStream.readUTF(dis);
+ int bookmark = dis.readInt();
+ int duration = dis.readInt();
+
+ if (!uriString.equals(uri.toString())) {
+ return null;
+ }
+
+ if ((bookmark < HALF_MINUTE) || (duration < TWO_MINUTES)
+ || (bookmark > (duration - HALF_MINUTE))) {
+ return null;
+ }
+ return Integer.valueOf(bookmark);
+ } catch (Throwable t) {
+ Log.w(TAG, "getBookmark failed", t);
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/gallery3d/app/MuteVideo.java b/src/com/android/gallery3d/app/MuteVideo.java
new file mode 100644
index 000000000..d3f3aa594
--- /dev/null
+++ b/src/com/android/gallery3d/app/MuteVideo.java
@@ -0,0 +1,104 @@
+/*
+ * 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 android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.MediaStore;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.util.SaveVideoFileInfo;
+import com.android.gallery3d.util.SaveVideoFileUtils;
+
+import java.io.IOException;
+
+public class MuteVideo {
+
+ private ProgressDialog mMuteProgress;
+
+ private String mFilePath = null;
+ private Uri mUri = null;
+ private SaveVideoFileInfo mDstFileInfo = null;
+ private Activity mActivity = null;
+ private final Handler mHandler = new Handler();
+
+ final String TIME_STAMP_NAME = "'MUTE'_yyyyMMdd_HHmmss";
+
+ public MuteVideo(String filePath, Uri uri, Activity activity) {
+ mUri = uri;
+ mFilePath = filePath;
+ mActivity = activity;
+ }
+
+ public void muteInBackground() {
+ mDstFileInfo = SaveVideoFileUtils.getDstMp4FileInfo(TIME_STAMP_NAME,
+ mActivity.getContentResolver(), mUri,
+ mActivity.getString(R.string.folder_download));
+
+ showProgressDialog();
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ VideoUtils.startMute(mFilePath, mDstFileInfo);
+ SaveVideoFileUtils.insertContent(
+ mDstFileInfo, mActivity.getContentResolver(), mUri);
+ } catch (IOException e) {
+ Toast.makeText(mActivity, mActivity.getString(R.string.video_mute_err),
+ Toast.LENGTH_SHORT).show();
+ }
+ // After muting is done, trigger the UI changed.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(mActivity.getApplicationContext(),
+ mActivity.getString(R.string.save_into,
+ mDstFileInfo.mFolderName),
+ Toast.LENGTH_SHORT)
+ .show();
+
+ if (mMuteProgress != null) {
+ mMuteProgress.dismiss();
+ mMuteProgress = null;
+
+ // Show the result only when the activity not
+ // stopped.
+ Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
+ intent.setDataAndType(Uri.fromFile(mDstFileInfo.mFile), "video/*");
+ intent.putExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, false);
+ mActivity.startActivity(intent);
+ }
+ }
+ });
+ }
+ }).start();
+ }
+
+ private void showProgressDialog() {
+ mMuteProgress = new ProgressDialog(mActivity);
+ mMuteProgress.setTitle(mActivity.getString(R.string.muting));
+ mMuteProgress.setMessage(mActivity.getString(R.string.please_wait));
+ mMuteProgress.setCancelable(false);
+ mMuteProgress.setCanceledOnTouchOutside(false);
+ mMuteProgress.show();
+ }
+}
diff --git a/src/com/android/gallery3d/app/NotificationIds.java b/src/com/android/gallery3d/app/NotificationIds.java
new file mode 100644
index 000000000..d697d854b
--- /dev/null
+++ b/src/com/android/gallery3d/app/NotificationIds.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2013 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;
+
+public class NotificationIds {
+ public static final int INGEST_NOTIFICATION_SCANNING = 10;
+ public static final int INGEST_NOTIFICATION_IMPORTING = 11;
+}
diff --git a/src/com/android/gallery3d/app/OrientationManager.java b/src/com/android/gallery3d/app/OrientationManager.java
new file mode 100644
index 000000000..f2f632c9f
--- /dev/null
+++ b/src/com/android/gallery3d/app/OrientationManager.java
@@ -0,0 +1,166 @@
+/*
+ * 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 android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.provider.Settings;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.ui.OrientationSource;
+
+public class OrientationManager implements OrientationSource {
+ private static final String TAG = "OrientationManager";
+
+ // Orientation hysteresis amount used in rounding, in degrees
+ private static final int ORIENTATION_HYSTERESIS = 5;
+
+ private Activity mActivity;
+ private MyOrientationEventListener mOrientationListener;
+ // If the framework orientation is locked.
+ private boolean mOrientationLocked = false;
+
+ // This is true if "Settings -> Display -> Rotation Lock" is checked. We
+ // don't allow the orientation to be unlocked if the value is true.
+ private boolean mRotationLockedSetting = false;
+
+ public OrientationManager(Activity activity) {
+ mActivity = activity;
+ mOrientationListener = new MyOrientationEventListener(activity);
+ }
+
+ public void resume() {
+ ContentResolver resolver = mActivity.getContentResolver();
+ mRotationLockedSetting = Settings.System.getInt(
+ resolver, Settings.System.ACCELEROMETER_ROTATION, 0) != 1;
+ mOrientationListener.enable();
+ }
+
+ public void pause() {
+ mOrientationListener.disable();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Orientation handling
+ //
+ // We can choose to lock the framework orientation or not. If we lock the
+ // framework orientation, we calculate a a compensation value according to
+ // current device orientation and send it to listeners. If we don't lock
+ // the framework orientation, we always set the compensation value to 0.
+ ////////////////////////////////////////////////////////////////////////////
+
+ // Lock the framework orientation to the current device orientation
+ public void lockOrientation() {
+ if (mOrientationLocked) return;
+ mOrientationLocked = true;
+ if (ApiHelper.HAS_ORIENTATION_LOCK) {
+ mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
+ } else {
+ mActivity.setRequestedOrientation(calculateCurrentScreenOrientation());
+ }
+ }
+
+ // Unlock the framework orientation, so it can change when the device
+ // rotates.
+ public void unlockOrientation() {
+ if (!mOrientationLocked) return;
+ mOrientationLocked = false;
+ Log.d(TAG, "unlock orientation");
+ mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
+ }
+
+ private int calculateCurrentScreenOrientation() {
+ int displayRotation = getDisplayRotation();
+ // Display rotation >= 180 means we need to use the REVERSE landscape/portrait
+ boolean standard = displayRotation < 180;
+ if (mActivity.getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_LANDSCAPE) {
+ return standard
+ ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ : ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+ } else {
+ if (displayRotation == 90 || displayRotation == 270) {
+ // If displayRotation = 90 or 270 then we are on a landscape
+ // device. On landscape devices, portrait is a 90 degree
+ // clockwise rotation from landscape, so we need
+ // to flip which portrait we pick as display rotation is counter clockwise
+ standard = !standard;
+ }
+ return standard
+ ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ : ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+ }
+ }
+
+ // This listens to the device orientation, so we can update the compensation.
+ private class MyOrientationEventListener extends OrientationEventListener {
+ public MyOrientationEventListener(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onOrientationChanged(int orientation) {
+ // We keep the last known orientation. So if the user first orient
+ // the camera then point the camera to floor or sky, we still have
+ // the correct orientation.
+ if (orientation == ORIENTATION_UNKNOWN) return;
+ orientation = roundOrientation(orientation, 0);
+ }
+ }
+
+ @Override
+ public int getDisplayRotation() {
+ return getDisplayRotation(mActivity);
+ }
+
+ @Override
+ public int getCompensation() {
+ return 0;
+ }
+
+ private static int roundOrientation(int orientation, int orientationHistory) {
+ boolean changeOrientation = false;
+ if (orientationHistory == OrientationEventListener.ORIENTATION_UNKNOWN) {
+ changeOrientation = true;
+ } else {
+ int dist = Math.abs(orientation - orientationHistory);
+ dist = Math.min(dist, 360 - dist);
+ changeOrientation = (dist >= 45 + ORIENTATION_HYSTERESIS);
+ }
+ if (changeOrientation) {
+ return ((orientation + 45) / 90 * 90) % 360;
+ }
+ return orientationHistory;
+ }
+
+ private static int getDisplayRotation(Activity activity) {
+ int rotation = activity.getWindowManager().getDefaultDisplay()
+ .getRotation();
+ switch (rotation) {
+ case Surface.ROTATION_0: return 0;
+ case Surface.ROTATION_90: return 90;
+ case Surface.ROTATION_180: return 180;
+ case Surface.ROTATION_270: return 270;
+ }
+ return 0;
+ }
+}
diff --git a/src/com/android/gallery3d/app/PackagesMonitor.java b/src/com/android/gallery3d/app/PackagesMonitor.java
new file mode 100644
index 000000000..9b2412f1b
--- /dev/null
+++ b/src/com/android/gallery3d/app/PackagesMonitor.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 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 android.app.IntentService;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.util.LightCycleHelper;
+
+public class PackagesMonitor extends BroadcastReceiver {
+ public static final String KEY_PACKAGES_VERSION = "packages-version";
+
+ public synchronized static int getPackagesVersion(Context context) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ return prefs.getInt(KEY_PACKAGES_VERSION, 1);
+ }
+
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ intent.setClass(context, AsyncService.class);
+ context.startService(intent);
+ }
+
+ public static class AsyncService extends IntentService {
+ public AsyncService() {
+ super("GalleryPackagesMonitorAsync");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ onReceiveAsync(this, intent);
+ }
+ }
+
+ // Runs in a background thread.
+ private static void onReceiveAsync(Context context, Intent intent) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+ int version = prefs.getInt(KEY_PACKAGES_VERSION, 1);
+ prefs.edit().putInt(KEY_PACKAGES_VERSION, version + 1).commit();
+
+ String action = intent.getAction();
+ String packageName = intent.getData().getSchemeSpecificPart();
+ if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+ PicasaSource.onPackageAdded(context, packageName);
+ } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+ PicasaSource.onPackageRemoved(context, packageName);
+ } else if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+ PicasaSource.onPackageChanged(context, packageName);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/PanoramaMetadataSupport.java b/src/com/android/gallery3d/app/PanoramaMetadataSupport.java
new file mode 100644
index 000000000..ba0c9e71a
--- /dev/null
+++ b/src/com/android/gallery3d/app/PanoramaMetadataSupport.java
@@ -0,0 +1,93 @@
+/*
+ * 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;
+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();
+ if (mPanoramaMetadata == null) {
+ // Error getting panorama data from file. Treat as not panorama.
+ mPanoramaMetadata = LightCycleHelper.NOT_PANORAMA;
+ }
+ for (PanoramaSupportCallback cb : mCallbacksWaiting) {
+ cb.panoramaInfoAvailable(mMediaObject, mPanoramaMetadata.mUsePanoramaViewer,
+ mPanoramaMetadata.mIsPanorama360);
+ }
+ mGetPanoMetadataTask = null;
+ mCallbacksWaiting = null;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java
new file mode 100644
index 000000000..fd3a7cf73
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -0,0 +1,1133 @@
+/*
+ * Copyright (C) 2010 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 android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.LocalMediaItem;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.TiledTexture;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.ui.TileImageViewAdapter;
+import com.android.gallery3d.ui.TiledScreenNail;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.MediaSetUtils;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+public class PhotoDataAdapter implements PhotoPage.Model {
+ @SuppressWarnings("unused")
+ private static final String TAG = "PhotoDataAdapter";
+
+ private static final int MSG_LOAD_START = 1;
+ private static final int MSG_LOAD_FINISH = 2;
+ private static final int MSG_RUN_OBJECT = 3;
+ private static final int MSG_UPDATE_IMAGE_REQUESTS = 4;
+
+ private static final int MIN_LOAD_COUNT = 16;
+ private static final int DATA_CACHE_SIZE = 256;
+ private static final int SCREEN_NAIL_MAX = PhotoView.SCREEN_NAIL_MAX;
+ private static final int IMAGE_CACHE_SIZE = 2 * SCREEN_NAIL_MAX + 1;
+
+ private static final int BIT_SCREEN_NAIL = 1;
+ private static final int BIT_FULL_IMAGE = 2;
+
+ // sImageFetchSeq is the fetching sequence for images.
+ // We want to fetch the current screennail first (offset = 0), the next
+ // screennail (offset = +1), then the previous screennail (offset = -1) etc.
+ // After all the screennail are fetched, we fetch the full images (only some
+ // of them because of we don't want to use too much memory).
+ private static ImageFetch[] sImageFetchSeq;
+
+ private static class ImageFetch {
+ int indexOffset;
+ int imageBit;
+ public ImageFetch(int offset, int bit) {
+ indexOffset = offset;
+ imageBit = bit;
+ }
+ }
+
+ static {
+ int k = 0;
+ sImageFetchSeq = new ImageFetch[1 + (IMAGE_CACHE_SIZE - 1) * 2 + 3];
+ sImageFetchSeq[k++] = new ImageFetch(0, BIT_SCREEN_NAIL);
+
+ for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) {
+ sImageFetchSeq[k++] = new ImageFetch(i, BIT_SCREEN_NAIL);
+ sImageFetchSeq[k++] = new ImageFetch(-i, BIT_SCREEN_NAIL);
+ }
+
+ sImageFetchSeq[k++] = new ImageFetch(0, BIT_FULL_IMAGE);
+ sImageFetchSeq[k++] = new ImageFetch(1, BIT_FULL_IMAGE);
+ sImageFetchSeq[k++] = new ImageFetch(-1, BIT_FULL_IMAGE);
+ }
+
+ private final TileImageViewAdapter mTileProvider = new TileImageViewAdapter();
+
+ // PhotoDataAdapter caches MediaItems (data) and ImageEntries (image).
+ //
+ // The MediaItems are stored in the mData array, which has DATA_CACHE_SIZE
+ // entries. The valid index range are [mContentStart, mContentEnd). We keep
+ // mContentEnd - mContentStart <= DATA_CACHE_SIZE, so we can use
+ // (i % DATA_CACHE_SIZE) as index to the array.
+ //
+ // The valid MediaItem window size (mContentEnd - mContentStart) may be
+ // smaller than DATA_CACHE_SIZE because we only update the window and reload
+ // the MediaItems when there are significant changes to the window position
+ // (>= MIN_LOAD_COUNT).
+ private final MediaItem mData[] = new MediaItem[DATA_CACHE_SIZE];
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ // The ImageCache is a Path-to-ImageEntry map. It only holds the
+ // ImageEntries in the range of [mActiveStart, mActiveEnd). We also keep
+ // mActiveEnd - mActiveStart <= IMAGE_CACHE_SIZE. Besides, the
+ // [mActiveStart, mActiveEnd) range must be contained within
+ // the [mContentStart, mContentEnd) range.
+ private HashMap<Path, ImageEntry> mImageCache =
+ new HashMap<Path, ImageEntry>();
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ // mCurrentIndex is the "center" image the user is viewing. The change of
+ // mCurrentIndex triggers the data loading and image loading.
+ private int mCurrentIndex;
+
+ // mChanges keeps the version number (of MediaItem) about the images. If any
+ // of the version number changes, we notify the view. This is used after a
+ // database reload or mCurrentIndex changes.
+ private final long mChanges[] = new long[IMAGE_CACHE_SIZE];
+ // mPaths keeps the corresponding Path (of MediaItem) for the images. This
+ // is used to determine the item movement.
+ private final Path mPaths[] = new Path[IMAGE_CACHE_SIZE];
+
+ private final Handler mMainHandler;
+ private final ThreadPool mThreadPool;
+
+ private final PhotoView mPhotoView;
+ private final MediaSet mSource;
+ private ReloadTask mReloadTask;
+
+ private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
+ private int mSize = 0;
+ private Path mItemPath;
+ private int mCameraIndex;
+ private boolean mIsPanorama;
+ private boolean mIsStaticCamera;
+ private boolean mIsActive;
+ private boolean mNeedFullImage;
+ private int mFocusHintDirection = FOCUS_HINT_NEXT;
+ private Path mFocusHintPath = null;
+
+ public interface DataListener extends LoadingListener {
+ public void onPhotoChanged(int index, Path item);
+ }
+
+ private DataListener mDataListener;
+
+ private final SourceListener mSourceListener = new SourceListener();
+ private final TiledTexture.Uploader mUploader;
+
+ // The path of the current viewing item will be stored in mItemPath.
+ // If mItemPath is not null, mCurrentIndex is only a hint for where we
+ // can find the item. If mItemPath is null, then we use the mCurrentIndex to
+ // find the image being viewed. cameraIndex is the index of the camera
+ // preview. If cameraIndex < 0, there is no camera preview.
+ public PhotoDataAdapter(AbstractGalleryActivity activity, PhotoView view,
+ MediaSet mediaSet, Path itemPath, int indexHint, int cameraIndex,
+ boolean isPanorama, boolean isStaticCamera) {
+ mSource = Utils.checkNotNull(mediaSet);
+ mPhotoView = Utils.checkNotNull(view);
+ mItemPath = Utils.checkNotNull(itemPath);
+ mCurrentIndex = indexHint;
+ mCameraIndex = cameraIndex;
+ mIsPanorama = isPanorama;
+ mIsStaticCamera = isStaticCamera;
+ mThreadPool = activity.getThreadPool();
+ mNeedFullImage = true;
+
+ Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION);
+
+ mUploader = new TiledTexture.Uploader(activity.getGLRoot());
+
+ mMainHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_RUN_OBJECT:
+ ((Runnable) message.obj).run();
+ return;
+ case MSG_LOAD_START: {
+ if (mDataListener != null) {
+ mDataListener.onLoadingStarted();
+ }
+ return;
+ }
+ case MSG_LOAD_FINISH: {
+ if (mDataListener != null) {
+ mDataListener.onLoadingFinished(false);
+ }
+ return;
+ }
+ case MSG_UPDATE_IMAGE_REQUESTS: {
+ updateImageRequests();
+ return;
+ }
+ default: throw new AssertionError();
+ }
+ }
+ };
+
+ updateSlidingWindow();
+ }
+
+ private MediaItem getItemInternal(int index) {
+ if (index < 0 || index >= mSize) return null;
+ if (index >= mContentStart && index < mContentEnd) {
+ return mData[index % DATA_CACHE_SIZE];
+ }
+ return null;
+ }
+
+ private long getVersion(int index) {
+ MediaItem item = getItemInternal(index);
+ if (item == null) return MediaObject.INVALID_DATA_VERSION;
+ return item.getDataVersion();
+ }
+
+ private Path getPath(int index) {
+ MediaItem item = getItemInternal(index);
+ if (item == null) return null;
+ return item.getPath();
+ }
+
+ private void fireDataChange() {
+ // First check if data actually changed.
+ boolean changed = false;
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+ long newVersion = getVersion(mCurrentIndex + i);
+ if (mChanges[i + SCREEN_NAIL_MAX] != newVersion) {
+ mChanges[i + SCREEN_NAIL_MAX] = newVersion;
+ changed = true;
+ }
+ }
+
+ if (!changed) return;
+
+ // Now calculate the fromIndex array. fromIndex represents the item
+ // movement. It records the index where the picture come from. The
+ // special value Integer.MAX_VALUE means it's a new picture.
+ final int N = IMAGE_CACHE_SIZE;
+ int fromIndex[] = new int[N];
+
+ // Remember the old path array.
+ Path oldPaths[] = new Path[N];
+ System.arraycopy(mPaths, 0, oldPaths, 0, N);
+
+ // Update the mPaths array.
+ for (int i = 0; i < N; ++i) {
+ mPaths[i] = getPath(mCurrentIndex + i - SCREEN_NAIL_MAX);
+ }
+
+ // Calculate the fromIndex array.
+ for (int i = 0; i < N; i++) {
+ Path p = mPaths[i];
+ if (p == null) {
+ fromIndex[i] = Integer.MAX_VALUE;
+ continue;
+ }
+
+ // Try to find the same path in the old array
+ int j;
+ for (j = 0; j < N; j++) {
+ if (oldPaths[j] == p) {
+ break;
+ }
+ }
+ fromIndex[i] = (j < N) ? j - SCREEN_NAIL_MAX : Integer.MAX_VALUE;
+ }
+
+ mPhotoView.notifyDataChange(fromIndex, -mCurrentIndex,
+ mSize - 1 - mCurrentIndex);
+ }
+
+ public void setDataListener(DataListener listener) {
+ mDataListener = listener;
+ }
+
+ private void updateScreenNail(Path path, Future<ScreenNail> future) {
+ ImageEntry entry = mImageCache.get(path);
+ ScreenNail screenNail = future.get();
+
+ if (entry == null || entry.screenNailTask != future) {
+ if (screenNail != null) screenNail.recycle();
+ return;
+ }
+
+ entry.screenNailTask = null;
+
+ // Combine the ScreenNails if we already have a BitmapScreenNail
+ if (entry.screenNail instanceof TiledScreenNail) {
+ TiledScreenNail original = (TiledScreenNail) entry.screenNail;
+ screenNail = original.combine(screenNail);
+ }
+
+ if (screenNail == null) {
+ entry.failToLoad = true;
+ } else {
+ entry.failToLoad = false;
+ entry.screenNail = screenNail;
+ }
+
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+ if (path == getPath(mCurrentIndex + i)) {
+ if (i == 0) updateTileProvider(entry);
+ mPhotoView.notifyImageChange(i);
+ break;
+ }
+ }
+ updateImageRequests();
+ updateScreenNailUploadQueue();
+ }
+
+ private void updateFullImage(Path path, Future<BitmapRegionDecoder> future) {
+ ImageEntry entry = mImageCache.get(path);
+ if (entry == null || entry.fullImageTask != future) {
+ BitmapRegionDecoder fullImage = future.get();
+ if (fullImage != null) fullImage.recycle();
+ return;
+ }
+
+ entry.fullImageTask = null;
+ entry.fullImage = future.get();
+ if (entry.fullImage != null) {
+ if (path == getPath(mCurrentIndex)) {
+ updateTileProvider(entry);
+ mPhotoView.notifyImageChange(0);
+ }
+ }
+ updateImageRequests();
+ }
+
+ @Override
+ public void resume() {
+ mIsActive = true;
+ TiledTexture.prepareResources();
+
+ mSource.addContentListener(mSourceListener);
+ updateImageCache();
+ updateImageRequests();
+
+ mReloadTask = new ReloadTask();
+ mReloadTask.start();
+
+ fireDataChange();
+ }
+
+ @Override
+ public void pause() {
+ mIsActive = false;
+
+ mReloadTask.terminate();
+ mReloadTask = null;
+
+ mSource.removeContentListener(mSourceListener);
+
+ for (ImageEntry entry : mImageCache.values()) {
+ if (entry.fullImageTask != null) entry.fullImageTask.cancel();
+ if (entry.screenNailTask != null) entry.screenNailTask.cancel();
+ if (entry.screenNail != null) entry.screenNail.recycle();
+ }
+ mImageCache.clear();
+ mTileProvider.clear();
+
+ mUploader.clear();
+ TiledTexture.freeResources();
+ }
+
+ private MediaItem getItem(int index) {
+ if (index < 0 || index >= mSize || !mIsActive) return null;
+ Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
+
+ if (index >= mContentStart && index < mContentEnd) {
+ return mData[index % DATA_CACHE_SIZE];
+ }
+ return null;
+ }
+
+ private void updateCurrentIndex(int index) {
+ if (mCurrentIndex == index) return;
+ mCurrentIndex = index;
+ updateSlidingWindow();
+
+ MediaItem item = mData[index % DATA_CACHE_SIZE];
+ mItemPath = item == null ? null : item.getPath();
+
+ updateImageCache();
+ updateImageRequests();
+ updateTileProvider();
+
+ if (mDataListener != null) {
+ mDataListener.onPhotoChanged(index, mItemPath);
+ }
+
+ fireDataChange();
+ }
+
+ private void uploadScreenNail(int offset) {
+ int index = mCurrentIndex + offset;
+ if (index < mActiveStart || index >= mActiveEnd) return;
+
+ MediaItem item = getItem(index);
+ if (item == null) return;
+
+ ImageEntry e = mImageCache.get(item.getPath());
+ if (e == null) return;
+
+ ScreenNail s = e.screenNail;
+ if (s instanceof TiledScreenNail) {
+ TiledTexture t = ((TiledScreenNail) s).getTexture();
+ if (t != null && !t.isReady()) mUploader.addTexture(t);
+ }
+ }
+
+ private void updateScreenNailUploadQueue() {
+ mUploader.clear();
+ uploadScreenNail(0);
+ for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) {
+ uploadScreenNail(i);
+ uploadScreenNail(-i);
+ }
+ }
+
+ @Override
+ public void moveTo(int index) {
+ updateCurrentIndex(index);
+ }
+
+ @Override
+ public ScreenNail getScreenNail(int offset) {
+ int index = mCurrentIndex + offset;
+ if (index < 0 || index >= mSize || !mIsActive) return null;
+ Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
+
+ MediaItem item = getItem(index);
+ if (item == null) return null;
+
+ ImageEntry entry = mImageCache.get(item.getPath());
+ if (entry == null) return null;
+
+ // Create a default ScreenNail if the real one is not available yet,
+ // except for camera that a black screen is better than a gray tile.
+ if (entry.screenNail == null && !isCamera(offset)) {
+ entry.screenNail = newPlaceholderScreenNail(item);
+ if (offset == 0) updateTileProvider(entry);
+ }
+
+ return entry.screenNail;
+ }
+
+ @Override
+ public void getImageSize(int offset, PhotoView.Size size) {
+ MediaItem item = getItem(mCurrentIndex + offset);
+ if (item == null) {
+ size.width = 0;
+ size.height = 0;
+ } else {
+ size.width = item.getWidth();
+ size.height = item.getHeight();
+ }
+ }
+
+ @Override
+ public int getImageRotation(int offset) {
+ MediaItem item = getItem(mCurrentIndex + offset);
+ return (item == null) ? 0 : item.getFullImageRotation();
+ }
+
+ @Override
+ public void setNeedFullImage(boolean enabled) {
+ mNeedFullImage = enabled;
+ mMainHandler.sendEmptyMessage(MSG_UPDATE_IMAGE_REQUESTS);
+ }
+
+ @Override
+ public boolean isCamera(int offset) {
+ return mCurrentIndex + offset == mCameraIndex;
+ }
+
+ @Override
+ public boolean isPanorama(int offset) {
+ return isCamera(offset) && mIsPanorama;
+ }
+
+ @Override
+ public boolean isStaticCamera(int offset) {
+ return isCamera(offset) && mIsStaticCamera;
+ }
+
+ @Override
+ public boolean isVideo(int offset) {
+ MediaItem item = getItem(mCurrentIndex + offset);
+ return (item == null)
+ ? false
+ : item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO;
+ }
+
+ @Override
+ public boolean isDeletable(int offset) {
+ MediaItem item = getItem(mCurrentIndex + offset);
+ return (item == null)
+ ? false
+ : (item.getSupportedOperations() & MediaItem.SUPPORT_DELETE) != 0;
+ }
+
+ @Override
+ public int getLoadingState(int offset) {
+ ImageEntry entry = mImageCache.get(getPath(mCurrentIndex + offset));
+ if (entry == null) return LOADING_INIT;
+ if (entry.failToLoad) return LOADING_FAIL;
+ if (entry.screenNail != null) return LOADING_COMPLETE;
+ return LOADING_INIT;
+ }
+
+ @Override
+ public ScreenNail getScreenNail() {
+ return getScreenNail(0);
+ }
+
+ @Override
+ public int getImageHeight() {
+ return mTileProvider.getImageHeight();
+ }
+
+ @Override
+ public int getImageWidth() {
+ return mTileProvider.getImageWidth();
+ }
+
+ @Override
+ public int getLevelCount() {
+ return mTileProvider.getLevelCount();
+ }
+
+ @Override
+ public Bitmap getTile(int level, int x, int y, int tileSize) {
+ return mTileProvider.getTile(level, x, y, tileSize);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return mSize == 0;
+ }
+
+ @Override
+ public int getCurrentIndex() {
+ return mCurrentIndex;
+ }
+
+ @Override
+ public MediaItem getMediaItem(int offset) {
+ int index = mCurrentIndex + offset;
+ if (index >= mContentStart && index < mContentEnd) {
+ return mData[index % DATA_CACHE_SIZE];
+ }
+ return null;
+ }
+
+ @Override
+ public void setCurrentPhoto(Path path, int indexHint) {
+ if (mItemPath == path) return;
+ mItemPath = path;
+ mCurrentIndex = indexHint;
+ updateSlidingWindow();
+ updateImageCache();
+ fireDataChange();
+
+ // We need to reload content if the path doesn't match.
+ MediaItem item = getMediaItem(0);
+ if (item != null && item.getPath() != path) {
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ @Override
+ public void setFocusHintDirection(int direction) {
+ mFocusHintDirection = direction;
+ }
+
+ @Override
+ public void setFocusHintPath(Path path) {
+ mFocusHintPath = path;
+ }
+
+ private void updateTileProvider() {
+ ImageEntry entry = mImageCache.get(getPath(mCurrentIndex));
+ if (entry == null) { // in loading
+ mTileProvider.clear();
+ } else {
+ updateTileProvider(entry);
+ }
+ }
+
+ private void updateTileProvider(ImageEntry entry) {
+ ScreenNail screenNail = entry.screenNail;
+ BitmapRegionDecoder fullImage = entry.fullImage;
+ if (screenNail != null) {
+ if (fullImage != null) {
+ mTileProvider.setScreenNail(screenNail,
+ fullImage.getWidth(), fullImage.getHeight());
+ mTileProvider.setRegionDecoder(fullImage);
+ } else {
+ int width = screenNail.getWidth();
+ int height = screenNail.getHeight();
+ mTileProvider.setScreenNail(screenNail, width, height);
+ }
+ } else {
+ mTileProvider.clear();
+ }
+ }
+
+ private void updateSlidingWindow() {
+ // 1. Update the image window
+ int start = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2,
+ 0, Math.max(0, mSize - IMAGE_CACHE_SIZE));
+ int end = Math.min(mSize, start + IMAGE_CACHE_SIZE);
+
+ if (mActiveStart == start && mActiveEnd == end) return;
+
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ // 2. Update the data window
+ start = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2,
+ 0, Math.max(0, mSize - DATA_CACHE_SIZE));
+ end = Math.min(mSize, start + DATA_CACHE_SIZE);
+ if (mContentStart > mActiveStart || mContentEnd < mActiveEnd
+ || Math.abs(start - mContentStart) > MIN_LOAD_COUNT) {
+ for (int i = mContentStart; i < mContentEnd; ++i) {
+ if (i < start || i >= end) {
+ mData[i % DATA_CACHE_SIZE] = null;
+ }
+ }
+ mContentStart = start;
+ mContentEnd = end;
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ private void updateImageRequests() {
+ if (!mIsActive) return;
+
+ int currentIndex = mCurrentIndex;
+ MediaItem item = mData[currentIndex % DATA_CACHE_SIZE];
+ if (item == null || item.getPath() != mItemPath) {
+ // current item mismatch - don't request image
+ return;
+ }
+
+ // 1. Find the most wanted request and start it (if not already started).
+ Future<?> task = null;
+ for (int i = 0; i < sImageFetchSeq.length; i++) {
+ int offset = sImageFetchSeq[i].indexOffset;
+ int bit = sImageFetchSeq[i].imageBit;
+ if (bit == BIT_FULL_IMAGE && !mNeedFullImage) continue;
+ task = startTaskIfNeeded(currentIndex + offset, bit);
+ if (task != null) break;
+ }
+
+ // 2. Cancel everything else.
+ for (ImageEntry entry : mImageCache.values()) {
+ if (entry.screenNailTask != null && entry.screenNailTask != task) {
+ entry.screenNailTask.cancel();
+ entry.screenNailTask = null;
+ entry.requestedScreenNail = MediaObject.INVALID_DATA_VERSION;
+ }
+ if (entry.fullImageTask != null && entry.fullImageTask != task) {
+ entry.fullImageTask.cancel();
+ entry.fullImageTask = null;
+ entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION;
+ }
+ }
+ }
+
+ private class ScreenNailJob implements Job<ScreenNail> {
+ private MediaItem mItem;
+
+ public ScreenNailJob(MediaItem item) {
+ mItem = item;
+ }
+
+ @Override
+ public ScreenNail run(JobContext jc) {
+ // We try to get a ScreenNail first, if it fails, we fallback to get
+ // a Bitmap and then wrap it in a BitmapScreenNail instead.
+ ScreenNail s = mItem.getScreenNail();
+ if (s != null) return s;
+
+ // If this is a temporary item, don't try to get its bitmap because
+ // it won't be available. We will get its bitmap after a data reload.
+ if (isTemporaryItem(mItem)) {
+ return newPlaceholderScreenNail(mItem);
+ }
+
+ Bitmap bitmap = mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc);
+ if (jc.isCancelled()) return null;
+ if (bitmap != null) {
+ bitmap = BitmapUtils.rotateBitmap(bitmap,
+ mItem.getRotation() - mItem.getFullImageRotation(), true);
+ }
+ return bitmap == null ? null : new TiledScreenNail(bitmap);
+ }
+ }
+
+ private class FullImageJob implements Job<BitmapRegionDecoder> {
+ private MediaItem mItem;
+
+ public FullImageJob(MediaItem item) {
+ mItem = item;
+ }
+
+ @Override
+ public BitmapRegionDecoder run(JobContext jc) {
+ if (isTemporaryItem(mItem)) {
+ return null;
+ }
+ return mItem.requestLargeImage().run(jc);
+ }
+ }
+
+ // Returns true if we think this is a temporary item created by Camera. A
+ // temporary item is an image or a video whose data is still being
+ // processed, but an incomplete entry is created first in MediaProvider, so
+ // we can display them (in grey tile) even if they are not saved to disk
+ // yet. When the image or video data is actually saved, we will get
+ // notification from MediaProvider, reload data, and show the actual image
+ // or video data.
+ private boolean isTemporaryItem(MediaItem mediaItem) {
+ // Must have camera to create a temporary item.
+ if (mCameraIndex < 0) return false;
+ // Must be an item in camera roll.
+ if (!(mediaItem instanceof LocalMediaItem)) return false;
+ LocalMediaItem item = (LocalMediaItem) mediaItem;
+ if (item.getBucketId() != MediaSetUtils.CAMERA_BUCKET_ID) return false;
+ // Must have no size, but must have width and height information
+ if (item.getSize() != 0) return false;
+ if (item.getWidth() == 0) return false;
+ if (item.getHeight() == 0) return false;
+ // Must be created in the last 10 seconds.
+ if (item.getDateInMs() - System.currentTimeMillis() > 10000) return false;
+ return true;
+ }
+
+ // Create a default ScreenNail when a ScreenNail is needed, but we don't yet
+ // have one available (because the image data is still being saved, or the
+ // Bitmap is still being loaded.
+ private ScreenNail newPlaceholderScreenNail(MediaItem item) {
+ int width = item.getWidth();
+ int height = item.getHeight();
+ return new TiledScreenNail(width, height);
+ }
+
+ // Returns the task if we started the task or the task is already started.
+ private Future<?> startTaskIfNeeded(int index, int which) {
+ if (index < mActiveStart || index >= mActiveEnd) return null;
+
+ ImageEntry entry = mImageCache.get(getPath(index));
+ if (entry == null) return null;
+ MediaItem item = mData[index % DATA_CACHE_SIZE];
+ Utils.assertTrue(item != null);
+ long version = item.getDataVersion();
+
+ if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null
+ && entry.requestedScreenNail == version) {
+ return entry.screenNailTask;
+ } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null
+ && entry.requestedFullImage == version) {
+ return entry.fullImageTask;
+ }
+
+ if (which == BIT_SCREEN_NAIL && entry.requestedScreenNail != version) {
+ entry.requestedScreenNail = version;
+ entry.screenNailTask = mThreadPool.submit(
+ new ScreenNailJob(item),
+ new ScreenNailListener(item));
+ // request screen nail
+ return entry.screenNailTask;
+ }
+ if (which == BIT_FULL_IMAGE && entry.requestedFullImage != version
+ && (item.getSupportedOperations()
+ & MediaItem.SUPPORT_FULL_IMAGE) != 0) {
+ entry.requestedFullImage = version;
+ entry.fullImageTask = mThreadPool.submit(
+ new FullImageJob(item),
+ new FullImageListener(item));
+ // request full image
+ return entry.fullImageTask;
+ }
+ return null;
+ }
+
+ private void updateImageCache() {
+ HashSet<Path> toBeRemoved = new HashSet<Path>(mImageCache.keySet());
+ for (int i = mActiveStart; i < mActiveEnd; ++i) {
+ MediaItem item = mData[i % DATA_CACHE_SIZE];
+ if (item == null) continue;
+ Path path = item.getPath();
+ ImageEntry entry = mImageCache.get(path);
+ toBeRemoved.remove(path);
+ if (entry != null) {
+ if (Math.abs(i - mCurrentIndex) > 1) {
+ if (entry.fullImageTask != null) {
+ entry.fullImageTask.cancel();
+ entry.fullImageTask = null;
+ }
+ entry.fullImage = null;
+ entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION;
+ }
+ if (entry.requestedScreenNail != item.getDataVersion()) {
+ // This ScreenNail is outdated, we want to update it if it's
+ // still a placeholder.
+ if (entry.screenNail instanceof TiledScreenNail) {
+ TiledScreenNail s = (TiledScreenNail) entry.screenNail;
+ s.updatePlaceholderSize(
+ item.getWidth(), item.getHeight());
+ }
+ }
+ } else {
+ entry = new ImageEntry();
+ mImageCache.put(path, entry);
+ }
+ }
+
+ // Clear the data and requests for ImageEntries outside the new window.
+ for (Path path : toBeRemoved) {
+ ImageEntry entry = mImageCache.remove(path);
+ if (entry.fullImageTask != null) entry.fullImageTask.cancel();
+ if (entry.screenNailTask != null) entry.screenNailTask.cancel();
+ if (entry.screenNail != null) entry.screenNail.recycle();
+ }
+
+ updateScreenNailUploadQueue();
+ }
+
+ private class FullImageListener
+ implements Runnable, FutureListener<BitmapRegionDecoder> {
+ private final Path mPath;
+ private Future<BitmapRegionDecoder> mFuture;
+
+ public FullImageListener(MediaItem item) {
+ mPath = item.getPath();
+ }
+
+ @Override
+ public void onFutureDone(Future<BitmapRegionDecoder> future) {
+ mFuture = future;
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
+ }
+
+ @Override
+ public void run() {
+ updateFullImage(mPath, mFuture);
+ }
+ }
+
+ private class ScreenNailListener
+ implements Runnable, FutureListener<ScreenNail> {
+ private final Path mPath;
+ private Future<ScreenNail> mFuture;
+
+ public ScreenNailListener(MediaItem item) {
+ mPath = item.getPath();
+ }
+
+ @Override
+ public void onFutureDone(Future<ScreenNail> future) {
+ mFuture = future;
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
+ }
+
+ @Override
+ public void run() {
+ updateScreenNail(mPath, mFuture);
+ }
+ }
+
+ private static class ImageEntry {
+ public BitmapRegionDecoder fullImage;
+ public ScreenNail screenNail;
+ public Future<ScreenNail> screenNailTask;
+ public Future<BitmapRegionDecoder> fullImageTask;
+ public long requestedScreenNail = MediaObject.INVALID_DATA_VERSION;
+ public long requestedFullImage = MediaObject.INVALID_DATA_VERSION;
+ public boolean failToLoad = false;
+ }
+
+ private class SourceListener implements ContentListener {
+ @Override
+ public void onContentDirty() {
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ private <T> T executeAndWait(Callable<T> callable) {
+ FutureTask<T> task = new FutureTask<T>(callable);
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
+ try {
+ return task.get();
+ } catch (InterruptedException e) {
+ return null;
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static class UpdateInfo {
+ public long version;
+ public boolean reloadContent;
+ public Path target;
+ public int indexHint;
+ public int contentStart;
+ public int contentEnd;
+
+ public int size;
+ public ArrayList<MediaItem> items;
+ }
+
+ private class GetUpdateInfo implements Callable<UpdateInfo> {
+
+ private boolean needContentReload() {
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ if (mData[i % DATA_CACHE_SIZE] == null) return true;
+ }
+ MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
+ return current == null || current.getPath() != mItemPath;
+ }
+
+ @Override
+ public UpdateInfo call() throws Exception {
+ // TODO: Try to load some data in first update
+ UpdateInfo info = new UpdateInfo();
+ info.version = mSourceVersion;
+ info.reloadContent = needContentReload();
+ info.target = mItemPath;
+ info.indexHint = mCurrentIndex;
+ info.contentStart = mContentStart;
+ info.contentEnd = mContentEnd;
+ info.size = mSize;
+ return info;
+ }
+ }
+
+ private class UpdateContent implements Callable<Void> {
+ UpdateInfo mUpdateInfo;
+
+ public UpdateContent(UpdateInfo updateInfo) {
+ mUpdateInfo = updateInfo;
+ }
+
+ @Override
+ public Void call() throws Exception {
+ UpdateInfo info = mUpdateInfo;
+ mSourceVersion = info.version;
+
+ if (info.size != mSize) {
+ mSize = info.size;
+ if (mContentEnd > mSize) mContentEnd = mSize;
+ if (mActiveEnd > mSize) mActiveEnd = mSize;
+ }
+
+ mCurrentIndex = info.indexHint;
+ updateSlidingWindow();
+
+ if (info.items != null) {
+ int start = Math.max(info.contentStart, mContentStart);
+ int end = Math.min(info.contentStart + info.items.size(), mContentEnd);
+ int dataIndex = start % DATA_CACHE_SIZE;
+ for (int i = start; i < end; ++i) {
+ mData[dataIndex] = info.items.get(i - info.contentStart);
+ if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0;
+ }
+ }
+
+ // update mItemPath
+ MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
+ mItemPath = current == null ? null : current.getPath();
+
+ updateImageCache();
+ updateTileProvider();
+ updateImageRequests();
+
+ if (mDataListener != null) {
+ mDataListener.onPhotoChanged(mCurrentIndex, mItemPath);
+ }
+
+ fireDataChange();
+ return null;
+ }
+ }
+
+ private class ReloadTask extends Thread {
+ private volatile boolean mActive = true;
+ private volatile boolean mDirty = true;
+
+ private boolean mIsLoading = false;
+
+ private void updateLoading(boolean loading) {
+ if (mIsLoading == loading) return;
+ mIsLoading = loading;
+ mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
+ }
+
+ @Override
+ public void run() {
+ while (mActive) {
+ synchronized (this) {
+ if (!mDirty && mActive) {
+ updateLoading(false);
+ Utils.waitWithoutInterrupt(this);
+ continue;
+ }
+ }
+ mDirty = false;
+ UpdateInfo info = executeAndWait(new GetUpdateInfo());
+ updateLoading(true);
+ long version = mSource.reload();
+ if (info.version != version) {
+ info.reloadContent = true;
+ info.size = mSource.getMediaItemCount();
+ }
+ if (!info.reloadContent) continue;
+ info.items = mSource.getMediaItem(
+ info.contentStart, info.contentEnd);
+
+ int index = MediaSet.INDEX_NOT_FOUND;
+
+ // First try to focus on the given hint path if there is one.
+ if (mFocusHintPath != null) {
+ index = findIndexOfPathInCache(info, mFocusHintPath);
+ mFocusHintPath = null;
+ }
+
+ // Otherwise try to see if the currently focused item can be found.
+ if (index == MediaSet.INDEX_NOT_FOUND) {
+ MediaItem item = findCurrentMediaItem(info);
+ if (item != null && item.getPath() == info.target) {
+ index = info.indexHint;
+ } else {
+ index = findIndexOfTarget(info);
+ }
+ }
+
+ // The image has been deleted. Focus on the next image (keep
+ // mCurrentIndex unchanged) or the previous image (decrease
+ // mCurrentIndex by 1). In page mode we want to see the next
+ // image, so we focus on the next one. In film mode we want the
+ // later images to shift left to fill the empty space, so we
+ // focus on the previous image (so it will not move). In any
+ // case the index needs to be limited to [0, mSize).
+ if (index == MediaSet.INDEX_NOT_FOUND) {
+ index = info.indexHint;
+ int focusHintDirection = mFocusHintDirection;
+ if (index == (mCameraIndex + 1)) {
+ focusHintDirection = FOCUS_HINT_NEXT;
+ }
+ if (focusHintDirection == FOCUS_HINT_PREVIOUS
+ && index > 0) {
+ index--;
+ }
+ }
+
+ // Don't change index if mSize == 0
+ if (mSize > 0) {
+ if (index >= mSize) index = mSize - 1;
+ }
+
+ info.indexHint = index;
+
+ executeAndWait(new UpdateContent(info));
+ }
+ }
+
+ public synchronized void notifyDirty() {
+ mDirty = true;
+ notifyAll();
+ }
+
+ public synchronized void terminate() {
+ mActive = false;
+ notifyAll();
+ }
+
+ private MediaItem findCurrentMediaItem(UpdateInfo info) {
+ ArrayList<MediaItem> items = info.items;
+ int index = info.indexHint - info.contentStart;
+ return index < 0 || index >= items.size() ? null : items.get(index);
+ }
+
+ private int findIndexOfTarget(UpdateInfo info) {
+ if (info.target == null) return info.indexHint;
+ ArrayList<MediaItem> items = info.items;
+
+ // First, try to find the item in the data just loaded
+ if (items != null) {
+ int i = findIndexOfPathInCache(info, info.target);
+ if (i != MediaSet.INDEX_NOT_FOUND) return i;
+ }
+
+ // Not found, find it in mSource.
+ return mSource.getIndexOfItem(info.target, info.indexHint);
+ }
+
+ private int findIndexOfPathInCache(UpdateInfo info, Path path) {
+ ArrayList<MediaItem> items = info.items;
+ for (int i = 0, n = items.size(); i < n; ++i) {
+ MediaItem item = items.get(i);
+ if (item != null && item.getPath() == path) {
+ return i + info.contentStart;
+ }
+ }
+ return MediaSet.INDEX_NOT_FOUND;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
new file mode 100644
index 000000000..7a71e9109
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -0,0 +1,1571 @@
+/*
+ * Copyright (C) 2010 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 android.annotation.TargetApi;
+import android.app.ActionBar.OnMenuVisibilityListener;
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcAdapter.CreateBeamUrisCallback;
+import android.nfc.NfcEvent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.RelativeLayout;
+import android.widget.ShareActionProvider;
+import android.widget.Toast;
+
+import com.android.camera.CameraActivity;
+import com.android.camera.ProxyLauncher;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.ComboAlbum;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.FilterDeleteSet;
+import com.android.gallery3d.data.FilterSource;
+import com.android.gallery3d.data.LocalImage;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.data.SecureAlbum;
+import com.android.gallery3d.data.SecureSource;
+import com.android.gallery3d.data.SnailAlbum;
+import com.android.gallery3d.data.SnailItem;
+import com.android.gallery3d.data.SnailSource;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.ui.DetailsHelper;
+import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.UsageStatistics;
+
+public abstract class PhotoPage extends ActivityState implements
+ PhotoView.Listener, AppBridge.Server, ShareActionProvider.OnShareTargetSelectedListener,
+ PhotoPageBottomControls.Delegate, GalleryActionBar.OnAlbumModeSelectedListener {
+ private static final String TAG = "PhotoPage";
+
+ private static final int MSG_HIDE_BARS = 1;
+ private static final int MSG_ON_FULL_SCREEN_CHANGED = 4;
+ private static final int MSG_UPDATE_ACTION_BAR = 5;
+ private static final int MSG_UNFREEZE_GLROOT = 6;
+ private static final int MSG_WANT_BARS = 7;
+ private static final int MSG_REFRESH_BOTTOM_CONTROLS = 8;
+ private static final int MSG_ON_CAMERA_CENTER = 9;
+ private static final int MSG_ON_PICTURE_CENTER = 10;
+ private static final int MSG_REFRESH_IMAGE = 11;
+ 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;
+
+ private static final int REQUEST_SLIDESHOW = 1;
+ private static final int REQUEST_CROP = 2;
+ private static final int REQUEST_CROP_PICASA = 3;
+ private static final int REQUEST_EDIT = 4;
+ private static final int REQUEST_PLAY_VIDEO = 5;
+ private static final int REQUEST_TRIM = 6;
+
+ public static final String KEY_MEDIA_SET_PATH = "media-set-path";
+ public static final String KEY_MEDIA_ITEM_PATH = "media-item-path";
+ public static final String KEY_INDEX_HINT = "index-hint";
+ public static final String KEY_OPEN_ANIMATION_RECT = "open-animation-rect";
+ public static final String KEY_APP_BRIDGE = "app-bridge";
+ public static final String KEY_TREAT_BACK_AS_UP = "treat-back-as-up";
+ public static final String KEY_START_IN_FILMSTRIP = "start-in-filmstrip";
+ public static final String KEY_RETURN_INDEX_HINT = "return-index-hint";
+ public static final String KEY_SHOW_WHEN_LOCKED = "show_when_locked";
+ public static final String KEY_IN_CAMERA_ROLL = "in_camera_roll";
+
+ public static final String KEY_ALBUMPAGE_TRANSITION = "albumpage-transition";
+ public static final int MSG_ALBUMPAGE_NONE = 0;
+ public static final int MSG_ALBUMPAGE_STARTED = 1;
+ public static final int MSG_ALBUMPAGE_RESUMED = 2;
+ public static final int MSG_ALBUMPAGE_PICKED = 4;
+
+ public static final String ACTION_NEXTGEN_EDIT = "action_nextgen_edit";
+ public static final String ACTION_SIMPLE_EDIT = "action_simple_edit";
+
+ private GalleryApp mApplication;
+ private SelectionManager mSelectionManager;
+
+ private PhotoView mPhotoView;
+ private PhotoPage.Model mModel;
+ private DetailsHelper mDetailsHelper;
+ private boolean mShowDetails;
+
+ // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied.
+ // E.g., viewing a photo in gmail attachment
+ private FilterDeleteSet mMediaSet;
+
+ // The mediaset used by camera launched from secure lock screen.
+ private SecureAlbum mSecureAlbum;
+
+ private int mCurrentIndex = 0;
+ private Handler mHandler;
+ private boolean mShowBars = true;
+ private volatile boolean mActionBarAllowed = true;
+ private GalleryActionBar mActionBar;
+ private boolean mIsMenuVisible;
+ private boolean mHaveImageEditor;
+ private PhotoPageBottomControls mBottomControls;
+ private PhotoPageProgressBar mProgressBar;
+ private MediaItem mCurrentPhoto = null;
+ private MenuExecutor mMenuExecutor;
+ private boolean mIsActive;
+ private boolean mShowSpinner;
+ private String mSetPathString;
+ // This is the original mSetPathString before adding the camera preview item.
+ private String mOriginalSetPathString;
+ private AppBridge mAppBridge;
+ private SnailItem mScreenNailItem;
+ private SnailAlbum mScreenNailSet;
+ private OrientationManager mOrientationManager;
+ private boolean mTreatBackAsUp;
+ private boolean mStartInFilmstrip;
+ private boolean mHasCameraScreennailOrPlaceholder = false;
+ private boolean mRecenterCameraOnResume = true;
+
+ // These are only valid after the panorama callback
+ private boolean mIsPanorama;
+ private boolean mIsPanorama360;
+
+ private long mCameraSwitchCutoff = 0;
+ private boolean mSkipUpdateCurrentPhoto = false;
+ private static final long CAMERA_SWITCH_CUTOFF_THRESHOLD_MS = 300;
+
+ private static final long DEFERRED_UPDATE_MS = 250;
+ private boolean mDeferredUpdateWaiting = false;
+ private long mDeferUpdateUntil = Long.MAX_VALUE;
+
+ // The item that is deleted (but it can still be undeleted before commiting)
+ private Path mDeletePath;
+ private boolean mDeleteIsFocus; // whether the deleted item was in focus
+
+ private Uri[] mNfcPushUris = new Uri[1];
+
+ private final MyMenuVisibilityListener mMenuVisibilityListener =
+ new MyMenuVisibilityListener();
+ private UpdateProgressListener mProgressListener;
+
+ 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, isPanorama360 ? 1 : 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();
+ public void pause();
+ public boolean isEmpty();
+ public void setCurrentPhoto(Path path, int indexHint);
+ }
+
+ private class MyMenuVisibilityListener implements OnMenuVisibilityListener {
+ @Override
+ public void onMenuVisibilityChanged(boolean isVisible) {
+ mIsMenuVisible = isVisible;
+ refreshHidingMessage();
+ }
+ }
+
+ private class UpdateProgressListener implements StitchingChangeListener {
+
+ @Override
+ public void onStitchingResult(Uri uri) {
+ sendUpdate(uri, MSG_REFRESH_IMAGE);
+ }
+
+ @Override
+ public void onStitchingQueued(Uri uri) {
+ sendUpdate(uri, MSG_UPDATE_PROGRESS);
+ }
+
+ @Override
+ public void onStitchingProgress(Uri uri, final int progress) {
+ sendUpdate(uri, MSG_UPDATE_PROGRESS);
+ }
+
+ private void sendUpdate(Uri uri, int message) {
+ MediaObject currentPhoto = mCurrentPhoto;
+ boolean isCurrentPhoto = currentPhoto instanceof LocalImage
+ && currentPhoto.getContentUri().equals(uri);
+ if (isCurrentPhoto) {
+ mHandler.sendEmptyMessage(message);
+ }
+ }
+ };
+
+ @Override
+ protected int getBackgroundColorId() {
+ return R.color.photo_background;
+ }
+
+ private final GLView mRootPane = new GLView() {
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ mPhotoView.layout(0, 0, right - left, bottom - top);
+ if (mShowDetails) {
+ mDetailsHelper.layout(left, mActionBar.getHeight(), right, bottom);
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ super.onCreate(data, restoreState);
+ mActionBar = mActivity.getGalleryActionBar();
+ mSelectionManager = new SelectionManager(mActivity, false);
+ mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager);
+
+ mPhotoView = new PhotoView(mActivity);
+ mPhotoView.setListener(this);
+ mRootPane.addComponent(mPhotoView);
+ mApplication = (GalleryApp) ((Activity) mActivity).getApplication();
+ mOrientationManager = mActivity.getOrientationManager();
+ mActivity.getGLRoot().setOrientationSource(mOrientationManager);
+
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_HIDE_BARS: {
+ hideBars();
+ break;
+ }
+ case MSG_REFRESH_BOTTOM_CONTROLS: {
+ if (mCurrentPhoto == message.obj && mBottomControls != null) {
+ mIsPanorama = message.arg1 == 1;
+ mIsPanorama360 = message.arg2 == 1;
+ mBottomControls.refresh();
+ }
+ break;
+ }
+ case MSG_ON_FULL_SCREEN_CHANGED: {
+ if (mAppBridge != null) {
+ mAppBridge.onFullScreenChanged(message.arg1 == 1);
+ }
+ break;
+ }
+ case MSG_UPDATE_ACTION_BAR: {
+ updateBars();
+ break;
+ }
+ case MSG_WANT_BARS: {
+ wantBars();
+ break;
+ }
+ case MSG_UNFREEZE_GLROOT: {
+ mActivity.getGLRoot().unfreeze();
+ break;
+ }
+ case MSG_UPDATE_DEFERRED: {
+ long nextUpdate = mDeferUpdateUntil - SystemClock.uptimeMillis();
+ if (nextUpdate <= 0) {
+ mDeferredUpdateWaiting = false;
+ updateUIForCurrentPhoto();
+ } else {
+ mHandler.sendEmptyMessageDelayed(MSG_UPDATE_DEFERRED, nextUpdate);
+ }
+ break;
+ }
+ case MSG_ON_CAMERA_CENTER: {
+ mSkipUpdateCurrentPhoto = false;
+ boolean stayedOnCamera = false;
+ if (!mPhotoView.getFilmMode()) {
+ stayedOnCamera = true;
+ } else if (SystemClock.uptimeMillis() < mCameraSwitchCutoff &&
+ mMediaSet.getMediaItemCount() > 1) {
+ mPhotoView.switchToImage(1);
+ } else {
+ if (mAppBridge != null) mPhotoView.setFilmMode(false);
+ stayedOnCamera = true;
+ }
+
+ if (stayedOnCamera) {
+ if (mAppBridge == null && mMediaSet.getTotalMediaItemCount() > 1) {
+ launchCamera();
+ /* We got here by swiping from photo 1 to the
+ placeholder, so make it be the thing that
+ is in focus when the user presses back from
+ the camera app */
+ mPhotoView.switchToImage(1);
+ } else {
+ updateBars();
+ updateCurrentPhoto(mModel.getMediaItem(0));
+ }
+ }
+ break;
+ }
+ case MSG_ON_PICTURE_CENTER: {
+ if (!mPhotoView.getFilmMode() && mCurrentPhoto != null
+ && (mCurrentPhoto.getSupportedOperations() & MediaObject.SUPPORT_ACTION) != 0) {
+ mPhotoView.setFilmMode(true);
+ }
+ break;
+ }
+ case MSG_REFRESH_IMAGE: {
+ final MediaItem photo = mCurrentPhoto;
+ mCurrentPhoto = null;
+ updateCurrentPhoto(photo);
+ break;
+ }
+ case MSG_UPDATE_PHOTO_UI: {
+ updateUIForCurrentPhoto();
+ break;
+ }
+ case MSG_UPDATE_PROGRESS: {
+ 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, PhotoPage.this);
+ 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);
+ }
+ }
+ };
+
+ mSetPathString = data.getString(KEY_MEDIA_SET_PATH);
+ mOriginalSetPathString = mSetPathString;
+ setupNfcBeamPush();
+ String itemPathString = data.getString(KEY_MEDIA_ITEM_PATH);
+ Path itemPath = itemPathString != null ?
+ Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH)) :
+ null;
+ mTreatBackAsUp = data.getBoolean(KEY_TREAT_BACK_AS_UP, false);
+ mStartInFilmstrip = data.getBoolean(KEY_START_IN_FILMSTRIP, false);
+ boolean inCameraRoll = data.getBoolean(KEY_IN_CAMERA_ROLL, false);
+ mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
+ if (mSetPathString != null) {
+ mShowSpinner = true;
+ mAppBridge = (AppBridge) data.getParcelable(KEY_APP_BRIDGE);
+ if (mAppBridge != null) {
+ mShowBars = false;
+ mHasCameraScreennailOrPlaceholder = true;
+ mAppBridge.setServer(this);
+
+ // Get the ScreenNail from AppBridge and register it.
+ int id = SnailSource.newId();
+ Path screenNailSetPath = SnailSource.getSetPath(id);
+ Path screenNailItemPath = SnailSource.getItemPath(id);
+ mScreenNailSet = (SnailAlbum) mActivity.getDataManager()
+ .getMediaObject(screenNailSetPath);
+ mScreenNailItem = (SnailItem) mActivity.getDataManager()
+ .getMediaObject(screenNailItemPath);
+ mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
+
+ if (data.getBoolean(KEY_SHOW_WHEN_LOCKED, false)) {
+ // Set the flag to be on top of the lock screen.
+ mFlags |= FLAG_SHOW_WHEN_LOCKED;
+ }
+
+ // Don't display "empty album" action item for capture intents.
+ if (!mSetPathString.equals("/local/all/0")) {
+ // Check if the path is a secure album.
+ if (SecureSource.isSecurePath(mSetPathString)) {
+ mSecureAlbum = (SecureAlbum) mActivity.getDataManager()
+ .getMediaSet(mSetPathString);
+ mShowSpinner = false;
+ }
+ mSetPathString = "/filter/empty/{"+mSetPathString+"}";
+ }
+
+ // Combine the original MediaSet with the one for ScreenNail
+ // from AppBridge.
+ mSetPathString = "/combo/item/{" + screenNailSetPath +
+ "," + mSetPathString + "}";
+
+ // Start from the screen nail.
+ itemPath = screenNailItemPath;
+ } else if (inCameraRoll && GalleryUtils.isCameraAvailable(mActivity)) {
+ mSetPathString = "/combo/item/{" + FilterSource.FILTER_CAMERA_SHORTCUT +
+ "," + mSetPathString + "}";
+ mCurrentIndex++;
+ mHasCameraScreennailOrPlaceholder = true;
+ }
+
+ MediaSet originalSet = mActivity.getDataManager()
+ .getMediaSet(mSetPathString);
+ if (mHasCameraScreennailOrPlaceholder && originalSet instanceof ComboAlbum) {
+ // Use the name of the camera album rather than the default
+ // ComboAlbum behavior
+ ((ComboAlbum) originalSet).useNameOfChild(1);
+ }
+ mSelectionManager.setSourceMediaSet(originalSet);
+ mSetPathString = "/filter/delete/{" + mSetPathString + "}";
+ mMediaSet = (FilterDeleteSet) mActivity.getDataManager()
+ .getMediaSet(mSetPathString);
+ if (mMediaSet == null) {
+ Log.w(TAG, "failed to restore " + mSetPathString);
+ }
+ if (itemPath == null) {
+ int mediaItemCount = mMediaSet.getMediaItemCount();
+ if (mediaItemCount > 0) {
+ if (mCurrentIndex >= mediaItemCount) mCurrentIndex = 0;
+ itemPath = mMediaSet.getMediaItem(mCurrentIndex, 1)
+ .get(0).getPath();
+ } else {
+ // Bail out, PhotoPage can't load on an empty album
+ return;
+ }
+ }
+ PhotoDataAdapter pda = new PhotoDataAdapter(
+ mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex,
+ mAppBridge == null ? -1 : 0,
+ mAppBridge == null ? false : mAppBridge.isPanorama(),
+ mAppBridge == null ? false : mAppBridge.isStaticCamera());
+ mModel = pda;
+ mPhotoView.setModel(mModel);
+
+ pda.setDataListener(new PhotoDataAdapter.DataListener() {
+
+ @Override
+ public void onPhotoChanged(int index, Path item) {
+ int oldIndex = mCurrentIndex;
+ mCurrentIndex = index;
+
+ if (mHasCameraScreennailOrPlaceholder) {
+ if (mCurrentIndex > 0) {
+ mSkipUpdateCurrentPhoto = false;
+ }
+
+ if (oldIndex == 0 && mCurrentIndex > 0
+ && !mPhotoView.getFilmMode()) {
+ mPhotoView.setFilmMode(true);
+ if (mAppBridge != null) {
+ UsageStatistics.onEvent("CameraToFilmstrip",
+ UsageStatistics.TRANSITION_SWIPE, null);
+ }
+ } else if (oldIndex == 2 && mCurrentIndex == 1) {
+ mCameraSwitchCutoff = SystemClock.uptimeMillis() +
+ CAMERA_SWITCH_CUTOFF_THRESHOLD_MS;
+ mPhotoView.stopScrolling();
+ } else if (oldIndex >= 1 && mCurrentIndex == 0) {
+ mPhotoView.setWantPictureCenterCallbacks(true);
+ mSkipUpdateCurrentPhoto = true;
+ }
+ }
+ if (!mSkipUpdateCurrentPhoto) {
+ if (item != null) {
+ MediaItem photo = mModel.getMediaItem(0);
+ if (photo != null) updateCurrentPhoto(photo);
+ }
+ updateBars();
+ }
+ // Reset the timeout for the bars after a swipe
+ refreshHidingMessage();
+ }
+
+ @Override
+ public void onLoadingFinished(boolean loadingFailed) {
+ if (!mModel.isEmpty()) {
+ MediaItem photo = mModel.getMediaItem(0);
+ if (photo != null) updateCurrentPhoto(photo);
+ } else if (mIsActive) {
+ // We only want to finish the PhotoPage if there is no
+ // deletion that the user can undo.
+ if (mMediaSet.getNumberOfDeletions() == 0) {
+ mActivity.getStateManager().finishState(
+ PhotoPage.this);
+ }
+ }
+ }
+
+ @Override
+ public void onLoadingStarted() {
+ }
+ });
+ } else {
+ // Get default media set by the URI
+ MediaItem mediaItem = (MediaItem)
+ mActivity.getDataManager().getMediaObject(itemPath);
+ mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem);
+ mPhotoView.setModel(mModel);
+ updateCurrentPhoto(mediaItem);
+ mShowSpinner = false;
+ }
+
+ mPhotoView.setFilmMode(mStartInFilmstrip && mMediaSet.getMediaItemCount() > 1);
+ RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity)
+ .findViewById(mAppBridge != null ? R.id.content : R.id.gallery_root);
+ if (galleryRoot != null) {
+ if (mSecureAlbum == null) {
+ mBottomControls = new PhotoPageBottomControls(this, mActivity, galleryRoot);
+ }
+ StitchingProgressManager progressManager = mApplication.getStitchingProgressManager();
+ if (progressManager != null) {
+ mProgressBar = new PhotoPageProgressBar(mActivity, galleryRoot);
+ mProgressListener = new UpdateProgressListener();
+ progressManager.addChangeListener(mProgressListener);
+ if (mSecureAlbum != null) {
+ progressManager.addChangeListener(mSecureAlbum);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onPictureCenter(boolean isCamera) {
+ isCamera = isCamera || (mHasCameraScreennailOrPlaceholder && mAppBridge == null);
+ mPhotoView.setWantPictureCenterCallbacks(false);
+ mHandler.removeMessages(MSG_ON_CAMERA_CENTER);
+ mHandler.removeMessages(MSG_ON_PICTURE_CENTER);
+ mHandler.sendEmptyMessage(isCamera ? MSG_ON_CAMERA_CENTER : MSG_ON_PICTURE_CENTER);
+ }
+
+ @Override
+ public boolean canDisplayBottomControls() {
+ return mIsActive && !mPhotoView.canUndo();
+ }
+
+ @Override
+ public boolean canDisplayBottomControl(int control) {
+ if (mCurrentPhoto == null) {
+ return false;
+ }
+ switch(control) {
+ case R.id.photopage_bottom_control_edit:
+ return mHaveImageEditor && mShowBars
+ && !mPhotoView.getFilmMode()
+ && (mCurrentPhoto.getSupportedOperations() & MediaItem.SUPPORT_EDIT) != 0
+ && mCurrentPhoto.getMediaType() == MediaObject.MEDIA_TYPE_IMAGE;
+ case R.id.photopage_bottom_control_panorama:
+ return mIsPanorama;
+ case R.id.photopage_bottom_control_tiny_planet:
+ return mHaveImageEditor && mShowBars
+ && mIsPanorama360 && !mPhotoView.getFilmMode();
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public void onBottomControlClicked(int control) {
+ switch(control) {
+ case R.id.photopage_bottom_control_edit:
+ launchPhotoEditor();
+ return;
+ case R.id.photopage_bottom_control_panorama:
+ mActivity.getPanoramaViewHelper()
+ .showPanorama(mCurrentPhoto.getContentUri());
+ return;
+ case R.id.photopage_bottom_control_tiny_planet:
+ launchTinyPlanet();
+ return;
+ default:
+ return;
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+ private void setupNfcBeamPush() {
+ if (!ApiHelper.HAS_SET_BEAM_PUSH_URIS) return;
+
+ NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mActivity);
+ if (adapter != null) {
+ adapter.setBeamPushUris(null, mActivity);
+ adapter.setBeamPushUrisCallback(new CreateBeamUrisCallback() {
+ @Override
+ public Uri[] createBeamUris(NfcEvent event) {
+ return mNfcPushUris;
+ }
+ }, mActivity);
+ }
+ }
+
+ private void setNfcBeamPushUri(Uri uri) {
+ mNfcPushUris[0] = uri;
+ }
+
+ private static Intent createShareIntent(MediaObject mediaObject) {
+ int type = mediaObject.getMediaType();
+ return new Intent(Intent.ACTION_SEND)
+ .setType(MenuExecutor.getMimeType(type))
+ .putExtra(Intent.EXTRA_STREAM, mediaObject.getContentUri())
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+
+ private static Intent createSharePanoramaIntent(Uri contentUri) {
+ return new Intent(Intent.ACTION_SEND)
+ .setType(GalleryUtils.MIME_TYPE_PANORAMA360)
+ .putExtra(Intent.EXTRA_STREAM, contentUri)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+
+ private void overrideTransitionToEditor() {
+ ((Activity) mActivity).overridePendingTransition(android.R.anim.fade_in,
+ android.R.anim.fade_out);
+ }
+
+ private void launchTinyPlanet() {
+ // Deep link into tiny planet
+ MediaItem current = mModel.getMediaItem(0);
+ Intent intent = new Intent(FilterShowActivity.TINY_PLANET_ACTION);
+ intent.setClass(mActivity, FilterShowActivity.class);
+ intent.setDataAndType(current.getContentUri(), current.getMimeType())
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.putExtra(FilterShowActivity.LAUNCH_FULLSCREEN,
+ mActivity.isFullscreen());
+ mActivity.startActivityForResult(intent, REQUEST_EDIT);
+ overrideTransitionToEditor();
+ }
+
+ private void launchCamera() {
+ Intent intent = new Intent(mActivity, CameraActivity.class)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mRecenterCameraOnResume = false;
+ mActivity.startActivity(intent);
+ }
+
+ private void launchPhotoEditor() {
+ MediaItem current = mModel.getMediaItem(0);
+ if (current == null || (current.getSupportedOperations()
+ & MediaObject.SUPPORT_EDIT) == 0) {
+ return;
+ }
+
+ Intent intent = new Intent(ACTION_NEXTGEN_EDIT);
+
+ intent.setDataAndType(current.getContentUri(), current.getMimeType())
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ if (mActivity.getPackageManager()
+ .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() == 0) {
+ intent.setAction(Intent.ACTION_EDIT);
+ }
+ intent.putExtra(FilterShowActivity.LAUNCH_FULLSCREEN,
+ mActivity.isFullscreen());
+ ((Activity) mActivity).startActivityForResult(Intent.createChooser(intent, null),
+ REQUEST_EDIT);
+ overrideTransitionToEditor();
+ }
+
+ private void launchSimpleEditor() {
+ MediaItem current = mModel.getMediaItem(0);
+ if (current == null || (current.getSupportedOperations()
+ & MediaObject.SUPPORT_EDIT) == 0) {
+ return;
+ }
+
+ Intent intent = new Intent(ACTION_SIMPLE_EDIT);
+
+ intent.setDataAndType(current.getContentUri(), current.getMimeType())
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ if (mActivity.getPackageManager()
+ .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() == 0) {
+ intent.setAction(Intent.ACTION_EDIT);
+ }
+ intent.putExtra(FilterShowActivity.LAUNCH_FULLSCREEN,
+ mActivity.isFullscreen());
+ ((Activity) mActivity).startActivityForResult(Intent.createChooser(intent, null),
+ REQUEST_EDIT);
+ overrideTransitionToEditor();
+ }
+
+ private void requestDeferredUpdate() {
+ mDeferUpdateUntil = SystemClock.uptimeMillis() + DEFERRED_UPDATE_MS;
+ if (!mDeferredUpdateWaiting) {
+ mDeferredUpdateWaiting = true;
+ mHandler.sendEmptyMessageDelayed(MSG_UPDATE_DEFERRED, DEFERRED_UPDATE_MS);
+ }
+ }
+
+ private void updateUIForCurrentPhoto() {
+ if (mCurrentPhoto == null) return;
+
+ // If by swiping or deletion the user ends up on an action item
+ // and zoomed in, zoom out so that the context of the action is
+ // more clear
+ if ((mCurrentPhoto.getSupportedOperations() & MediaObject.SUPPORT_ACTION) != 0
+ && !mPhotoView.getFilmMode()) {
+ mPhotoView.setWantPictureCenterCallbacks(true);
+ }
+
+ updateMenuOperations();
+ refreshBottomControlsWhenReady();
+ if (mShowDetails) {
+ mDetailsHelper.reloadDetails();
+ }
+ if ((mSecureAlbum == null)
+ && (mCurrentPhoto.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) {
+ mCurrentPhoto.getPanoramaSupport(mUpdateShareURICallback);
+ }
+ updateProgressBar();
+ }
+
+ private void updateCurrentPhoto(MediaItem photo) {
+ if (mCurrentPhoto == photo) return;
+ mCurrentPhoto = photo;
+ if (mPhotoView.getFilmMode()) {
+ requestDeferredUpdate();
+ } else {
+ updateUIForCurrentPhoto();
+ }
+ }
+
+ private void updateProgressBar() {
+ if (mProgressBar != null) {
+ mProgressBar.hideProgress();
+ StitchingProgressManager progressManager = mApplication.getStitchingProgressManager();
+ if (progressManager != null && mCurrentPhoto instanceof LocalImage) {
+ Integer progress = progressManager.getProgress(mCurrentPhoto.getContentUri());
+ if (progress != null) {
+ mProgressBar.setProgress(progress);
+ }
+ }
+ }
+ }
+
+ private void updateMenuOperations() {
+ Menu menu = mActionBar.getMenu();
+
+ // it could be null if onCreateActionBar has not been called yet
+ if (menu == null) return;
+
+ MenuItem item = menu.findItem(R.id.action_slideshow);
+ if (item != null) {
+ item.setVisible((mSecureAlbum == null) && canDoSlideShow());
+ }
+ if (mCurrentPhoto == null) return;
+
+ int supportedOperations = mCurrentPhoto.getSupportedOperations();
+ if (mSecureAlbum != null) {
+ supportedOperations &= MediaObject.SUPPORT_DELETE;
+ } else {
+ mCurrentPhoto.getPanoramaSupport(mUpdatePanoramaMenuItemsCallback);
+ if (!mHaveImageEditor) {
+ supportedOperations &= ~MediaObject.SUPPORT_EDIT;
+ }
+ }
+ MenuExecutor.updateMenuOperation(menu, supportedOperations);
+ }
+
+ private boolean canDoSlideShow() {
+ if (mMediaSet == null || mCurrentPhoto == null) {
+ return false;
+ }
+ if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) {
+ return false;
+ }
+ return true;
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ // Action Bar show/hide management
+ //////////////////////////////////////////////////////////////////////////
+
+ private void showBars() {
+ if (mShowBars) return;
+ mShowBars = true;
+ mOrientationManager.unlockOrientation();
+ mActionBar.show();
+ mActivity.getGLRoot().setLightsOutMode(false);
+ refreshHidingMessage();
+ refreshBottomControlsWhenReady();
+ }
+
+ private void hideBars() {
+ if (!mShowBars) return;
+ mShowBars = false;
+ mActionBar.hide();
+ mActivity.getGLRoot().setLightsOutMode(true);
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ refreshBottomControlsWhenReady();
+ }
+
+ private void refreshHidingMessage() {
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ if (!mIsMenuVisible && !mPhotoView.getFilmMode()) {
+ mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
+ }
+ }
+
+ private boolean canShowBars() {
+ // No bars if we are showing camera preview.
+ if (mAppBridge != null && mCurrentIndex == 0
+ && !mPhotoView.getFilmMode()) return false;
+
+ // No bars if it's not allowed.
+ if (!mActionBarAllowed) return false;
+
+ Configuration config = mActivity.getResources().getConfiguration();
+ if (config.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void wantBars() {
+ if (canShowBars()) showBars();
+ }
+
+ private void toggleBars() {
+ if (mShowBars) {
+ hideBars();
+ } else {
+ if (canShowBars()) showBars();
+ }
+ }
+
+ private void updateBars() {
+ if (!canShowBars()) {
+ hideBars();
+ }
+ }
+
+ @Override
+ protected void onBackPressed() {
+ if (mShowDetails) {
+ hideDetails();
+ } else if (mAppBridge == null || !switchWithCaptureAnimation(-1)) {
+ // We are leaving this page. Set the result now.
+ setResult();
+ if (mStartInFilmstrip && !mPhotoView.getFilmMode()) {
+ mPhotoView.setFilmMode(true);
+ } else if (mTreatBackAsUp) {
+ onUpPressed();
+ } else {
+ super.onBackPressed();
+ }
+ }
+ }
+
+ private void onUpPressed() {
+ if ((mStartInFilmstrip || mAppBridge != null)
+ && !mPhotoView.getFilmMode()) {
+ mPhotoView.setFilmMode(true);
+ return;
+ }
+
+ if (mActivity.getStateManager().getStateCount() > 1) {
+ setResult();
+ super.onBackPressed();
+ return;
+ }
+
+ if (mOriginalSetPathString == null) return;
+
+ if (mAppBridge == null) {
+ // We're in view mode so set up the stacks on our own.
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
+ data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
+ mActivity.getDataManager().getTopSetPath(
+ DataManager.INCLUDE_ALL));
+ mActivity.getStateManager().switchState(this, AlbumPage.class, data);
+ } else {
+ GalleryUtils.startGalleryActivity(mActivity);
+ }
+ }
+
+ private void setResult() {
+ Intent result = null;
+ result = new Intent();
+ result.putExtra(KEY_RETURN_INDEX_HINT, mCurrentIndex);
+ setStateResult(Activity.RESULT_OK, result);
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ // AppBridge.Server interface
+ //////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void setCameraRelativeFrame(Rect frame) {
+ mPhotoView.setCameraRelativeFrame(frame);
+ }
+
+ @Override
+ public boolean switchWithCaptureAnimation(int offset) {
+ return mPhotoView.switchWithCaptureAnimation(offset);
+ }
+
+ @Override
+ public void setSwipingEnabled(boolean enabled) {
+ mPhotoView.setSwipingEnabled(enabled);
+ }
+
+ @Override
+ public void notifyScreenNailChanged() {
+ mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
+ mScreenNailSet.notifyChange();
+ }
+
+ @Override
+ public void addSecureAlbumItem(boolean isVideo, int id) {
+ mSecureAlbum.addMediaItem(isVideo, id);
+ }
+
+ @Override
+ protected boolean onCreateActionBar(Menu menu) {
+ mActionBar.createActionBarMenu(R.menu.photo, menu);
+ mHaveImageEditor = GalleryUtils.isEditorAvailable(mActivity, "image/*");
+ updateMenuOperations();
+ mActionBar.setTitle(mMediaSet != null ? mMediaSet.getName() : "");
+ return true;
+ }
+
+ private MenuExecutor.ProgressListener mConfirmDialogListener =
+ new MenuExecutor.ProgressListener() {
+ @Override
+ public void onProgressUpdate(int index) {}
+
+ @Override
+ public void onProgressComplete(int result) {}
+
+ @Override
+ public void onConfirmDialogShown() {
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ }
+
+ @Override
+ public void onConfirmDialogDismissed(boolean confirmed) {
+ refreshHidingMessage();
+ }
+
+ @Override
+ public void onProgressStart() {}
+ };
+
+ private void switchToGrid() {
+ if (mActivity.getStateManager().hasStateClass(AlbumPage.class)) {
+ onUpPressed();
+ } else {
+ if (mOriginalSetPathString == null) return;
+ if (mProgressBar != null) {
+ updateCurrentPhoto(null);
+ mProgressBar.hideProgress();
+ }
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
+ data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
+ mActivity.getDataManager().getTopSetPath(
+ DataManager.INCLUDE_ALL));
+
+ // We only show cluster menu in the first AlbumPage in stack
+ // TODO: Enable this when running from the camera app
+ boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
+ data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum
+ && mAppBridge == null);
+
+ data.putBoolean(PhotoPage.KEY_APP_BRIDGE, mAppBridge != null);
+
+ // Account for live preview being first item
+ mActivity.getTransitionStore().put(KEY_RETURN_INDEX_HINT,
+ mAppBridge != null ? mCurrentIndex - 1 : mCurrentIndex);
+
+ if (mHasCameraScreennailOrPlaceholder && mAppBridge != null) {
+ mActivity.getStateManager().startState(AlbumPage.class, data);
+ } else {
+ mActivity.getStateManager().switchState(this, AlbumPage.class, data);
+ }
+ }
+ }
+
+ @Override
+ protected boolean onItemSelected(MenuItem item) {
+ if (mModel == null) return true;
+ refreshHidingMessage();
+ MediaItem current = mModel.getMediaItem(0);
+
+ // This is a shield for monkey when it clicks the action bar
+ // menu when transitioning from filmstrip to camera
+ if (current instanceof SnailItem) return true;
+ // TODO: We should check the current photo against the MediaItem
+ // that the menu was initially created for. We need to fix this
+ // after PhotoPage being refactored.
+ if (current == null) {
+ // item is not ready, ignore
+ return true;
+ }
+ int currentIndex = mModel.getCurrentIndex();
+ Path path = current.getPath();
+
+ DataManager manager = mActivity.getDataManager();
+ int action = item.getItemId();
+ String confirmMsg = null;
+ switch (action) {
+ case android.R.id.home: {
+ onUpPressed();
+ return true;
+ }
+ case R.id.action_slideshow: {
+ Bundle data = new Bundle();
+ data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
+ data.putString(SlideshowPage.KEY_ITEM_PATH, path.toString());
+ data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex);
+ data.putBoolean(SlideshowPage.KEY_REPEAT, true);
+ mActivity.getStateManager().startStateForResult(
+ SlideshowPage.class, REQUEST_SLIDESHOW, data);
+ return true;
+ }
+ case R.id.action_crop: {
+ Activity activity = mActivity;
+ Intent intent = new Intent(CropActivity.CROP_ACTION);
+ intent.setClass(activity, CropActivity.class);
+ intent.setDataAndType(manager.getContentUri(path), current.getMimeType())
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
+ ? REQUEST_CROP_PICASA
+ : REQUEST_CROP);
+ return true;
+ }
+ case R.id.action_trim: {
+ Intent intent = new Intent(mActivity, TrimVideo.class);
+ intent.setData(manager.getContentUri(path));
+ // We need the file path to wrap this into a RandomAccessFile.
+ intent.putExtra(KEY_MEDIA_ITEM_PATH, current.getFilePath());
+ mActivity.startActivityForResult(intent, REQUEST_TRIM);
+ return true;
+ }
+ case R.id.action_mute: {
+ MuteVideo muteVideo = new MuteVideo(current.getFilePath(),
+ manager.getContentUri(path), mActivity);
+ muteVideo.muteInBackground();
+ return true;
+ }
+ case R.id.action_edit: {
+ launchPhotoEditor();
+ return true;
+ }
+ case R.id.action_simple_edit: {
+ launchSimpleEditor();
+ return true;
+ }
+ case R.id.action_details: {
+ if (mShowDetails) {
+ hideDetails();
+ } else {
+ showDetails();
+ }
+ return true;
+ }
+ case R.id.action_delete:
+ confirmMsg = mActivity.getResources().getQuantityString(
+ R.plurals.delete_selection, 1);
+ case R.id.action_setas:
+ case R.id.action_rotate_ccw:
+ case R.id.action_rotate_cw:
+ case R.id.action_show_on_map:
+ mSelectionManager.deSelectAll();
+ mSelectionManager.toggle(path);
+ mMenuExecutor.onMenuClicked(item, confirmMsg, mConfirmDialogListener);
+ return true;
+ default :
+ return false;
+ }
+ }
+
+ private void hideDetails() {
+ mShowDetails = false;
+ mDetailsHelper.hide();
+ }
+
+ private void showDetails() {
+ mShowDetails = true;
+ if (mDetailsHelper == null) {
+ mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource());
+ mDetailsHelper.setCloseListener(new CloseListener() {
+ @Override
+ public void onClose() {
+ hideDetails();
+ }
+ });
+ }
+ mDetailsHelper.show();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Callbacks from PhotoView
+ ////////////////////////////////////////////////////////////////////////////
+ @Override
+ public void onSingleTapUp(int x, int y) {
+ if (mAppBridge != null) {
+ if (mAppBridge.onSingleTapUp(x, y)) return;
+ }
+
+ MediaItem item = mModel.getMediaItem(0);
+ if (item == null || item == mScreenNailItem) {
+ // item is not ready or it is camera preview, ignore
+ return;
+ }
+
+ int supported = item.getSupportedOperations();
+ boolean playVideo = ((supported & MediaItem.SUPPORT_PLAY) != 0);
+ boolean unlock = ((supported & MediaItem.SUPPORT_UNLOCK) != 0);
+ boolean goBack = ((supported & MediaItem.SUPPORT_BACK) != 0);
+ boolean launchCamera = ((supported & MediaItem.SUPPORT_CAMERA_SHORTCUT) != 0);
+
+ if (playVideo) {
+ // determine if the point is at center (1/6) of the photo view.
+ // (The position of the "play" icon is at center (1/6) of the photo)
+ int w = mPhotoView.getWidth();
+ int h = mPhotoView.getHeight();
+ playVideo = (Math.abs(x - w / 2) * 12 <= w)
+ && (Math.abs(y - h / 2) * 12 <= h);
+ }
+
+ if (playVideo) {
+ if (mSecureAlbum == null) {
+ playVideo(mActivity, item.getPlayUri(), item.getName());
+ } else {
+ mActivity.getStateManager().finishState(this);
+ }
+ } else if (goBack) {
+ onBackPressed();
+ } else if (unlock) {
+ Intent intent = new Intent(mActivity, Gallery.class);
+ intent.putExtra(Gallery.KEY_DISMISS_KEYGUARD, true);
+ mActivity.startActivity(intent);
+ } else if (launchCamera) {
+ launchCamera();
+ } else {
+ toggleBars();
+ }
+ }
+
+ @Override
+ public void onActionBarAllowed(boolean allowed) {
+ mActionBarAllowed = allowed;
+ mHandler.sendEmptyMessage(MSG_UPDATE_ACTION_BAR);
+ }
+
+ @Override
+ public void onActionBarWanted() {
+ mHandler.sendEmptyMessage(MSG_WANT_BARS);
+ }
+
+ @Override
+ public void onFullScreenChanged(boolean full) {
+ Message m = mHandler.obtainMessage(
+ MSG_ON_FULL_SCREEN_CHANGED, full ? 1 : 0, 0);
+ m.sendToTarget();
+ }
+
+ // How we do delete/undo:
+ //
+ // When the user choose to delete a media item, we just tell the
+ // FilterDeleteSet to hide that item. If the user choose to undo it, we
+ // again tell FilterDeleteSet not to hide it. If the user choose to commit
+ // the deletion, we then actually delete the media item.
+ @Override
+ public void onDeleteImage(Path path, int offset) {
+ onCommitDeleteImage(); // commit the previous deletion
+ mDeletePath = path;
+ mDeleteIsFocus = (offset == 0);
+ mMediaSet.addDeletion(path, mCurrentIndex + offset);
+ }
+
+ @Override
+ public void onUndoDeleteImage() {
+ if (mDeletePath == null) return;
+ // If the deletion was done on the focused item, we want the model to
+ // focus on it when it is undeleted.
+ if (mDeleteIsFocus) mModel.setFocusHintPath(mDeletePath);
+ mMediaSet.removeDeletion(mDeletePath);
+ mDeletePath = null;
+ }
+
+ @Override
+ public void onCommitDeleteImage() {
+ if (mDeletePath == null) return;
+ mMenuExecutor.startSingleItemAction(R.id.action_delete, mDeletePath);
+ mDeletePath = null;
+ }
+
+ public void playVideo(Activity activity, Uri uri, String title) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW)
+ .setDataAndType(uri, "video/*")
+ .putExtra(Intent.EXTRA_TITLE, title)
+ .putExtra(MovieActivity.KEY_TREAT_UP_AS_BACK, true);
+ activity.startActivityForResult(intent, REQUEST_PLAY_VIDEO);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(activity, activity.getString(R.string.video_err),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void setCurrentPhotoByIntent(Intent intent) {
+ if (intent == null) return;
+ Path path = mApplication.getDataManager()
+ .findPathByUri(intent.getData(), intent.getType());
+ if (path != null) {
+ Path albumPath = mApplication.getDataManager().getDefaultSetOf(path);
+ if (!albumPath.equalsIgnoreCase(mOriginalSetPathString)) {
+ // If the edited image is stored in a different album, we need
+ // to start a new activity state to show the new image
+ Bundle data = new Bundle(getData());
+ data.putString(KEY_MEDIA_SET_PATH, albumPath.toString());
+ data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, path.toString());
+ mActivity.getStateManager().startState(SinglePhotoPage.class, data);
+ return;
+ }
+ mModel.setCurrentPhoto(path, mCurrentIndex);
+ }
+ }
+
+ @Override
+ protected void onStateResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == Activity.RESULT_CANCELED) {
+ // This is a reset, not a canceled
+ return;
+ }
+ if (resultCode == ProxyLauncher.RESULT_USER_CANCELED) {
+ // Unmap reset vs. canceled
+ resultCode = Activity.RESULT_CANCELED;
+ }
+ mRecenterCameraOnResume = false;
+ switch (requestCode) {
+ case REQUEST_EDIT:
+ setCurrentPhotoByIntent(data);
+ break;
+ case REQUEST_CROP:
+ if (resultCode == Activity.RESULT_OK) {
+ setCurrentPhotoByIntent(data);
+ }
+ break;
+ case REQUEST_CROP_PICASA: {
+ if (resultCode == Activity.RESULT_OK) {
+ Context context = mActivity.getAndroidContext();
+ String message = context.getString(R.string.crop_saved,
+ context.getString(R.string.folder_edited_online_photos));
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+ }
+ break;
+ }
+ case REQUEST_SLIDESHOW: {
+ if (data == null) break;
+ String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH);
+ int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
+ if (path != null) {
+ mModel.setCurrentPhoto(Path.fromString(path), index);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mIsActive = false;
+
+ mActivity.getGLRoot().unfreeze();
+ mHandler.removeMessages(MSG_UNFREEZE_GLROOT);
+
+ DetailsHelper.pause();
+ // Hide the detail dialog on exit
+ if (mShowDetails) hideDetails();
+ if (mModel != null) {
+ mModel.pause();
+ }
+ mPhotoView.pause();
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ mHandler.removeMessages(MSG_REFRESH_BOTTOM_CONTROLS);
+ refreshBottomControlsWhenReady();
+ mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
+ if (mShowSpinner) {
+ mActionBar.disableAlbumModeMenu(true);
+ }
+ onCommitDeleteImage();
+ mMenuExecutor.pause();
+ if (mMediaSet != null) mMediaSet.clearDeletion();
+ }
+
+ @Override
+ public void onCurrentImageUpdated() {
+ mActivity.getGLRoot().unfreeze();
+ }
+
+ @Override
+ public void onFilmModeChanged(boolean enabled) {
+ refreshBottomControlsWhenReady();
+ if (mShowSpinner) {
+ if (enabled) {
+ mActionBar.enableAlbumModeMenu(
+ GalleryActionBar.ALBUM_FILMSTRIP_MODE_SELECTED, this);
+ } else {
+ mActionBar.disableAlbumModeMenu(true);
+ }
+ }
+ if (enabled) {
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ UsageStatistics.onContentViewChanged(
+ UsageStatistics.COMPONENT_GALLERY, "FilmstripPage");
+ } else {
+ refreshHidingMessage();
+ if (mAppBridge == null || mCurrentIndex > 0) {
+ UsageStatistics.onContentViewChanged(
+ UsageStatistics.COMPONENT_GALLERY, "SinglePhotoPage");
+ } else {
+ UsageStatistics.onContentViewChanged(
+ UsageStatistics.COMPONENT_CAMERA, "Unknown"); // TODO
+ }
+ }
+ }
+
+ private void transitionFromAlbumPageIfNeeded() {
+ TransitionStore transitions = mActivity.getTransitionStore();
+
+ int albumPageTransition = transitions.get(
+ KEY_ALBUMPAGE_TRANSITION, MSG_ALBUMPAGE_NONE);
+
+ if (albumPageTransition == MSG_ALBUMPAGE_NONE && mAppBridge != null
+ && mRecenterCameraOnResume) {
+ // Generally, resuming the PhotoPage when in Camera should
+ // reset to the capture mode to allow quick photo taking
+ mCurrentIndex = 0;
+ mPhotoView.resetToFirstPicture();
+ } else {
+ int resumeIndex = transitions.get(KEY_INDEX_HINT, -1);
+ if (resumeIndex >= 0) {
+ if (mHasCameraScreennailOrPlaceholder) {
+ // Account for preview/placeholder being the first item
+ resumeIndex++;
+ }
+ if (resumeIndex < mMediaSet.getMediaItemCount()) {
+ mCurrentIndex = resumeIndex;
+ mModel.moveTo(mCurrentIndex);
+ }
+ }
+ }
+
+ if (albumPageTransition == MSG_ALBUMPAGE_RESUMED) {
+ mPhotoView.setFilmMode(mStartInFilmstrip || mAppBridge != null);
+ } else if (albumPageTransition == MSG_ALBUMPAGE_PICKED) {
+ mPhotoView.setFilmMode(false);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (mModel == null) {
+ mActivity.getStateManager().finishState(this);
+ return;
+ }
+ transitionFromAlbumPageIfNeeded();
+
+ mActivity.getGLRoot().freeze();
+ mIsActive = true;
+ setContentPane(mRootPane);
+
+ mModel.resume();
+ mPhotoView.resume();
+ mActionBar.setDisplayOptions(
+ ((mSecureAlbum == null) && (mSetPathString != null)), false);
+ mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
+ refreshBottomControlsWhenReady();
+ if (mShowSpinner && mPhotoView.getFilmMode()) {
+ mActionBar.enableAlbumModeMenu(
+ GalleryActionBar.ALBUM_FILMSTRIP_MODE_SELECTED, this);
+ }
+ if (!mShowBars) {
+ mActionBar.hide();
+ mActivity.getGLRoot().setLightsOutMode(true);
+ }
+ boolean haveImageEditor = GalleryUtils.isEditorAvailable(mActivity, "image/*");
+ if (haveImageEditor != mHaveImageEditor) {
+ mHaveImageEditor = haveImageEditor;
+ updateMenuOperations();
+ }
+
+ mRecenterCameraOnResume = true;
+ mHandler.sendEmptyMessageDelayed(MSG_UNFREEZE_GLROOT, UNFREEZE_GLROOT_TIMEOUT);
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (mAppBridge != null) {
+ mAppBridge.setServer(null);
+ mScreenNailItem.setScreenNail(null);
+ mAppBridge.detachScreenNail();
+ mAppBridge = null;
+ mScreenNailSet = null;
+ mScreenNailItem = null;
+ }
+ mActivity.getGLRoot().setOrientationSource(null);
+ if (mBottomControls != null) mBottomControls.cleanup();
+
+ // Remove all pending messages.
+ mHandler.removeCallbacksAndMessages(null);
+ super.onDestroy();
+ }
+
+ private class MyDetailsSource implements DetailsSource {
+
+ @Override
+ public MediaDetails getDetails() {
+ return mModel.getMediaItem(0).getDetails();
+ }
+
+ @Override
+ public int size() {
+ return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1;
+ }
+
+ @Override
+ public int setIndex() {
+ return mModel.getCurrentIndex();
+ }
+ }
+
+ @Override
+ public void onAlbumModeSelected(int mode) {
+ if (mode == GalleryActionBar.ALBUM_GRID_MODE_SELECTED) {
+ 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));
+ }
+ }
+ }
+
+ @Override
+ public void onUndoBarVisibilityChanged(boolean visible) {
+ refreshBottomControlsWhenReady();
+ }
+
+ @Override
+ public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) {
+ final long timestampMillis = mCurrentPhoto.getDateInMs();
+ final String mediaType = getMediaTypeString(mCurrentPhoto);
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_GALLERY,
+ UsageStatistics.ACTION_SHARE,
+ mediaType,
+ timestampMillis > 0
+ ? System.currentTimeMillis() - timestampMillis
+ : -1);
+ return false;
+ }
+
+ private static String getMediaTypeString(MediaItem item) {
+ if (item.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO) {
+ return "Video";
+ } else if (item.getMediaType() == MediaObject.MEDIA_TYPE_IMAGE) {
+ return "Photo";
+ } else {
+ return "Unknown:" + item.getMediaType();
+ }
+ }
+
+}
diff --git a/src/com/android/gallery3d/app/PhotoPageBottomControls.java b/src/com/android/gallery3d/app/PhotoPageBottomControls.java
new file mode 100644
index 000000000..24b8ceb7e
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoPageBottomControls.java
@@ -0,0 +1,137 @@
+/*
+ * 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 android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.widget.RelativeLayout;
+
+import com.android.gallery3d.R;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class PhotoPageBottomControls implements OnClickListener {
+ public interface Delegate {
+ public boolean canDisplayBottomControls();
+ public boolean canDisplayBottomControl(int control);
+ public void onBottomControlClicked(int control);
+ public void refreshBottomControlsWhenReady();
+ }
+
+ private Delegate mDelegate;
+ private ViewGroup mParentLayout;
+ private ViewGroup mContainer;
+
+ private boolean mContainerVisible = false;
+ private Map<View, Boolean> mControlsVisible = new HashMap<View, Boolean>();
+
+ private Animation mContainerAnimIn = new AlphaAnimation(0f, 1f);
+ private Animation mContainerAnimOut = new AlphaAnimation(1f, 0f);
+ private static final int CONTAINER_ANIM_DURATION_MS = 200;
+
+ private static final int CONTROL_ANIM_DURATION_MS = 150;
+ private static Animation getControlAnimForVisibility(boolean visible) {
+ Animation anim = visible ? new AlphaAnimation(0f, 1f)
+ : new AlphaAnimation(1f, 0f);
+ anim.setDuration(CONTROL_ANIM_DURATION_MS);
+ return anim;
+ }
+
+ public PhotoPageBottomControls(Delegate delegate, Context context, RelativeLayout layout) {
+ mDelegate = delegate;
+ mParentLayout = layout;
+
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mContainer = (ViewGroup) inflater
+ .inflate(R.layout.photopage_bottom_controls, mParentLayout, false);
+ mParentLayout.addView(mContainer);
+
+ for (int i = mContainer.getChildCount() - 1; i >= 0; i--) {
+ View child = mContainer.getChildAt(i);
+ child.setOnClickListener(this);
+ mControlsVisible.put(child, false);
+ }
+
+ mContainerAnimIn.setDuration(CONTAINER_ANIM_DURATION_MS);
+ mContainerAnimOut.setDuration(CONTAINER_ANIM_DURATION_MS);
+
+ mDelegate.refreshBottomControlsWhenReady();
+ }
+
+ private void hide() {
+ mContainer.clearAnimation();
+ mContainerAnimOut.reset();
+ mContainer.startAnimation(mContainerAnimOut);
+ mContainer.setVisibility(View.INVISIBLE);
+ }
+
+ private void show() {
+ mContainer.clearAnimation();
+ mContainerAnimIn.reset();
+ mContainer.startAnimation(mContainerAnimIn);
+ mContainer.setVisibility(View.VISIBLE);
+ }
+
+ public void refresh() {
+ boolean visible = mDelegate.canDisplayBottomControls();
+ boolean containerVisibilityChanged = (visible != mContainerVisible);
+ if (containerVisibilityChanged) {
+ if (visible) {
+ show();
+ } else {
+ hide();
+ }
+ mContainerVisible = visible;
+ }
+ if (!mContainerVisible) {
+ return;
+ }
+ for (View control : mControlsVisible.keySet()) {
+ Boolean prevVisibility = mControlsVisible.get(control);
+ boolean curVisibility = mDelegate.canDisplayBottomControl(control.getId());
+ if (prevVisibility.booleanValue() != curVisibility) {
+ if (!containerVisibilityChanged) {
+ control.clearAnimation();
+ control.startAnimation(getControlAnimForVisibility(curVisibility));
+ }
+ control.setVisibility(curVisibility ? View.VISIBLE : View.INVISIBLE);
+ mControlsVisible.put(control, curVisibility);
+ }
+ }
+ // Force a layout change
+ mContainer.requestLayout(); // Kick framework to draw the control.
+ }
+
+ public void cleanup() {
+ mParentLayout.removeView(mContainer);
+ mControlsVisible.clear();
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (mContainerVisible && mControlsVisible.get(view).booleanValue()) {
+ mDelegate.onBottomControlClicked(view.getId());
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/PhotoPageProgressBar.java b/src/com/android/gallery3d/app/PhotoPageProgressBar.java
new file mode 100644
index 000000000..141fea698
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoPageProgressBar.java
@@ -0,0 +1,50 @@
+/*
+ * 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 android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.RelativeLayout;
+
+import com.android.gallery3d.R;
+
+public class PhotoPageProgressBar {
+ private ViewGroup mContainer;
+ private View mProgress;
+
+ public PhotoPageProgressBar(Context context, RelativeLayout parentLayout) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mContainer = (ViewGroup) inflater.inflate(R.layout.photopage_progress_bar, parentLayout,
+ false);
+ parentLayout.addView(mContainer);
+ mProgress = mContainer.findViewById(R.id.photopage_progress_foreground);
+ }
+
+ public void setProgress(int progressPercent) {
+ mContainer.setVisibility(View.VISIBLE);
+ LayoutParams layoutParams = mProgress.getLayoutParams();
+ layoutParams.width = mContainer.getWidth() * progressPercent / 100;
+ mProgress.setLayoutParams(layoutParams);
+ }
+
+ public void hideProgress() {
+ mContainer.setVisibility(View.INVISIBLE);
+ }
+}
diff --git a/src/com/android/gallery3d/app/PickerActivity.java b/src/com/android/gallery3d/app/PickerActivity.java
new file mode 100644
index 000000000..d5bb218ea
--- /dev/null
+++ b/src/com/android/gallery3d/app/PickerActivity.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2011 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 android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.Window;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ui.GLRootView;
+
+public class PickerActivity extends AbstractGalleryActivity
+ implements OnClickListener {
+
+ public static final String KEY_ALBUM_PATH = "album-path";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // We show the picker in two ways. One smaller screen we use a full
+ // screen window with an action bar. On larger screen we use a dialog.
+ boolean isDialog = getResources().getBoolean(R.bool.picker_is_dialog);
+
+ if (!isDialog) {
+ requestWindowFeature(Window.FEATURE_ACTION_BAR);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+ }
+
+ setContentView(R.layout.dialog_picker);
+
+ if (isDialog) {
+ // In dialog mode, we don't have the action bar to show the
+ // "cancel" action, so we show an additional "cancel" button.
+ View view = findViewById(R.id.cancel);
+ view.setOnClickListener(this);
+ view.setVisibility(View.VISIBLE);
+
+ // We need this, otherwise the view will be dimmed because it
+ // is "behind" the dialog.
+ ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.pickup, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.action_cancel) {
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.cancel) finish();
+ }
+}
diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
new file mode 100644
index 000000000..00f2fe78f
--- /dev/null
+++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2010 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 android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.BitmapScreenNail;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.ui.TileImageViewAdapter;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+
+public class SinglePhotoDataAdapter extends TileImageViewAdapter
+ implements PhotoPage.Model {
+
+ private static final String TAG = "SinglePhotoDataAdapter";
+ private static final int SIZE_BACKUP = 1024;
+ private static final int MSG_UPDATE_IMAGE = 1;
+
+ private MediaItem mItem;
+ private boolean mHasFullImage;
+ private Future<?> mTask;
+ private Handler mHandler;
+
+ private PhotoView mPhotoView;
+ private ThreadPool mThreadPool;
+ private int mLoadingState = LOADING_INIT;
+ private BitmapScreenNail mBitmapScreenNail;
+
+ public SinglePhotoDataAdapter(
+ AbstractGalleryActivity activity, PhotoView view, MediaItem item) {
+ mItem = Utils.checkNotNull(item);
+ mHasFullImage = (item.getSupportedOperations() &
+ MediaItem.SUPPORT_FULL_IMAGE) != 0;
+ mPhotoView = Utils.checkNotNull(view);
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ @SuppressWarnings("unchecked")
+ public void handleMessage(Message message) {
+ Utils.assertTrue(message.what == MSG_UPDATE_IMAGE);
+ if (mHasFullImage) {
+ onDecodeLargeComplete((ImageBundle) message.obj);
+ } else {
+ onDecodeThumbComplete((Future<Bitmap>) message.obj);
+ }
+ }
+ };
+ mThreadPool = activity.getThreadPool();
+ }
+
+ private static class ImageBundle {
+ public final BitmapRegionDecoder decoder;
+ public final Bitmap backupImage;
+
+ public ImageBundle(BitmapRegionDecoder decoder, Bitmap backupImage) {
+ this.decoder = decoder;
+ this.backupImage = backupImage;
+ }
+ }
+
+ private FutureListener<BitmapRegionDecoder> mLargeListener =
+ new FutureListener<BitmapRegionDecoder>() {
+ @Override
+ public void onFutureDone(Future<BitmapRegionDecoder> future) {
+ BitmapRegionDecoder decoder = future.get();
+ if (decoder == null) return;
+ int width = decoder.getWidth();
+ int height = decoder.getHeight();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = BitmapUtils.computeSampleSize(
+ (float) SIZE_BACKUP / Math.max(width, height));
+ Bitmap bitmap = decoder.decodeRegion(new Rect(0, 0, width, height), options);
+ mHandler.sendMessage(mHandler.obtainMessage(
+ MSG_UPDATE_IMAGE, new ImageBundle(decoder, bitmap)));
+ }
+ };
+
+ private FutureListener<Bitmap> mThumbListener =
+ new FutureListener<Bitmap>() {
+ @Override
+ public void onFutureDone(Future<Bitmap> future) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_UPDATE_IMAGE, future));
+ }
+ };
+
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ private void setScreenNail(Bitmap bitmap, int width, int height) {
+ mBitmapScreenNail = new BitmapScreenNail(bitmap);
+ setScreenNail(mBitmapScreenNail, width, height);
+ }
+
+ private void onDecodeLargeComplete(ImageBundle bundle) {
+ try {
+ setScreenNail(bundle.backupImage,
+ bundle.decoder.getWidth(), bundle.decoder.getHeight());
+ setRegionDecoder(bundle.decoder);
+ mPhotoView.notifyImageChange(0);
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to decode large", t);
+ }
+ }
+
+ private void onDecodeThumbComplete(Future<Bitmap> future) {
+ try {
+ Bitmap backup = future.get();
+ if (backup == null) {
+ mLoadingState = LOADING_FAIL;
+ return;
+ } else {
+ mLoadingState = LOADING_COMPLETE;
+ }
+ setScreenNail(backup, backup.getWidth(), backup.getHeight());
+ mPhotoView.notifyImageChange(0);
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to decode thumb", t);
+ }
+ }
+
+ @Override
+ public void resume() {
+ if (mTask == null) {
+ if (mHasFullImage) {
+ mTask = mThreadPool.submit(
+ mItem.requestLargeImage(), mLargeListener);
+ } else {
+ mTask = mThreadPool.submit(
+ mItem.requestImage(MediaItem.TYPE_THUMBNAIL),
+ mThumbListener);
+ }
+ }
+ }
+
+ @Override
+ public void pause() {
+ Future<?> task = mTask;
+ task.cancel();
+ task.waitDone();
+ if (task.get() == null) {
+ mTask = null;
+ }
+ if (mBitmapScreenNail != null) {
+ mBitmapScreenNail.recycle();
+ mBitmapScreenNail = null;
+ }
+ }
+
+ @Override
+ public void moveTo(int index) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void getImageSize(int offset, PhotoView.Size size) {
+ if (offset == 0) {
+ size.width = mItem.getWidth();
+ size.height = mItem.getHeight();
+ } else {
+ size.width = 0;
+ size.height = 0;
+ }
+ }
+
+ @Override
+ public int getImageRotation(int offset) {
+ return (offset == 0) ? mItem.getFullImageRotation() : 0;
+ }
+
+ @Override
+ public ScreenNail getScreenNail(int offset) {
+ return (offset == 0) ? getScreenNail() : null;
+ }
+
+ @Override
+ public void setNeedFullImage(boolean enabled) {
+ // currently not necessary.
+ }
+
+ @Override
+ public boolean isCamera(int offset) {
+ return false;
+ }
+
+ @Override
+ public boolean isPanorama(int offset) {
+ return false;
+ }
+
+ @Override
+ public boolean isStaticCamera(int offset) {
+ return false;
+ }
+
+ @Override
+ public boolean isVideo(int offset) {
+ return mItem.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO;
+ }
+
+ @Override
+ public boolean isDeletable(int offset) {
+ return (mItem.getSupportedOperations() & MediaItem.SUPPORT_DELETE) != 0;
+ }
+
+ @Override
+ public MediaItem getMediaItem(int offset) {
+ return offset == 0 ? mItem : null;
+ }
+
+ @Override
+ public int getCurrentIndex() {
+ return 0;
+ }
+
+ @Override
+ public void setCurrentPhoto(Path path, int indexHint) {
+ // ignore
+ }
+
+ @Override
+ public void setFocusHintDirection(int direction) {
+ // ignore
+ }
+
+ @Override
+ public void setFocusHintPath(Path path) {
+ // ignore
+ }
+
+ @Override
+ public int getLoadingState(int offset) {
+ return mLoadingState;
+ }
+}
diff --git a/src/com/android/gallery3d/app/SinglePhotoPage.java b/src/com/android/gallery3d/app/SinglePhotoPage.java
new file mode 100644
index 000000000..beb87d358
--- /dev/null
+++ b/src/com/android/gallery3d/app/SinglePhotoPage.java
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+public class SinglePhotoPage extends PhotoPage {
+
+}
diff --git a/src/com/android/gallery3d/app/SlideshowDataAdapter.java b/src/com/android/gallery3d/app/SlideshowDataAdapter.java
new file mode 100644
index 000000000..7a0fba5fb
--- /dev/null
+++ b/src/com/android/gallery3d/app/SlideshowDataAdapter.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2010 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 android.graphics.Bitmap;
+
+import com.android.gallery3d.app.SlideshowPage.Slide;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.LinkedList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class SlideshowDataAdapter implements SlideshowPage.Model {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SlideshowDataAdapter";
+
+ private static final int IMAGE_QUEUE_CAPACITY = 3;
+
+ public interface SlideshowSource {
+ public void addContentListener(ContentListener listener);
+ public void removeContentListener(ContentListener listener);
+ public long reload();
+ public MediaItem getMediaItem(int index);
+ public int findItemIndex(Path path, int hint);
+ }
+
+ private final SlideshowSource mSource;
+
+ private int mLoadIndex = 0;
+ private int mNextOutput = 0;
+ private boolean mIsActive = false;
+ private boolean mNeedReset;
+ private boolean mDataReady;
+ private Path mInitialPath;
+
+ private final LinkedList<Slide> mImageQueue = new LinkedList<Slide>();
+
+ private Future<Void> mReloadTask;
+ private final ThreadPool mThreadPool;
+
+ private long mDataVersion = MediaObject.INVALID_DATA_VERSION;
+ private final AtomicBoolean mNeedReload = new AtomicBoolean(false);
+ private final SourceListener mSourceListener = new SourceListener();
+
+ // The index is just a hint if initialPath is set
+ public SlideshowDataAdapter(GalleryContext context, SlideshowSource source, int index,
+ Path initialPath) {
+ mSource = source;
+ mInitialPath = initialPath;
+ mLoadIndex = index;
+ mNextOutput = index;
+ mThreadPool = context.getThreadPool();
+ }
+
+ private MediaItem loadItem() {
+ if (mNeedReload.compareAndSet(true, false)) {
+ long v = mSource.reload();
+ if (v != mDataVersion) {
+ mDataVersion = v;
+ mNeedReset = true;
+ return null;
+ }
+ }
+ int index = mLoadIndex;
+ if (mInitialPath != null) {
+ index = mSource.findItemIndex(mInitialPath, index);
+ mInitialPath = null;
+ }
+ return mSource.getMediaItem(index);
+ }
+
+ private class ReloadTask implements Job<Void> {
+ @Override
+ public Void run(JobContext jc) {
+ while (true) {
+ synchronized (SlideshowDataAdapter.this) {
+ while (mIsActive && (!mDataReady
+ || mImageQueue.size() >= IMAGE_QUEUE_CAPACITY)) {
+ try {
+ SlideshowDataAdapter.this.wait();
+ } catch (InterruptedException ex) {
+ // ignored.
+ }
+ continue;
+ }
+ }
+ if (!mIsActive) return null;
+ mNeedReset = false;
+
+ MediaItem item = loadItem();
+
+ if (mNeedReset) {
+ synchronized (SlideshowDataAdapter.this) {
+ mImageQueue.clear();
+ mLoadIndex = mNextOutput;
+ }
+ continue;
+ }
+
+ if (item == null) {
+ synchronized (SlideshowDataAdapter.this) {
+ if (!mNeedReload.get()) mDataReady = false;
+ SlideshowDataAdapter.this.notifyAll();
+ }
+ continue;
+ }
+
+ Bitmap bitmap = item
+ .requestImage(MediaItem.TYPE_THUMBNAIL)
+ .run(jc);
+
+ if (bitmap != null) {
+ synchronized (SlideshowDataAdapter.this) {
+ mImageQueue.addLast(
+ new Slide(item, mLoadIndex, bitmap));
+ if (mImageQueue.size() == 1) {
+ SlideshowDataAdapter.this.notifyAll();
+ }
+ }
+ }
+ ++mLoadIndex;
+ }
+ }
+ }
+
+ private class SourceListener implements ContentListener {
+ @Override
+ public void onContentDirty() {
+ synchronized (SlideshowDataAdapter.this) {
+ mNeedReload.set(true);
+ mDataReady = true;
+ SlideshowDataAdapter.this.notifyAll();
+ }
+ }
+ }
+
+ private synchronized Slide innerNextBitmap() {
+ while (mIsActive && mDataReady && mImageQueue.isEmpty()) {
+ try {
+ wait();
+ } catch (InterruptedException t) {
+ throw new AssertionError();
+ }
+ }
+ if (mImageQueue.isEmpty()) return null;
+ mNextOutput++;
+ this.notifyAll();
+ return mImageQueue.removeFirst();
+ }
+
+ @Override
+ public Future<Slide> nextSlide(FutureListener<Slide> listener) {
+ return mThreadPool.submit(new Job<Slide>() {
+ @Override
+ public Slide run(JobContext jc) {
+ jc.setMode(ThreadPool.MODE_NONE);
+ return innerNextBitmap();
+ }
+ }, listener);
+ }
+
+ @Override
+ public void pause() {
+ synchronized (this) {
+ mIsActive = false;
+ notifyAll();
+ }
+ mSource.removeContentListener(mSourceListener);
+ mReloadTask.cancel();
+ mReloadTask.waitDone();
+ mReloadTask = null;
+ }
+
+ @Override
+ public synchronized void resume() {
+ mIsActive = true;
+ mSource.addContentListener(mSourceListener);
+ mNeedReload.set(true);
+ mDataReady = true;
+ mReloadTask = mThreadPool.submit(new ReloadTask());
+ }
+}
diff --git a/src/com/android/gallery3d/app/SlideshowPage.java b/src/com/android/gallery3d/app/SlideshowPage.java
new file mode 100644
index 000000000..174058dc8
--- /dev/null
+++ b/src/com/android/gallery3d/app/SlideshowPage.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2010 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 android.app.Activity;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.SlideshowView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+
+import java.util.ArrayList;
+import java.util.Random;
+
+public class SlideshowPage extends ActivityState {
+ private static final String TAG = "SlideshowPage";
+
+ public static final String KEY_SET_PATH = "media-set-path";
+ public static final String KEY_ITEM_PATH = "media-item-path";
+ public static final String KEY_PHOTO_INDEX = "photo-index";
+ public static final String KEY_RANDOM_ORDER = "random-order";
+ public static final String KEY_REPEAT = "repeat";
+ public static final String KEY_DREAM = "dream";
+
+ private static final long SLIDESHOW_DELAY = 3000; // 3 seconds
+
+ private static final int MSG_LOAD_NEXT_BITMAP = 1;
+ private static final int MSG_SHOW_PENDING_BITMAP = 2;
+
+ public static interface Model {
+ public void pause();
+
+ public void resume();
+
+ public Future<Slide> nextSlide(FutureListener<Slide> listener);
+ }
+
+ public static class Slide {
+ public Bitmap bitmap;
+ public MediaItem item;
+ public int index;
+
+ public Slide(MediaItem item, int index, Bitmap bitmap) {
+ this.bitmap = bitmap;
+ this.item = item;
+ this.index = index;
+ }
+ }
+
+ private Handler mHandler;
+ private Model mModel;
+ private SlideshowView mSlideshowView;
+
+ private Slide mPendingSlide = null;
+ private boolean mIsActive = false;
+ private final Intent mResultIntent = new Intent();
+
+ @Override
+ protected int getBackgroundColorId() {
+ return R.color.slideshow_background;
+ }
+
+ private final GLView mRootPane = new GLView() {
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ mSlideshowView.layout(0, 0, right - left, bottom - top);
+ }
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ onBackPressed();
+ }
+ return true;
+ }
+
+ @Override
+ protected void renderBackground(GLCanvas canvas) {
+ canvas.clearBuffer(getBackgroundColor());
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ super.onCreate(data, restoreState);
+ mFlags |= (FLAG_HIDE_ACTION_BAR | FLAG_HIDE_STATUS_BAR);
+ if (data.getBoolean(KEY_DREAM)) {
+ // Dream screensaver only keeps screen on for plugged devices.
+ mFlags |= FLAG_SCREEN_ON_WHEN_PLUGGED | FLAG_SHOW_WHEN_LOCKED;
+ } else {
+ // User-initiated slideshow would always keep screen on.
+ mFlags |= FLAG_SCREEN_ON_ALWAYS;
+ }
+
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_SHOW_PENDING_BITMAP:
+ showPendingBitmap();
+ break;
+ case MSG_LOAD_NEXT_BITMAP:
+ loadNextBitmap();
+ break;
+ default: throw new AssertionError();
+ }
+ }
+ };
+ initializeViews();
+ initializeData(data);
+ }
+
+ private void loadNextBitmap() {
+ mModel.nextSlide(new FutureListener<Slide>() {
+ @Override
+ public void onFutureDone(Future<Slide> future) {
+ mPendingSlide = future.get();
+ mHandler.sendEmptyMessage(MSG_SHOW_PENDING_BITMAP);
+ }
+ });
+ }
+
+ private void showPendingBitmap() {
+ // mPendingBitmap could be null, if
+ // 1.) there is no more items
+ // 2.) mModel is paused
+ Slide slide = mPendingSlide;
+ if (slide == null) {
+ if (mIsActive) {
+ mActivity.getStateManager().finishState(SlideshowPage.this);
+ }
+ return;
+ }
+
+ mSlideshowView.next(slide.bitmap, slide.item.getRotation());
+
+ setStateResult(Activity.RESULT_OK, mResultIntent
+ .putExtra(KEY_ITEM_PATH, slide.item.getPath().toString())
+ .putExtra(KEY_PHOTO_INDEX, slide.index));
+ mHandler.sendEmptyMessageDelayed(MSG_LOAD_NEXT_BITMAP, SLIDESHOW_DELAY);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mIsActive = false;
+ mModel.pause();
+ mSlideshowView.release();
+
+ mHandler.removeMessages(MSG_LOAD_NEXT_BITMAP);
+ mHandler.removeMessages(MSG_SHOW_PENDING_BITMAP);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mIsActive = true;
+ mModel.resume();
+
+ if (mPendingSlide != null) {
+ showPendingBitmap();
+ } else {
+ loadNextBitmap();
+ }
+ }
+
+ private void initializeData(Bundle data) {
+ boolean random = data.getBoolean(KEY_RANDOM_ORDER, false);
+
+ // We only want to show slideshow for images only, not videos.
+ String mediaPath = data.getString(KEY_SET_PATH);
+ mediaPath = FilterUtils.newFilterPath(mediaPath, FilterUtils.FILTER_IMAGE_ONLY);
+ MediaSet mediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
+
+ if (random) {
+ boolean repeat = data.getBoolean(KEY_REPEAT);
+ mModel = new SlideshowDataAdapter(mActivity,
+ new ShuffleSource(mediaSet, repeat), 0, null);
+ setStateResult(Activity.RESULT_OK, mResultIntent.putExtra(KEY_PHOTO_INDEX, 0));
+ } else {
+ int index = data.getInt(KEY_PHOTO_INDEX);
+ String itemPath = data.getString(KEY_ITEM_PATH);
+ Path path = itemPath != null ? Path.fromString(itemPath) : null;
+ boolean repeat = data.getBoolean(KEY_REPEAT);
+ mModel = new SlideshowDataAdapter(mActivity, new SequentialSource(mediaSet, repeat),
+ index, path);
+ setStateResult(Activity.RESULT_OK, mResultIntent.putExtra(KEY_PHOTO_INDEX, index));
+ }
+ }
+
+ private void initializeViews() {
+ mSlideshowView = new SlideshowView();
+ mRootPane.addComponent(mSlideshowView);
+ setContentPane(mRootPane);
+ }
+
+ private static MediaItem findMediaItem(MediaSet mediaSet, int index) {
+ for (int i = 0, n = mediaSet.getSubMediaSetCount(); i < n; ++i) {
+ MediaSet subset = mediaSet.getSubMediaSet(i);
+ int count = subset.getTotalMediaItemCount();
+ if (index < count) {
+ return findMediaItem(subset, index);
+ }
+ index -= count;
+ }
+ ArrayList<MediaItem> list = mediaSet.getMediaItem(index, 1);
+ return list.isEmpty() ? null : list.get(0);
+ }
+
+ private static class ShuffleSource implements SlideshowDataAdapter.SlideshowSource {
+ private static final int RETRY_COUNT = 5;
+ private final MediaSet mMediaSet;
+ private final Random mRandom = new Random();
+ private int mOrder[] = new int[0];
+ private final boolean mRepeat;
+ private long mSourceVersion = MediaSet.INVALID_DATA_VERSION;
+ private int mLastIndex = -1;
+
+ public ShuffleSource(MediaSet mediaSet, boolean repeat) {
+ mMediaSet = Utils.checkNotNull(mediaSet);
+ mRepeat = repeat;
+ }
+
+ @Override
+ public int findItemIndex(Path path, int hint) {
+ return hint;
+ }
+
+ @Override
+ public MediaItem getMediaItem(int index) {
+ if (!mRepeat && index >= mOrder.length) return null;
+ if (mOrder.length == 0) return null;
+ mLastIndex = mOrder[index % mOrder.length];
+ MediaItem item = findMediaItem(mMediaSet, mLastIndex);
+ for (int i = 0; i < RETRY_COUNT && item == null; ++i) {
+ Log.w(TAG, "fail to find image: " + mLastIndex);
+ mLastIndex = mRandom.nextInt(mOrder.length);
+ item = findMediaItem(mMediaSet, mLastIndex);
+ }
+ return item;
+ }
+
+ @Override
+ public long reload() {
+ long version = mMediaSet.reload();
+ if (version != mSourceVersion) {
+ mSourceVersion = version;
+ int count = mMediaSet.getTotalMediaItemCount();
+ if (count != mOrder.length) generateOrderArray(count);
+ }
+ return version;
+ }
+
+ private void generateOrderArray(int totalCount) {
+ if (mOrder.length != totalCount) {
+ mOrder = new int[totalCount];
+ for (int i = 0; i < totalCount; ++i) {
+ mOrder[i] = i;
+ }
+ }
+ for (int i = totalCount - 1; i > 0; --i) {
+ Utils.swap(mOrder, i, mRandom.nextInt(i + 1));
+ }
+ if (mOrder[0] == mLastIndex && totalCount > 1) {
+ Utils.swap(mOrder, 0, mRandom.nextInt(totalCount - 1) + 1);
+ }
+ }
+
+ @Override
+ public void addContentListener(ContentListener listener) {
+ mMediaSet.addContentListener(listener);
+ }
+
+ @Override
+ public void removeContentListener(ContentListener listener) {
+ mMediaSet.removeContentListener(listener);
+ }
+ }
+
+ private static class SequentialSource implements SlideshowDataAdapter.SlideshowSource {
+ private static final int DATA_SIZE = 32;
+
+ private ArrayList<MediaItem> mData = new ArrayList<MediaItem>();
+ private int mDataStart = 0;
+ private long mDataVersion = MediaObject.INVALID_DATA_VERSION;
+ private final MediaSet mMediaSet;
+ private final boolean mRepeat;
+
+ public SequentialSource(MediaSet mediaSet, boolean repeat) {
+ mMediaSet = mediaSet;
+ mRepeat = repeat;
+ }
+
+ @Override
+ public int findItemIndex(Path path, int hint) {
+ return mMediaSet.getIndexOfItem(path, hint);
+ }
+
+ @Override
+ public MediaItem getMediaItem(int index) {
+ int dataEnd = mDataStart + mData.size();
+
+ if (mRepeat) {
+ int count = mMediaSet.getMediaItemCount();
+ if (count == 0) return null;
+ index = index % count;
+ }
+ if (index < mDataStart || index >= dataEnd) {
+ mData = mMediaSet.getMediaItem(index, DATA_SIZE);
+ mDataStart = index;
+ dataEnd = index + mData.size();
+ }
+
+ return (index < mDataStart || index >= dataEnd) ? null : mData.get(index - mDataStart);
+ }
+
+ @Override
+ public long reload() {
+ long version = mMediaSet.reload();
+ if (version != mDataVersion) {
+ mDataVersion = version;
+ mData.clear();
+ }
+ return mDataVersion;
+ }
+
+ @Override
+ public void addContentListener(ContentListener listener) {
+ mMediaSet.addContentListener(listener);
+ }
+
+ @Override
+ public void removeContentListener(ContentListener listener) {
+ mMediaSet.removeContentListener(listener);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/StateManager.java b/src/com/android/gallery3d/app/StateManager.java
new file mode 100644
index 000000000..53c3fc228
--- /dev/null
+++ b/src/com/android/gallery3d/app/StateManager.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2010 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 android.app.Activity;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.camera.CameraActivity;
+import com.android.gallery3d.anim.StateTransitionAnimation;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.util.Stack;
+
+public class StateManager {
+ @SuppressWarnings("unused")
+ private static final String TAG = "StateManager";
+ private boolean mIsResumed = false;
+
+ private static final String KEY_MAIN = "activity-state";
+ private static final String KEY_DATA = "data";
+ private static final String KEY_STATE = "bundle";
+ private static final String KEY_CLASS = "class";
+
+ private AbstractGalleryActivity mActivity;
+ private Stack<StateEntry> mStack = new Stack<StateEntry>();
+ private ActivityState.ResultEntry mResult;
+
+ public StateManager(AbstractGalleryActivity activity) {
+ mActivity = activity;
+ }
+
+ public void startState(Class<? extends ActivityState> klass,
+ Bundle data) {
+ Log.v(TAG, "startState " + klass);
+ ActivityState state = null;
+ try {
+ state = klass.newInstance();
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ if (!mStack.isEmpty()) {
+ ActivityState top = getTopState();
+ top.transitionOnNextPause(top.getClass(), klass,
+ StateTransitionAnimation.Transition.Incoming);
+ if (mIsResumed) top.onPause();
+ }
+
+ UsageStatistics.onContentViewChanged(
+ UsageStatistics.COMPONENT_GALLERY,
+ klass.getSimpleName());
+ state.initialize(mActivity, data);
+
+ mStack.push(new StateEntry(data, state));
+ state.onCreate(data, null);
+ if (mIsResumed) state.resume();
+ }
+
+ public void startStateForResult(Class<? extends ActivityState> klass,
+ int requestCode, Bundle data) {
+ Log.v(TAG, "startStateForResult " + klass + ", " + requestCode);
+ ActivityState state = null;
+ try {
+ state = klass.newInstance();
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ state.initialize(mActivity, data);
+ state.mResult = new ActivityState.ResultEntry();
+ state.mResult.requestCode = requestCode;
+
+ if (!mStack.isEmpty()) {
+ ActivityState as = getTopState();
+ as.transitionOnNextPause(as.getClass(), klass,
+ StateTransitionAnimation.Transition.Incoming);
+ as.mReceivedResults = state.mResult;
+ if (mIsResumed) as.onPause();
+ } else {
+ mResult = state.mResult;
+ }
+ UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY,
+ klass.getSimpleName());
+ mStack.push(new StateEntry(data, state));
+ state.onCreate(data, null);
+ if (mIsResumed) state.resume();
+ }
+
+ public boolean createOptionsMenu(Menu menu) {
+ if (mStack.isEmpty()) {
+ return false;
+ } else {
+ return getTopState().onCreateActionBar(menu);
+ }
+ }
+
+ public void onConfigurationChange(Configuration config) {
+ for (StateEntry entry : mStack) {
+ entry.activityState.onConfigurationChanged(config);
+ }
+ }
+
+ public void resume() {
+ if (mIsResumed) return;
+ mIsResumed = true;
+ if (!mStack.isEmpty()) getTopState().resume();
+ }
+
+ public void pause() {
+ if (!mIsResumed) return;
+ mIsResumed = false;
+ if (!mStack.isEmpty()) getTopState().onPause();
+ }
+
+ public void notifyActivityResult(int requestCode, int resultCode, Intent data) {
+ getTopState().onStateResult(requestCode, resultCode, data);
+ }
+
+ public void clearActivityResult() {
+ if (!mStack.isEmpty()) {
+ getTopState().clearStateResult();
+ }
+ }
+
+ public int getStateCount() {
+ return mStack.size();
+ }
+
+ public boolean itemSelected(MenuItem item) {
+ if (!mStack.isEmpty()) {
+ if (getTopState().onItemSelected(item)) return true;
+ if (item.getItemId() == android.R.id.home) {
+ if (mStack.size() > 1) {
+ getTopState().onBackPressed();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void onBackPressed() {
+ if (!mStack.isEmpty()) {
+ getTopState().onBackPressed();
+ }
+ }
+
+ void finishState(ActivityState state) {
+ finishState(state, true);
+ }
+
+ public void clearTasks() {
+ // Remove all the states that are on top of the bottom PhotoPage state
+ while (mStack.size() > 1) {
+ mStack.pop().activityState.onDestroy();
+ }
+ }
+
+ void finishState(ActivityState state, boolean fireOnPause) {
+ // The finish() request could be rejected (only happens under Monkey),
+ // If it is rejected, we won't close the last page.
+ if (mStack.size() == 1) {
+ Activity activity = (Activity) mActivity.getAndroidContext();
+ if (mResult != null) {
+ activity.setResult(mResult.resultCode, mResult.resultData);
+ }
+ activity.finish();
+ if (!activity.isFinishing()) {
+ Log.w(TAG, "finish is rejected, keep the last state");
+ return;
+ }
+ Log.v(TAG, "no more state, finish activity");
+ }
+
+ Log.v(TAG, "finishState " + state);
+ if (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.
+ mStack.pop();
+ state.mIsFinishing = true;
+ ActivityState top = !mStack.isEmpty() ? mStack.peek().activityState : null;
+ if (mIsResumed && fireOnPause) {
+ if (top != null) {
+ state.transitionOnNextPause(state.getClass(), top.getClass(),
+ StateTransitionAnimation.Transition.Outgoing);
+ }
+ state.onPause();
+ }
+ mActivity.getGLRoot().setContentPane(null);
+ state.onDestroy();
+
+ if (top != null && mIsResumed) top.resume();
+ if (top != null) {
+ UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY,
+ top.getClass().getSimpleName());
+ }
+ }
+
+ public void switchState(ActivityState oldState,
+ Class<? extends ActivityState> klass, Bundle data) {
+ Log.v(TAG, "switchState " + oldState + ", " + klass);
+ if (oldState != mStack.peek().activityState) {
+ throw new IllegalArgumentException("The stateview to be finished"
+ + " is not at the top of the stack: " + oldState + ", "
+ + mStack.peek().activityState);
+ }
+ // Remove the top state.
+ mStack.pop();
+ if (!data.containsKey(PhotoPage.KEY_APP_BRIDGE)) {
+ // Do not do the fade out stuff when we are switching camera modes
+ oldState.transitionOnNextPause(oldState.getClass(), klass,
+ StateTransitionAnimation.Transition.Incoming);
+ }
+ if (mIsResumed) oldState.onPause();
+ oldState.onDestroy();
+
+ // Create new state.
+ ActivityState state = null;
+ try {
+ state = klass.newInstance();
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ state.initialize(mActivity, data);
+ mStack.push(new StateEntry(data, state));
+ state.onCreate(data, null);
+ if (mIsResumed) state.resume();
+ UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY,
+ klass.getSimpleName());
+ }
+
+ public void destroy() {
+ Log.v(TAG, "destroy");
+ while (!mStack.isEmpty()) {
+ mStack.pop().activityState.onDestroy();
+ }
+ mStack.clear();
+ }
+
+ @SuppressWarnings("unchecked")
+ public void restoreFromState(Bundle inState) {
+ Log.v(TAG, "restoreFromState");
+ Parcelable list[] = inState.getParcelableArray(KEY_MAIN);
+ ActivityState topState = null;
+ for (Parcelable parcelable : list) {
+ Bundle bundle = (Bundle) parcelable;
+ Class<? extends ActivityState> klass =
+ (Class<? extends ActivityState>) bundle.getSerializable(KEY_CLASS);
+
+ Bundle data = bundle.getBundle(KEY_DATA);
+ Bundle state = bundle.getBundle(KEY_STATE);
+
+ ActivityState activityState;
+ try {
+ Log.v(TAG, "restoreFromState " + klass);
+ activityState = klass.newInstance();
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ activityState.initialize(mActivity, data);
+ activityState.onCreate(data, state);
+ mStack.push(new StateEntry(data, activityState));
+ topState = activityState;
+ }
+ if (topState != null) {
+ UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY,
+ topState.getClass().getSimpleName());
+ }
+ }
+
+ public void saveState(Bundle outState) {
+ Log.v(TAG, "saveState");
+
+ Parcelable list[] = new Parcelable[mStack.size()];
+ int i = 0;
+ for (StateEntry entry : mStack) {
+ Bundle bundle = new Bundle();
+ bundle.putSerializable(KEY_CLASS, entry.activityState.getClass());
+ bundle.putBundle(KEY_DATA, entry.data);
+ Bundle state = new Bundle();
+ entry.activityState.onSaveState(state);
+ bundle.putBundle(KEY_STATE, state);
+ Log.v(TAG, "saveState " + entry.activityState.getClass());
+ list[i++] = bundle;
+ }
+ outState.putParcelableArray(KEY_MAIN, list);
+ }
+
+ public boolean hasStateClass(Class<? extends ActivityState> klass) {
+ for (StateEntry entry : mStack) {
+ if (klass.isInstance(entry.activityState)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public ActivityState getTopState() {
+ Utils.assertTrue(!mStack.isEmpty());
+ return mStack.peek().activityState;
+ }
+
+ private static class StateEntry {
+ public Bundle data;
+ public ActivityState activityState;
+
+ public StateEntry(Bundle data, ActivityState state) {
+ this.data = data;
+ this.activityState = state;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/StitchingChangeListener.java b/src/com/android/gallery3d/app/StitchingChangeListener.java
new file mode 100644
index 000000000..0b8c2d6d6
--- /dev/null
+++ b/src/com/android/gallery3d/app/StitchingChangeListener.java
@@ -0,0 +1,27 @@
+/*
+ * 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 android.net.Uri;
+
+public interface StitchingChangeListener {
+ public void onStitchingQueued(Uri uri);
+
+ public void onStitchingResult(Uri uri);
+
+ public void onStitchingProgress(Uri uri, int progress);
+}
diff --git a/src/com/android/gallery3d/app/TimeBar.java b/src/com/android/gallery3d/app/TimeBar.java
new file mode 100644
index 000000000..246346a56
--- /dev/null
+++ b/src/com/android/gallery3d/app/TimeBar.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2011 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 android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+
+/**
+ * The time bar view, which includes the current and total time, the progress
+ * bar, and the scrubber.
+ */
+public class TimeBar extends View {
+
+ public interface Listener {
+ void onScrubbingStart();
+
+ void onScrubbingMove(int time);
+
+ void onScrubbingEnd(int time, int start, int end);
+ }
+
+ // Padding around the scrubber to increase its touch target
+ private static final int SCRUBBER_PADDING_IN_DP = 10;
+
+ // The total padding, top plus bottom
+ private static final int V_PADDING_IN_DP = 30;
+
+ private static final int TEXT_SIZE_IN_DP = 14;
+
+ protected final Listener mListener;
+
+ // the bars we use for displaying the progress
+ protected final Rect mProgressBar;
+ protected final Rect mPlayedBar;
+
+ protected final Paint mProgressPaint;
+ protected final Paint mPlayedPaint;
+ protected final Paint mTimeTextPaint;
+
+ protected final Bitmap mScrubber;
+ protected int mScrubberPadding; // adds some touch tolerance around the
+ // scrubber
+
+ protected int mScrubberLeft;
+ protected int mScrubberTop;
+ protected int mScrubberCorrection;
+ protected boolean mScrubbing;
+ protected boolean mShowTimes;
+ protected boolean mShowScrubber;
+
+ protected int mTotalTime;
+ protected int mCurrentTime;
+
+ protected final Rect mTimeBounds;
+
+ protected int mVPaddingInPx;
+
+ public TimeBar(Context context, Listener listener) {
+ super(context);
+ mListener = Utils.checkNotNull(listener);
+
+ mShowTimes = true;
+ mShowScrubber = true;
+
+ mProgressBar = new Rect();
+ mPlayedBar = new Rect();
+
+ mProgressPaint = new Paint();
+ mProgressPaint.setColor(0xFF808080);
+ mPlayedPaint = new Paint();
+ mPlayedPaint.setColor(0xFFFFFFFF);
+
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ float textSizeInPx = metrics.density * TEXT_SIZE_IN_DP;
+ mTimeTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mTimeTextPaint.setColor(0xFFCECECE);
+ mTimeTextPaint.setTextSize(textSizeInPx);
+ mTimeTextPaint.setTextAlign(Paint.Align.CENTER);
+
+ mTimeBounds = new Rect();
+ mTimeTextPaint.getTextBounds("0:00:00", 0, 7, mTimeBounds);
+
+ mScrubber = BitmapFactory.decodeResource(getResources(), R.drawable.scrubber_knob);
+ mScrubberPadding = (int) (metrics.density * SCRUBBER_PADDING_IN_DP);
+
+ mVPaddingInPx = (int) (metrics.density * V_PADDING_IN_DP);
+ }
+
+ private void update() {
+ mPlayedBar.set(mProgressBar);
+
+ if (mTotalTime > 0) {
+ mPlayedBar.right =
+ mPlayedBar.left + (int) ((mProgressBar.width() * (long) mCurrentTime) / mTotalTime);
+ } else {
+ mPlayedBar.right = mProgressBar.left;
+ }
+
+ if (!mScrubbing) {
+ mScrubberLeft = mPlayedBar.right - mScrubber.getWidth() / 2;
+ }
+ invalidate();
+ }
+
+ /**
+ * @return the preferred height of this view, including invisible padding
+ */
+ public int getPreferredHeight() {
+ return mTimeBounds.height() + mVPaddingInPx + mScrubberPadding;
+ }
+
+ /**
+ * @return the height of the time bar, excluding invisible padding
+ */
+ public int getBarHeight() {
+ return mTimeBounds.height() + mVPaddingInPx;
+ }
+
+ public void setTime(int currentTime, int totalTime,
+ int trimStartTime, int trimEndTime) {
+ if (mCurrentTime == currentTime && mTotalTime == totalTime) {
+ return;
+ }
+ mCurrentTime = currentTime;
+ mTotalTime = totalTime;
+ update();
+ }
+
+ private boolean inScrubber(float x, float y) {
+ int scrubberRight = mScrubberLeft + mScrubber.getWidth();
+ int scrubberBottom = mScrubberTop + mScrubber.getHeight();
+ return mScrubberLeft - mScrubberPadding < x && x < scrubberRight + mScrubberPadding
+ && mScrubberTop - mScrubberPadding < y && y < scrubberBottom + mScrubberPadding;
+ }
+
+ private void clampScrubber() {
+ int half = mScrubber.getWidth() / 2;
+ int max = mProgressBar.right - half;
+ int min = mProgressBar.left - half;
+ mScrubberLeft = Math.min(max, Math.max(min, mScrubberLeft));
+ }
+
+ private int getScrubberTime() {
+ return (int) ((long) (mScrubberLeft + mScrubber.getWidth() / 2 - mProgressBar.left)
+ * mTotalTime / mProgressBar.width());
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int w = r - l;
+ int h = b - t;
+ if (!mShowTimes && !mShowScrubber) {
+ mProgressBar.set(0, 0, w, h);
+ } else {
+ int margin = mScrubber.getWidth() / 3;
+ if (mShowTimes) {
+ margin += mTimeBounds.width();
+ }
+ int progressY = (h + mScrubberPadding) / 2;
+ mScrubberTop = progressY - mScrubber.getHeight() / 2 + 1;
+ mProgressBar.set(
+ getPaddingLeft() + margin, progressY,
+ w - getPaddingRight() - margin, progressY + 4);
+ }
+ update();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // draw progress bars
+ canvas.drawRect(mProgressBar, mProgressPaint);
+ canvas.drawRect(mPlayedBar, mPlayedPaint);
+
+ // draw scrubber and timers
+ if (mShowScrubber) {
+ canvas.drawBitmap(mScrubber, mScrubberLeft, mScrubberTop, null);
+ }
+ if (mShowTimes) {
+ canvas.drawText(
+ stringForTime(mCurrentTime),
+ mTimeBounds.width() / 2 + getPaddingLeft(),
+ mTimeBounds.height() + mVPaddingInPx / 2 + mScrubberPadding + 1,
+ mTimeTextPaint);
+ canvas.drawText(
+ stringForTime(mTotalTime),
+ getWidth() - getPaddingRight() - mTimeBounds.width() / 2,
+ mTimeBounds.height() + mVPaddingInPx / 2 + mScrubberPadding + 1,
+ mTimeTextPaint);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mShowScrubber) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ mScrubberCorrection = inScrubber(x, y)
+ ? x - mScrubberLeft
+ : mScrubber.getWidth() / 2;
+ mScrubbing = true;
+ mListener.onScrubbingStart();
+ }
+ // fall-through
+ case MotionEvent.ACTION_MOVE: {
+ mScrubberLeft = x - mScrubberCorrection;
+ clampScrubber();
+ mCurrentTime = getScrubberTime();
+ mListener.onScrubbingMove(mCurrentTime);
+ invalidate();
+ return true;
+ }
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ mListener.onScrubbingEnd(getScrubberTime(), 0, 0);
+ mScrubbing = false;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ protected String stringForTime(long millis) {
+ int totalSeconds = (int) millis / 1000;
+ int seconds = totalSeconds % 60;
+ int minutes = (totalSeconds / 60) % 60;
+ int hours = totalSeconds / 3600;
+ if (hours > 0) {
+ return String.format("%d:%02d:%02d", hours, minutes, seconds).toString();
+ } else {
+ return String.format("%02d:%02d", minutes, seconds).toString();
+ }
+ }
+
+ public void setSeekable(boolean canSeek) {
+ mShowScrubber = canSeek;
+ }
+
+}
diff --git a/src/com/android/gallery3d/app/TransitionStore.java b/src/com/android/gallery3d/app/TransitionStore.java
new file mode 100644
index 000000000..aa38ed77e
--- /dev/null
+++ b/src/com/android/gallery3d/app/TransitionStore.java
@@ -0,0 +1,46 @@
+/*
+ * 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 java.util.HashMap;
+
+public class TransitionStore {
+ private HashMap<Object, Object> mStorage = new HashMap<Object, Object>();
+
+ public void put(Object key, Object value) {
+ mStorage.put(key, value);
+ }
+
+ public <T> void putIfNotPresent(Object key, T valueIfNull) {
+ mStorage.put(key, get(key, valueIfNull));
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T> T get(Object key) {
+ return (T) mStorage.get(key);
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T> T get(Object key, T valueIfNull) {
+ T value = (T) mStorage.get(key);
+ return value == null ? valueIfNull : value;
+ }
+
+ public void clear() {
+ mStorage.clear();
+ }
+}
diff --git a/src/com/android/gallery3d/app/TrimControllerOverlay.java b/src/com/android/gallery3d/app/TrimControllerOverlay.java
new file mode 100644
index 000000000..cae016626
--- /dev/null
+++ b/src/com/android/gallery3d/app/TrimControllerOverlay.java
@@ -0,0 +1,111 @@
+/*
+ * 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 android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.common.ApiHelper;
+
+/**
+ * The controller for the Trimming Video.
+ */
+public class TrimControllerOverlay extends CommonControllerOverlay {
+
+ public TrimControllerOverlay(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void createTimeBar(Context context) {
+ mTimeBar = new TrimTimeBar(context, this);
+ }
+
+ private void hidePlayButtonIfPlaying() {
+ if (mState == State.PLAYING) {
+ mPlayPauseReplayView.setVisibility(View.INVISIBLE);
+ }
+ if (ApiHelper.HAS_OBJECT_ANIMATION) {
+ mPlayPauseReplayView.setAlpha(1f);
+ }
+ }
+
+ @Override
+ public void showPlaying() {
+ super.showPlaying();
+ if (ApiHelper.HAS_OBJECT_ANIMATION) {
+ // Add animation to hide the play button while playing.
+ ObjectAnimator anim = ObjectAnimator.ofFloat(mPlayPauseReplayView, "alpha", 1f, 0f);
+ anim.setDuration(200);
+ anim.start();
+ anim.addListener(new AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ hidePlayButtonIfPlaying();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ hidePlayButtonIfPlaying();
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+ } else {
+ hidePlayButtonIfPlaying();
+ }
+ }
+
+ @Override
+ public void setTimes(int currentTime, int totalTime, int trimStartTime, int trimEndTime) {
+ mTimeBar.setTime(currentTime, totalTime, trimStartTime, trimEndTime);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (super.onTouchEvent(event)) {
+ return true;
+ }
+
+ // The special thing here is that the State.ENDED include both cases of
+ // the video completed and current == trimEnd. Both request a replay.
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (mState == State.PLAYING || mState == State.PAUSED) {
+ mListener.onPlayPause();
+ } else if (mState == State.ENDED) {
+ if (mCanReplay) {
+ mListener.onReplay();
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ break;
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/gallery3d/app/TrimTimeBar.java b/src/com/android/gallery3d/app/TrimTimeBar.java
new file mode 100644
index 000000000..f8dbc749e
--- /dev/null
+++ b/src/com/android/gallery3d/app/TrimTimeBar.java
@@ -0,0 +1,339 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.R;
+
+/**
+ * The trim time bar view, which includes the current and total time, the progress
+ * bar, and the scrubbers for current time, start and end time for trimming.
+ */
+public class TrimTimeBar extends TimeBar {
+
+ public static final int SCRUBBER_NONE = 0;
+ public static final int SCRUBBER_START = 1;
+ public static final int SCRUBBER_CURRENT = 2;
+ public static final int SCRUBBER_END = 3;
+
+ private int mPressedThumb = SCRUBBER_NONE;
+
+ // On touch event, the setting order is Scrubber Position -> Time ->
+ // PlayedBar. At the setTimes(), activity can update the Time directly, then
+ // PlayedBar will be updated too.
+ private int mTrimStartScrubberLeft;
+ private int mTrimEndScrubberLeft;
+
+ private int mTrimStartScrubberTop;
+ private int mTrimEndScrubberTop;
+
+ private int mTrimStartTime;
+ private int mTrimEndTime;
+
+ private final Bitmap mTrimStartScrubber;
+ private final Bitmap mTrimEndScrubber;
+ public TrimTimeBar(Context context, Listener listener) {
+ super(context, listener);
+
+ mTrimStartTime = 0;
+ mTrimEndTime = 0;
+ mTrimStartScrubberLeft = 0;
+ mTrimEndScrubberLeft = 0;
+ mTrimStartScrubberTop = 0;
+ mTrimEndScrubberTop = 0;
+
+ mTrimStartScrubber = BitmapFactory.decodeResource(getResources(),
+ R.drawable.text_select_handle_left);
+ mTrimEndScrubber = BitmapFactory.decodeResource(getResources(),
+ R.drawable.text_select_handle_right);
+ // Increase the size of this trimTimeBar, but minimize the scrubber
+ // touch padding since we have 3 scrubbers now.
+ mScrubberPadding = 0;
+ mVPaddingInPx = mVPaddingInPx * 3 / 2;
+ }
+
+ private int getBarPosFromTime(int time) {
+ return mProgressBar.left +
+ (int) ((mProgressBar.width() * (long) time) / mTotalTime);
+ }
+
+ private int trimStartScrubberTipOffset() {
+ return mTrimStartScrubber.getWidth() * 3 / 4;
+ }
+
+ private int trimEndScrubberTipOffset() {
+ return mTrimEndScrubber.getWidth() / 4;
+ }
+
+ // Based on all the time info (current, total, trimStart, trimEnd), we
+ // decide the playedBar size.
+ private void updatePlayedBarAndScrubberFromTime() {
+ // According to the Time, update the Played Bar
+ mPlayedBar.set(mProgressBar);
+ if (mTotalTime > 0) {
+ // set playedBar according to the trim time.
+ mPlayedBar.left = getBarPosFromTime(mTrimStartTime);
+ mPlayedBar.right = getBarPosFromTime(mCurrentTime);
+ if (!mScrubbing) {
+ mScrubberLeft = mPlayedBar.right - mScrubber.getWidth() / 2;
+ mTrimStartScrubberLeft = mPlayedBar.left - trimStartScrubberTipOffset();
+ mTrimEndScrubberLeft = getBarPosFromTime(mTrimEndTime)
+ - trimEndScrubberTipOffset();
+ }
+ } else {
+ // If the video is not prepared, just show the scrubber at the end
+ // of progressBar
+ mPlayedBar.right = mProgressBar.left;
+ mScrubberLeft = mProgressBar.left - mScrubber.getWidth() / 2;
+ mTrimStartScrubberLeft = mProgressBar.left - trimStartScrubberTipOffset();
+ mTrimEndScrubberLeft = mProgressBar.right - trimEndScrubberTipOffset();
+ }
+ }
+
+ private void initTrimTimeIfNeeded() {
+ if (mTotalTime > 0 && mTrimEndTime == 0) {
+ mTrimEndTime = mTotalTime;
+ }
+ }
+
+ private void update() {
+ initTrimTimeIfNeeded();
+ updatePlayedBarAndScrubberFromTime();
+ invalidate();
+ }
+
+ @Override
+ public void setTime(int currentTime, int totalTime,
+ int trimStartTime, int trimEndTime) {
+ if (mCurrentTime == currentTime && mTotalTime == totalTime
+ && mTrimStartTime == trimStartTime && mTrimEndTime == trimEndTime) {
+ return;
+ }
+ mCurrentTime = currentTime;
+ mTotalTime = totalTime;
+ mTrimStartTime = trimStartTime;
+ mTrimEndTime = trimEndTime;
+ update();
+ }
+
+ private int whichScrubber(float x, float y) {
+ if (inScrubber(x, y, mTrimStartScrubberLeft, mTrimStartScrubberTop, mTrimStartScrubber)) {
+ return SCRUBBER_START;
+ } else if (inScrubber(x, y, mTrimEndScrubberLeft, mTrimEndScrubberTop, mTrimEndScrubber)) {
+ return SCRUBBER_END;
+ } else if (inScrubber(x, y, mScrubberLeft, mScrubberTop, mScrubber)) {
+ return SCRUBBER_CURRENT;
+ }
+ return SCRUBBER_NONE;
+ }
+
+ private boolean inScrubber(float x, float y, int startX, int startY, Bitmap scrubber) {
+ int scrubberRight = startX + scrubber.getWidth();
+ int scrubberBottom = startY + scrubber.getHeight();
+ return startX < x && x < scrubberRight && startY < y && y < scrubberBottom;
+ }
+
+ private int clampScrubber(int scrubberLeft, int offset, int lowerBound, int upperBound) {
+ int max = upperBound - offset;
+ int min = lowerBound - offset;
+ return Math.min(max, Math.max(min, scrubberLeft));
+ }
+
+ private int getScrubberTime(int scrubberLeft, int offset) {
+ return (int) ((long) (scrubberLeft + offset - mProgressBar.left)
+ * mTotalTime / mProgressBar.width());
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int w = r - l;
+ int h = b - t;
+ if (!mShowTimes && !mShowScrubber) {
+ mProgressBar.set(0, 0, w, h);
+ } else {
+ int margin = mScrubber.getWidth() / 3;
+ if (mShowTimes) {
+ margin += mTimeBounds.width();
+ }
+ int progressY = h / 4;
+ int scrubberY = progressY - mScrubber.getHeight() / 2 + 1;
+ mScrubberTop = scrubberY;
+ mTrimStartScrubberTop = progressY;
+ mTrimEndScrubberTop = progressY;
+ mProgressBar.set(
+ getPaddingLeft() + margin, progressY,
+ w - getPaddingRight() - margin, progressY + 4);
+ }
+ update();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // draw progress bars
+ canvas.drawRect(mProgressBar, mProgressPaint);
+ canvas.drawRect(mPlayedBar, mPlayedPaint);
+
+ if (mShowTimes) {
+ canvas.drawText(
+ stringForTime(mCurrentTime),
+ mTimeBounds.width() / 2 + getPaddingLeft(),
+ mTimeBounds.height() / 2 + mTrimStartScrubberTop,
+ mTimeTextPaint);
+ canvas.drawText(
+ stringForTime(mTotalTime),
+ getWidth() - getPaddingRight() - mTimeBounds.width() / 2,
+ mTimeBounds.height() / 2 + mTrimStartScrubberTop,
+ mTimeTextPaint);
+ }
+
+ // draw extra scrubbers
+ if (mShowScrubber) {
+ canvas.drawBitmap(mScrubber, mScrubberLeft, mScrubberTop, null);
+ canvas.drawBitmap(mTrimStartScrubber, mTrimStartScrubberLeft,
+ mTrimStartScrubberTop, null);
+ canvas.drawBitmap(mTrimEndScrubber, mTrimEndScrubberLeft,
+ mTrimEndScrubberTop, null);
+ }
+ }
+
+ private void updateTimeFromPos() {
+ mCurrentTime = getScrubberTime(mScrubberLeft, mScrubber.getWidth() / 2);
+ mTrimStartTime = getScrubberTime(mTrimStartScrubberLeft, trimStartScrubberTipOffset());
+ mTrimEndTime = getScrubberTime(mTrimEndScrubberLeft, trimEndScrubberTipOffset());
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mShowScrubber) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mPressedThumb = whichScrubber(x, y);
+ switch (mPressedThumb) {
+ case SCRUBBER_NONE:
+ break;
+ case SCRUBBER_CURRENT:
+ mScrubbing = true;
+ mScrubberCorrection = x - mScrubberLeft;
+ break;
+ case SCRUBBER_START:
+ mScrubbing = true;
+ mScrubberCorrection = x - mTrimStartScrubberLeft;
+ break;
+ case SCRUBBER_END:
+ mScrubbing = true;
+ mScrubberCorrection = x - mTrimEndScrubberLeft;
+ break;
+ }
+ if (mScrubbing == true) {
+ mListener.onScrubbingStart();
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (mScrubbing) {
+ int seekToTime = -1;
+ int lowerBound = mTrimStartScrubberLeft + trimStartScrubberTipOffset();
+ int upperBound = mTrimEndScrubberLeft + trimEndScrubberTipOffset();
+ switch (mPressedThumb) {
+ case SCRUBBER_CURRENT:
+ mScrubberLeft = x - mScrubberCorrection;
+ mScrubberLeft =
+ clampScrubber(mScrubberLeft,
+ mScrubber.getWidth() / 2,
+ lowerBound, upperBound);
+ seekToTime = getScrubberTime(mScrubberLeft,
+ mScrubber.getWidth() / 2);
+ break;
+ case SCRUBBER_START:
+ mTrimStartScrubberLeft = x - mScrubberCorrection;
+ // Limit start <= end
+ if (mTrimStartScrubberLeft > mTrimEndScrubberLeft) {
+ mTrimStartScrubberLeft = mTrimEndScrubberLeft;
+ }
+ lowerBound = mProgressBar.left;
+ mTrimStartScrubberLeft =
+ clampScrubber(mTrimStartScrubberLeft,
+ trimStartScrubberTipOffset(),
+ lowerBound, upperBound);
+ seekToTime = getScrubberTime(mTrimStartScrubberLeft,
+ trimStartScrubberTipOffset());
+ break;
+ case SCRUBBER_END:
+ mTrimEndScrubberLeft = x - mScrubberCorrection;
+ upperBound = mProgressBar.right;
+ mTrimEndScrubberLeft =
+ clampScrubber(mTrimEndScrubberLeft,
+ trimEndScrubberTipOffset(),
+ lowerBound, upperBound);
+ seekToTime = getScrubberTime(mTrimEndScrubberLeft,
+ trimEndScrubberTipOffset());
+ break;
+ }
+ updateTimeFromPos();
+ updatePlayedBarAndScrubberFromTime();
+ if (seekToTime != -1) {
+ mListener.onScrubbingMove(seekToTime);
+ }
+ invalidate();
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ if (mScrubbing) {
+ int seekToTime = 0;
+ switch (mPressedThumb) {
+ case SCRUBBER_CURRENT:
+ seekToTime = getScrubberTime(mScrubberLeft,
+ mScrubber.getWidth() / 2);
+ break;
+ case SCRUBBER_START:
+ seekToTime = getScrubberTime(mTrimStartScrubberLeft,
+ trimStartScrubberTipOffset());
+ mScrubberLeft = mTrimStartScrubberLeft +
+ trimStartScrubberTipOffset() - mScrubber.getWidth() / 2;
+ break;
+ case SCRUBBER_END:
+ seekToTime = getScrubberTime(mTrimEndScrubberLeft,
+ trimEndScrubberTipOffset());
+ mScrubberLeft = mTrimEndScrubberLeft +
+ trimEndScrubberTipOffset() - mScrubber.getWidth() / 2;
+ break;
+ }
+ updateTimeFromPos();
+ mListener.onScrubbingEnd(seekToTime,
+ getScrubberTime(mTrimStartScrubberLeft,
+ trimStartScrubberTipOffset()),
+ getScrubberTime(mTrimEndScrubberLeft, trimEndScrubberTipOffset()));
+ mScrubbing = false;
+ mPressedThumb = SCRUBBER_NONE;
+ return true;
+ }
+ break;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/app/TrimVideo.java b/src/com/android/gallery3d/app/TrimVideo.java
new file mode 100644
index 000000000..1e7728162
--- /dev/null
+++ b/src/com/android/gallery3d/app/TrimVideo.java
@@ -0,0 +1,337 @@
+/*
+ * 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 android.app.ActionBar;
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.MediaStore;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.VideoView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.util.SaveVideoFileInfo;
+import com.android.gallery3d.util.SaveVideoFileUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+public class TrimVideo extends Activity implements
+ MediaPlayer.OnErrorListener,
+ MediaPlayer.OnCompletionListener,
+ ControllerOverlay.Listener {
+
+ private VideoView mVideoView;
+ private TextView mSaveVideoTextView;
+ private TrimControllerOverlay mController;
+ private Context mContext;
+ private Uri mUri;
+ private final Handler mHandler = new Handler();
+ public static final String TRIM_ACTION = "com.android.camera.action.TRIM";
+
+ public ProgressDialog mProgress;
+
+ private int mTrimStartTime = 0;
+ private int mTrimEndTime = 0;
+ private int mVideoPosition = 0;
+ public static final String KEY_TRIM_START = "trim_start";
+ public static final String KEY_TRIM_END = "trim_end";
+ public static final String KEY_VIDEO_POSITION = "video_pos";
+ private boolean mHasPaused = false;
+
+ private String mSrcVideoPath = null;
+ private static final String TIME_STAMP_NAME = "'TRIM'_yyyyMMdd_HHmmss";
+ private SaveVideoFileInfo mDstFileInfo = null;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mContext = getApplicationContext();
+ super.onCreate(savedInstanceState);
+
+ requestWindowFeature(Window.FEATURE_ACTION_BAR);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+
+ ActionBar actionBar = getActionBar();
+ int displayOptions = ActionBar.DISPLAY_SHOW_HOME;
+ actionBar.setDisplayOptions(0, displayOptions);
+ displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM;
+ actionBar.setDisplayOptions(displayOptions, displayOptions);
+ actionBar.setCustomView(R.layout.trim_menu);
+
+ mSaveVideoTextView = (TextView) findViewById(R.id.start_trim);
+ mSaveVideoTextView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ trimVideo();
+ }
+ });
+ mSaveVideoTextView.setEnabled(false);
+
+ Intent intent = getIntent();
+ mUri = intent.getData();
+ mSrcVideoPath = intent.getStringExtra(PhotoPage.KEY_MEDIA_ITEM_PATH);
+ setContentView(R.layout.trim_view);
+ View rootView = findViewById(R.id.trim_view_root);
+
+ mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
+
+ mController = new TrimControllerOverlay(mContext);
+ ((ViewGroup) rootView).addView(mController.getView());
+ mController.setListener(this);
+ mController.setCanReplay(true);
+
+ mVideoView.setOnErrorListener(this);
+ mVideoView.setOnCompletionListener(this);
+ mVideoView.setVideoURI(mUri);
+
+ playVideo();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mHasPaused) {
+ mVideoView.seekTo(mVideoPosition);
+ mVideoView.resume();
+ mHasPaused = false;
+ }
+ mHandler.post(mProgressChecker);
+ }
+
+ @Override
+ public void onPause() {
+ mHasPaused = true;
+ mHandler.removeCallbacksAndMessages(null);
+ mVideoPosition = mVideoView.getCurrentPosition();
+ mVideoView.suspend();
+ super.onPause();
+ }
+
+ @Override
+ public void onStop() {
+ if (mProgress != null) {
+ mProgress.dismiss();
+ mProgress = null;
+ }
+ super.onStop();
+ }
+
+ @Override
+ public void onDestroy() {
+ mVideoView.stopPlayback();
+ super.onDestroy();
+ }
+
+ private final Runnable mProgressChecker = new Runnable() {
+ @Override
+ public void run() {
+ int pos = setProgress();
+ mHandler.postDelayed(mProgressChecker, 200 - (pos % 200));
+ }
+ };
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ savedInstanceState.putInt(KEY_TRIM_START, mTrimStartTime);
+ savedInstanceState.putInt(KEY_TRIM_END, mTrimEndTime);
+ savedInstanceState.putInt(KEY_VIDEO_POSITION, mVideoPosition);
+ super.onSaveInstanceState(savedInstanceState);
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ mTrimStartTime = savedInstanceState.getInt(KEY_TRIM_START, 0);
+ mTrimEndTime = savedInstanceState.getInt(KEY_TRIM_END, 0);
+ mVideoPosition = savedInstanceState.getInt(KEY_VIDEO_POSITION, 0);
+ }
+
+ // This updates the time bar display (if necessary). It is called by
+ // mProgressChecker and also from places where the time bar needs
+ // to be updated immediately.
+ private int setProgress() {
+ mVideoPosition = mVideoView.getCurrentPosition();
+ // If the video position is smaller than the starting point of trimming,
+ // correct it.
+ if (mVideoPosition < mTrimStartTime) {
+ mVideoView.seekTo(mTrimStartTime);
+ mVideoPosition = mTrimStartTime;
+ }
+ // If the position is bigger than the end point of trimming, show the
+ // replay button and pause.
+ if (mVideoPosition >= mTrimEndTime && mTrimEndTime > 0) {
+ if (mVideoPosition > mTrimEndTime) {
+ mVideoView.seekTo(mTrimEndTime);
+ mVideoPosition = mTrimEndTime;
+ }
+ mController.showEnded();
+ mVideoView.pause();
+ }
+
+ int duration = mVideoView.getDuration();
+ if (duration > 0 && mTrimEndTime == 0) {
+ mTrimEndTime = duration;
+ }
+ mController.setTimes(mVideoPosition, duration, mTrimStartTime, mTrimEndTime);
+ return mVideoPosition;
+ }
+
+ private void playVideo() {
+ mVideoView.start();
+ mController.showPlaying();
+ setProgress();
+ }
+
+ private void pauseVideo() {
+ mVideoView.pause();
+ mController.showPaused();
+ }
+
+
+ private boolean isModified() {
+ int delta = mTrimEndTime - mTrimStartTime;
+
+ // Considering that we only trim at sync frame, we don't want to trim
+ // when the time interval is too short or too close to the origin.
+ if (delta < 100 || Math.abs(mVideoView.getDuration() - delta) < 100) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ private void trimVideo() {
+
+ mDstFileInfo = SaveVideoFileUtils.getDstMp4FileInfo(TIME_STAMP_NAME,
+ getContentResolver(), mUri, getString(R.string.folder_download));
+ final File mSrcFile = new File(mSrcVideoPath);
+
+ showProgressDialog();
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ VideoUtils.startTrim(mSrcFile, mDstFileInfo.mFile,
+ mTrimStartTime, mTrimEndTime);
+ // Update the database for adding a new video file.
+ SaveVideoFileUtils.insertContent(mDstFileInfo,
+ getContentResolver(), mUri);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ // After trimming is done, trigger the UI changed.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(getApplicationContext(),
+ getString(R.string.save_into, mDstFileInfo.mFolderName),
+ Toast.LENGTH_SHORT)
+ .show();
+ // TODO: change trimming into a service to avoid
+ // this progressDialog and add notification properly.
+ if (mProgress != null) {
+ mProgress.dismiss();
+ mProgress = null;
+ // Show the result only when the activity not stopped.
+ Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
+ intent.setDataAndType(Uri.fromFile(mDstFileInfo.mFile), "video/*");
+ intent.putExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, false);
+ startActivity(intent);
+ finish();
+ }
+ }
+ });
+ }
+ }).start();
+ }
+
+ private void showProgressDialog() {
+ // create a background thread to trim the video.
+ // and show the progress.
+ mProgress = new ProgressDialog(this);
+ mProgress.setTitle(getString(R.string.trimming));
+ mProgress.setMessage(getString(R.string.please_wait));
+ // TODO: make this cancelable.
+ mProgress.setCancelable(false);
+ mProgress.setCanceledOnTouchOutside(false);
+ mProgress.show();
+ }
+
+ @Override
+ public void onPlayPause() {
+ if (mVideoView.isPlaying()) {
+ pauseVideo();
+ } else {
+ playVideo();
+ }
+ }
+
+ @Override
+ public void onSeekStart() {
+ pauseVideo();
+ }
+
+ @Override
+ public void onSeekMove(int time) {
+ mVideoView.seekTo(time);
+ }
+
+ @Override
+ public void onSeekEnd(int time, int start, int end) {
+ mVideoView.seekTo(time);
+ mTrimStartTime = start;
+ mTrimEndTime = end;
+ setProgress();
+ // Enable save if there's modifications
+ mSaveVideoTextView.setEnabled(isModified());
+ }
+
+ @Override
+ public void onShown() {
+ }
+
+ @Override
+ public void onHidden() {
+ }
+
+ @Override
+ public void onReplay() {
+ mVideoView.seekTo(mTrimStartTime);
+ playVideo();
+ }
+
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ mController.showEnded();
+ }
+
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/app/VideoUtils.java b/src/com/android/gallery3d/app/VideoUtils.java
new file mode 100644
index 000000000..a3c3ef273
--- /dev/null
+++ b/src/com/android/gallery3d/app/VideoUtils.java
@@ -0,0 +1,328 @@
+/*
+ * 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.
+ */
+
+// Modified example based on mp4parser google code open source project.
+// http://code.google.com/p/mp4parser/source/browse/trunk/examples/src/main/java/com/googlecode/mp4parser/ShortenExample.java
+
+package com.android.gallery3d.app;
+
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaMuxer;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.util.SaveVideoFileInfo;
+import com.coremedia.iso.IsoFile;
+import com.coremedia.iso.boxes.TimeToSampleBox;
+import com.googlecode.mp4parser.authoring.Movie;
+import com.googlecode.mp4parser.authoring.Track;
+import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
+import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
+import com.googlecode.mp4parser.authoring.tracks.CroppedTrack;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+public class VideoUtils {
+ private static final String LOGTAG = "VideoUtils";
+ private static final int DEFAULT_BUFFER_SIZE = 1 * 1024 * 1024;
+
+ /**
+ * Remove the sound track.
+ */
+ public static void startMute(String filePath, SaveVideoFileInfo dstFileInfo)
+ throws IOException {
+ if (ApiHelper.HAS_MEDIA_MUXER) {
+ genVideoUsingMuxer(filePath, dstFileInfo.mFile.getPath(), -1, -1,
+ false, true);
+ } else {
+ startMuteUsingMp4Parser(filePath, dstFileInfo);
+ }
+ }
+
+ /**
+ * Shortens/Crops tracks
+ */
+ public static void startTrim(File src, File dst, int startMs, int endMs)
+ throws IOException {
+ if (ApiHelper.HAS_MEDIA_MUXER) {
+ genVideoUsingMuxer(src.getPath(), dst.getPath(), startMs, endMs,
+ true, true);
+ } else {
+ trimUsingMp4Parser(src, dst, startMs, endMs);
+ }
+ }
+
+ private static void startMuteUsingMp4Parser(String filePath,
+ SaveVideoFileInfo dstFileInfo) throws FileNotFoundException, IOException {
+ File dst = dstFileInfo.mFile;
+ File src = new File(filePath);
+ RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r");
+ Movie movie = MovieCreator.build(randomAccessFile.getChannel());
+
+ // remove all tracks we will create new tracks from the old
+ List<Track> tracks = movie.getTracks();
+ movie.setTracks(new LinkedList<Track>());
+
+ for (Track track : tracks) {
+ if (track.getHandler().equals("vide")) {
+ movie.addTrack(track);
+ }
+ }
+ writeMovieIntoFile(dst, movie);
+ randomAccessFile.close();
+ }
+
+ private static void writeMovieIntoFile(File dst, Movie movie)
+ throws IOException {
+ if (!dst.exists()) {
+ dst.createNewFile();
+ }
+
+ IsoFile out = new DefaultMp4Builder().build(movie);
+ FileOutputStream fos = new FileOutputStream(dst);
+ FileChannel fc = fos.getChannel();
+ out.getBox(fc); // This one build up the memory.
+
+ fc.close();
+ fos.close();
+ }
+
+ /**
+ * @param srcPath the path of source video file.
+ * @param dstPath the path of destination video file.
+ * @param startMs starting time in milliseconds for trimming. Set to
+ * negative if starting from beginning.
+ * @param endMs end time for trimming in milliseconds. Set to negative if
+ * no trimming at the end.
+ * @param useAudio true if keep the audio track from the source.
+ * @param useVideo true if keep the video track from the source.
+ * @throws IOException
+ */
+ private static void genVideoUsingMuxer(String srcPath, String dstPath,
+ int startMs, int endMs, boolean useAudio, boolean useVideo)
+ throws IOException {
+ // Set up MediaExtractor to read from the source.
+ MediaExtractor extractor = new MediaExtractor();
+ extractor.setDataSource(srcPath);
+
+ int trackCount = extractor.getTrackCount();
+
+ // Set up MediaMuxer for the destination.
+ MediaMuxer muxer;
+ muxer = new MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+
+ // Set up the tracks and retrieve the max buffer size for selected
+ // tracks.
+ HashMap<Integer, Integer> indexMap = new HashMap<Integer,
+ Integer>(trackCount);
+ int bufferSize = -1;
+ for (int i = 0; i < trackCount; i++) {
+ MediaFormat format = extractor.getTrackFormat(i);
+ String mime = format.getString(MediaFormat.KEY_MIME);
+
+ boolean selectCurrentTrack = false;
+
+ if (mime.startsWith("audio/") && useAudio) {
+ selectCurrentTrack = true;
+ } else if (mime.startsWith("video/") && useVideo) {
+ selectCurrentTrack = true;
+ }
+
+ if (selectCurrentTrack) {
+ extractor.selectTrack(i);
+ int dstIndex = muxer.addTrack(format);
+ indexMap.put(i, dstIndex);
+ if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
+ int newSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
+ bufferSize = newSize > bufferSize ? newSize : bufferSize;
+ }
+ }
+ }
+
+ if (bufferSize < 0) {
+ bufferSize = DEFAULT_BUFFER_SIZE;
+ }
+
+ // Set up the orientation and starting time for extractor.
+ MediaMetadataRetriever retrieverSrc = new MediaMetadataRetriever();
+ retrieverSrc.setDataSource(srcPath);
+ String degreesString = retrieverSrc.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
+ if (degreesString != null) {
+ int degrees = Integer.parseInt(degreesString);
+ if (degrees >= 0) {
+ muxer.setOrientationHint(degrees);
+ }
+ }
+
+ if (startMs > 0) {
+ extractor.seekTo(startMs * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
+ }
+
+ // Copy the samples from MediaExtractor to MediaMuxer. We will loop
+ // for copying each sample and stop when we get to the end of the source
+ // file or exceed the end time of the trimming.
+ int offset = 0;
+ int trackIndex = -1;
+ ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize);
+ BufferInfo bufferInfo = new BufferInfo();
+
+ muxer.start();
+ while (true) {
+ bufferInfo.offset = offset;
+ bufferInfo.size = extractor.readSampleData(dstBuf, offset);
+ if (bufferInfo.size < 0) {
+ Log.d(LOGTAG, "Saw input EOS.");
+ bufferInfo.size = 0;
+ break;
+ } else {
+ bufferInfo.presentationTimeUs = extractor.getSampleTime();
+ if (endMs > 0 && bufferInfo.presentationTimeUs > (endMs * 1000)) {
+ Log.d(LOGTAG, "The current sample is over the trim end time.");
+ break;
+ } else {
+ bufferInfo.flags = extractor.getSampleFlags();
+ trackIndex = extractor.getSampleTrackIndex();
+
+ muxer.writeSampleData(indexMap.get(trackIndex), dstBuf,
+ bufferInfo);
+ extractor.advance();
+ }
+ }
+ }
+
+ muxer.stop();
+ muxer.release();
+ return;
+ }
+
+ private static void trimUsingMp4Parser(File src, File dst, int startMs, int endMs)
+ throws FileNotFoundException, IOException {
+ RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r");
+ Movie movie = MovieCreator.build(randomAccessFile.getChannel());
+
+ // remove all tracks we will create new tracks from the old
+ List<Track> tracks = movie.getTracks();
+ movie.setTracks(new LinkedList<Track>());
+
+ double startTime = startMs / 1000;
+ double endTime = endMs / 1000;
+
+ boolean timeCorrected = false;
+
+ // Here we try to find a track that has sync samples. Since we can only
+ // start decoding at such a sample we SHOULD make sure that the start of
+ // the new fragment is exactly such a frame.
+ for (Track track : tracks) {
+ if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) {
+ if (timeCorrected) {
+ // This exception here could be a false positive in case we
+ // have multiple tracks with sync samples at exactly the
+ // same positions. E.g. a single movie containing multiple
+ // qualities of the same video (Microsoft Smooth Streaming
+ // file)
+ throw new RuntimeException(
+ "The startTime has already been corrected by" +
+ " another track with SyncSample. Not Supported.");
+ }
+ startTime = correctTimeToSyncSample(track, startTime, false);
+ endTime = correctTimeToSyncSample(track, endTime, true);
+ timeCorrected = true;
+ }
+ }
+
+ for (Track track : tracks) {
+ long currentSample = 0;
+ double currentTime = 0;
+ long startSample = -1;
+ long endSample = -1;
+
+ for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) {
+ TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i);
+ for (int j = 0; j < entry.getCount(); j++) {
+ // entry.getDelta() is the amount of time the current sample
+ // covers.
+
+ if (currentTime <= startTime) {
+ // current sample is still before the new starttime
+ startSample = currentSample;
+ }
+ if (currentTime <= endTime) {
+ // current sample is after the new start time and still
+ // before the new endtime
+ endSample = currentSample;
+ } else {
+ // current sample is after the end of the cropped video
+ break;
+ }
+ currentTime += (double) entry.getDelta()
+ / (double) track.getTrackMetaData().getTimescale();
+ currentSample++;
+ }
+ }
+ movie.addTrack(new CroppedTrack(track, startSample, endSample));
+ }
+ writeMovieIntoFile(dst, movie);
+ randomAccessFile.close();
+ }
+
+ private static double correctTimeToSyncSample(Track track, double cutHere,
+ boolean next) {
+ double[] timeOfSyncSamples = new double[track.getSyncSamples().length];
+ long currentSample = 0;
+ double currentTime = 0;
+ for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) {
+ TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i);
+ for (int j = 0; j < entry.getCount(); j++) {
+ if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) {
+ // samples always start with 1 but we start with zero
+ // therefore +1
+ timeOfSyncSamples[Arrays.binarySearch(
+ track.getSyncSamples(), currentSample + 1)] = currentTime;
+ }
+ currentTime += (double) entry.getDelta()
+ / (double) track.getTrackMetaData().getTimescale();
+ currentSample++;
+ }
+ }
+ double previous = 0;
+ for (double timeOfSyncSample : timeOfSyncSamples) {
+ if (timeOfSyncSample > cutHere) {
+ if (next) {
+ return timeOfSyncSample;
+ } else {
+ return previous;
+ }
+ }
+ previous = timeOfSyncSample;
+ }
+ return timeOfSyncSamples[timeOfSyncSamples.length - 1];
+ }
+
+}
diff --git a/src/com/android/gallery3d/app/Wallpaper.java b/src/com/android/gallery3d/app/Wallpaper.java
new file mode 100644
index 000000000..b0a26c236
--- /dev/null
+++ b/src/com/android/gallery3d/app/Wallpaper.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2007 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 android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Point;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.Display;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.filtershow.crop.CropExtras;
+
+/**
+ * Wallpaper picker for the gallery application. This just redirects to the
+ * standard pick action.
+ */
+public class Wallpaper extends Activity {
+ @SuppressWarnings("unused")
+ private static final String TAG = "Wallpaper";
+
+ private static final String IMAGE_TYPE = "image/*";
+ private static final String KEY_STATE = "activity-state";
+ private static final String KEY_PICKED_ITEM = "picked-item";
+
+ private static final int STATE_INIT = 0;
+ private static final int STATE_PHOTO_PICKED = 1;
+
+ private int mState = STATE_INIT;
+ private Uri mPickedItem;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ if (bundle != null) {
+ mState = bundle.getInt(KEY_STATE);
+ mPickedItem = (Uri) bundle.getParcelable(KEY_PICKED_ITEM);
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle saveState) {
+ saveState.putInt(KEY_STATE, mState);
+ if (mPickedItem != null) {
+ saveState.putParcelable(KEY_PICKED_ITEM, mPickedItem);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
+ private Point getDefaultDisplaySize(Point size) {
+ Display d = getWindowManager().getDefaultDisplay();
+ if (Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.HONEYCOMB_MR2) {
+ d.getSize(size);
+ } else {
+ size.set(d.getWidth(), d.getHeight());
+ }
+ return size;
+ }
+
+ @SuppressWarnings("fallthrough")
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Intent intent = getIntent();
+ switch (mState) {
+ case STATE_INIT: {
+ mPickedItem = intent.getData();
+ if (mPickedItem == null) {
+ Intent request = new Intent(Intent.ACTION_GET_CONTENT)
+ .setClass(this, DialogPicker.class)
+ .setType(IMAGE_TYPE);
+ startActivityForResult(request, STATE_PHOTO_PICKED);
+ return;
+ }
+ mState = STATE_PHOTO_PICKED;
+ // fall-through
+ }
+ case STATE_PHOTO_PICKED: {
+ int width = getWallpaperDesiredMinimumWidth();
+ int height = getWallpaperDesiredMinimumHeight();
+ Point size = getDefaultDisplaySize(new Point());
+ float spotlightX = (float) size.x / width;
+ float spotlightY = (float) size.y / height;
+ Intent request = new Intent(CropActivity.CROP_ACTION)
+ .setDataAndType(mPickedItem, IMAGE_TYPE)
+ .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
+ .putExtra(CropExtras.KEY_OUTPUT_X, width)
+ .putExtra(CropExtras.KEY_OUTPUT_Y, height)
+ .putExtra(CropExtras.KEY_ASPECT_X, width)
+ .putExtra(CropExtras.KEY_ASPECT_Y, height)
+ .putExtra(CropExtras.KEY_SPOTLIGHT_X, spotlightX)
+ .putExtra(CropExtras.KEY_SPOTLIGHT_Y, spotlightY)
+ .putExtra(CropExtras.KEY_SCALE, true)
+ .putExtra(CropExtras.KEY_SCALE_UP_IF_NEEDED, true)
+ .putExtra(CropExtras.KEY_SET_AS_WALLPAPER, true);
+ startActivity(request);
+ finish();
+ }
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode != RESULT_OK) {
+ setResult(resultCode);
+ finish();
+ return;
+ }
+ mState = requestCode;
+ if (mState == STATE_PHOTO_PICKED) {
+ mPickedItem = data.getData();
+ }
+
+ // onResume() would be called next
+ }
+}