diff options
Diffstat (limited to 'src/com/android/gallery3d/app')
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 + } +} |