summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/gallery3d/app')
-rw-r--r--src/com/android/gallery3d/app/AbstractGalleryActivity.java198
-rw-r--r--src/com/android/gallery3d/app/ActivityState.java136
-rw-r--r--src/com/android/gallery3d/app/AlbumDataAdapter.java367
-rw-r--r--src/com/android/gallery3d/app/AlbumPage.java602
-rw-r--r--src/com/android/gallery3d/app/AlbumPicker.java68
-rw-r--r--src/com/android/gallery3d/app/AlbumSetDataAdapter.java384
-rw-r--r--src/com/android/gallery3d/app/AlbumSetPage.java586
-rw-r--r--src/com/android/gallery3d/app/Config.java140
-rw-r--r--src/com/android/gallery3d/app/CropImage.java850
-rw-r--r--src/com/android/gallery3d/app/DialogPicker.java68
-rw-r--r--src/com/android/gallery3d/app/EyePosition.java218
-rw-r--r--src/com/android/gallery3d/app/FilterUtils.java296
-rw-r--r--src/com/android/gallery3d/app/Gallery.java232
-rw-r--r--src/com/android/gallery3d/app/GalleryActionBar.java218
-rw-r--r--src/com/android/gallery3d/app/GalleryActivity.java28
-rw-r--r--src/com/android/gallery3d/app/GalleryApp.java39
-rw-r--r--src/com/android/gallery3d/app/GalleryAppImpl.java90
-rw-r--r--src/com/android/gallery3d/app/GalleryContext.java38
-rw-r--r--src/com/android/gallery3d/app/LoadingListener.java22
-rw-r--r--src/com/android/gallery3d/app/Log.java53
-rw-r--r--src/com/android/gallery3d/app/ManageCachePage.java271
-rw-r--r--src/com/android/gallery3d/app/MovieActivity.java129
-rw-r--r--src/com/android/gallery3d/app/MoviePlayer.java291
-rw-r--r--src/com/android/gallery3d/app/PackagesMonitor.java50
-rw-r--r--src/com/android/gallery3d/app/PhotoDataAdapter.java794
-rw-r--r--src/com/android/gallery3d/app/PhotoPage.java581
-rw-r--r--src/com/android/gallery3d/app/SinglePhotoDataAdapter.java181
-rw-r--r--src/com/android/gallery3d/app/SlideshowDataAdapter.java187
-rw-r--r--src/com/android/gallery3d/app/SlideshowDream.java28
-rw-r--r--src/com/android/gallery3d/app/SlideshowPage.java338
-rw-r--r--src/com/android/gallery3d/app/StateManager.java277
-rw-r--r--src/com/android/gallery3d/app/UsbDeviceActivity.java47
-rw-r--r--src/com/android/gallery3d/app/Wallpaper.java116
33 files changed, 7923 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..d0d7b0fad
--- /dev/null
+++ b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
@@ -0,0 +1,198 @@
+/*
+ * 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.DataManager;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRootView;
+import com.android.gallery3d.ui.PositionRepository;
+import com.android.gallery3d.util.ThreadPool;
+
+import android.app.ActionBar;
+import android.app.Activity;
+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.os.Bundle;
+
+public class AbstractGalleryActivity extends Activity implements GalleryActivity {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AbstractGalleryActivity";
+ private GLRootView mGLRootView;
+ private StateManager mStateManager;
+ private PositionRepository mPositionRepository = new PositionRepository();
+
+ 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 onSaveInstanceState(Bundle outState) {
+ mGLRootView.lockRenderThread();
+ try {
+ super.onSaveInstanceState(outState);
+ getStateManager().saveState(outState);
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ }
+
+ public Context getAndroidContext() {
+ return this;
+ }
+
+ public ImageCacheService getImageCacheService() {
+ return ((GalleryApp) getApplication()).getImageCacheService();
+ }
+
+ public DataManager getDataManager() {
+ return ((GalleryApp) getApplication()).getDataManager();
+ }
+
+ public ThreadPool getThreadPool() {
+ return ((GalleryApp) getApplication()).getThreadPool();
+ }
+
+ public GalleryApp getGalleryApplication() {
+ return (GalleryApp) getApplication();
+ }
+
+ public synchronized StateManager getStateManager() {
+ if (mStateManager == null) {
+ mStateManager = new StateManager(this);
+ }
+ return mStateManager;
+ }
+
+ public GLRoot getGLRoot() {
+ return mGLRootView;
+ }
+
+ public PositionRepository getPositionRepository() {
+ return mPositionRepository;
+ }
+
+ @Override
+ public void setContentView(int resId) {
+ super.setContentView(resId);
+ mGLRootView = (GLRootView) findViewById(R.id.gl_root_view);
+ }
+
+ public int getActionBarHeight() {
+ ActionBar actionBar = getActionBar();
+ return actionBar != null ? actionBar.getHeight() : 0;
+ }
+
+ 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();
+ }
+ };
+ mAlertDialog = new AlertDialog.Builder(this)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setTitle("No Storage")
+ .setMessage("No external storage available.")
+ .setNegativeButton(android.R.string.cancel, onClick)
+ .setOnCancelListener(onCancel)
+ .show();
+ registerReceiver(mMountReceiver, mMountFilter);
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mAlertDialog != null) {
+ unregisterReceiver(mMountReceiver);
+ mAlertDialog.dismiss();
+ mAlertDialog = null;
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mGLRootView.lockRenderThread();
+ try {
+ getStateManager().resume();
+ getDataManager().resume();
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ mGLRootView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mGLRootView.onPause();
+ mGLRootView.lockRenderThread();
+ try {
+ getStateManager().pause();
+ getDataManager().pause();
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ mGLRootView.lockRenderThread();
+ try {
+ getStateManager().notifyActivityResult(
+ requestCode, resultCode, data);
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ }
+
+ @Override
+ public GalleryActionBar getGalleryActionBar() {
+ return null;
+ }
+}
diff --git a/src/com/android/gallery3d/app/ActivityState.java b/src/com/android/gallery3d/app/ActivityState.java
new file mode 100644
index 000000000..bfacc5484
--- /dev/null
+++ b/src/com/android/gallery3d/app/ActivityState.java
@@ -0,0 +1,136 @@
+/*
+ * 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.ui.GLView;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.WindowManager;
+
+abstract public class ActivityState {
+ public static final int FLAG_HIDE_ACTION_BAR = 1;
+ public static final int FLAG_HIDE_STATUS_BAR = 2;
+
+ protected GalleryActivity 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;
+ ResultEntry next;
+ }
+
+ protected ActivityState() {
+ }
+
+ protected void setContentPane(GLView content) {
+ mActivity.getGLRoot().setContentPane(content);
+ }
+
+ void initialize(GalleryActivity 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 onSaveState(Bundle outState) {
+ }
+
+ protected void onStateResult(int requestCode, int resultCode, Intent data) {
+ }
+
+ protected void onCreate(Bundle data, Bundle storedState) {
+ }
+
+ protected void onPause() {
+ }
+
+ // should only be called by StateManager
+ void resume() {
+ Activity activity = (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();
+ actionBar.setDisplayOptions(
+ stateCount == 1 ? 0 : ActionBar.DISPLAY_HOME_AS_UP,
+ ActionBar.DISPLAY_HOME_AS_UP);
+ }
+
+ activity.invalidateOptionsMenu();
+
+ if ((mFlags & FLAG_HIDE_STATUS_BAR) != 0) {
+ WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
+ params.systemUiVisibility = View.STATUS_BAR_HIDDEN;
+ ((Activity) mActivity).getWindow().setAttributes(params);
+ } else {
+ WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
+ params.systemUiVisibility = View.STATUS_BAR_VISIBLE;
+ ((Activity) mActivity).getWindow().setAttributes(params);
+ }
+
+ ResultEntry entry = mReceivedResults;
+ if (entry != null) {
+ mReceivedResults = null;
+ onStateResult(entry.requestCode, entry.resultCode, entry.resultData);
+ }
+ onResume();
+ }
+
+ // a subclass of ActivityState should override the method to resume itself
+ protected void onResume() {
+ }
+
+ protected boolean onCreateActionBar(Menu menu) {
+ return false;
+ }
+
+ protected boolean onItemSelected(MenuItem item) {
+ return false;
+ }
+
+ protected void onDestroy() {
+ }
+}
diff --git a/src/com/android/gallery3d/app/AlbumDataAdapter.java b/src/com/android/gallery3d/app/AlbumDataAdapter.java
new file mode 100644
index 000000000..9934cf88c
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumDataAdapter.java
@@ -0,0 +1,367 @@
+/*
+ * 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.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.ui.AlbumView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+
+import android.os.Handler;
+import android.os.Message;
+
+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 AlbumDataAdapter implements AlbumView.Model {
+ @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;
+
+ 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 AlbumView.ModelListener mModelListener;
+ private MySourceListener mSourceListener = new MySourceListener();
+ private LoadingListener mLoadingListener;
+
+ private ReloadTask mReloadTask;
+
+ public AlbumDataAdapter(GalleryActivity 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) mLoadingListener.onLoadingFinished();
+ 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)) {
+ throw new IllegalArgumentException(String.format(
+ "%s not in (%s, %s)", index, mActiveStart, mActiveEnd));
+ }
+ return mData[index % mData.length];
+ }
+
+ public int getActiveStart() {
+ return mActiveStart;
+ }
+
+ public int getActiveEnd() {
+ return mActiveEnd;
+ }
+
+ public boolean isActive(int index) {
+ return index >= mActiveStart && index < mActiveEnd;
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ 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;
+ }
+ MediaItem[] data = mData;
+ 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;
+
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ 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 {
+ public void onContentDirty() {
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ public void setModelListener(AlbumView.ModelListener listener) {
+ mModelListener = 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;
+ }
+
+ public UpdateInfo call() throws Exception {
+ 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 (mModelListener != null) mModelListener.onSizeChanged(mSize);
+ if (mContentEnd > mSize) mContentEnd = mSize;
+ if (mActiveEnd > mSize) mActiveEnd = mSize;
+ }
+
+ ArrayList<MediaItem> items = info.items;
+
+ if (items == null) 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 (mModelListener != null && i >= mActiveStart && i < mActiveEnd) {
+ mModelListener.onWindowContentChanged(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() {
+ boolean updateComplete = false;
+ while (mActive) {
+ synchronized (this) {
+ if (mActive && !mDirty && updateComplete) {
+ updateLoading(false);
+ Utils.waitWithoutInterrupt(this);
+ continue;
+ }
+ }
+ mDirty = false;
+ updateLoading(true);
+ long version;
+ synchronized (DataManager.LOCK) {
+ version = mSource.reload();
+ }
+ UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
+ updateComplete = info == null;
+ if (updateComplete) continue;
+ synchronized (DataManager.LOCK) {
+ 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..5c09ce2d2
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -0,0 +1,602 @@
+/*
+ * 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.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.MtpDevice;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.ActionModeHandler;
+import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener;
+import com.android.gallery3d.ui.AlbumView;
+import com.android.gallery3d.ui.DetailsWindow;
+import com.android.gallery3d.ui.DetailsWindow.CloseListener;
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.GridDrawer;
+import com.android.gallery3d.ui.HighlightDrawer;
+import com.android.gallery3d.ui.PositionProvider;
+import com.android.gallery3d.ui.PositionRepository;
+import com.android.gallery3d.ui.PositionRepository.Position;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.StaticBackground;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View.MeasureSpec;
+import android.widget.Toast;
+
+import java.util.Random;
+
+public class AlbumPage extends ActivityState implements GalleryActionBar.ClusterRunner,
+ SelectionManager.SelectionListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumPage";
+
+ public static final String KEY_MEDIA_PATH = "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";
+
+ private static final int REQUEST_SLIDESHOW = 1;
+ private static final int REQUEST_PHOTO = 2;
+ private static final int REQUEST_DO_ANIMATION = 3;
+
+ private static final float USER_DISTANCE_METER = 0.3f;
+
+ private boolean mIsActive = false;
+ private StaticBackground mStaticBackground;
+ private AlbumView mAlbumView;
+ private Path mMediaSetPath;
+
+ private AlbumDataAdapter mAlbumDataAdapter;
+
+ protected SelectionManager mSelectionManager;
+ private GridDrawer mGridDrawer;
+ private HighlightDrawer mHighlightDrawer;
+
+ private boolean mGetContent;
+ private boolean mShowClusterMenu;
+
+ private ActionMode mActionMode;
+ private ActionModeHandler mActionModeHandler;
+ private int mFocusIndex = 0;
+ private DetailsWindow mDetailsWindow;
+ private MediaSet mMediaSet;
+ private boolean mShowDetails;
+ private float mUserDistance; // in pixel
+
+ private ProgressDialog mProgressDialog;
+ private Future<?> mPendingTask;
+
+ private Future<Void> mSyncTask = null;
+
+ private GLView mRootPane = new GLView() {
+ private float mMatrix[] = new float[16];
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ mStaticBackground.layout(0, 0, right - left, bottom - top);
+
+ int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity);
+ int slotViewBottom = bottom - top;
+ int slotViewRight = right - left;
+
+ if (mShowDetails) {
+ mDetailsWindow.measure(
+ MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int width = mDetailsWindow.getMeasuredWidth();
+ int detailLeft = right - left - width;
+ slotViewRight = detailLeft;
+ mDetailsWindow.layout(detailLeft, slotViewTop, detailLeft + width,
+ bottom - top);
+ } else {
+ mAlbumView.setSelectionDrawer(mGridDrawer);
+ }
+
+ mAlbumView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
+ GalleryUtils.setViewPointMatrix(mMatrix,
+ (right - left) / 2, (bottom - top) / 2, -mUserDistance);
+ PositionRepository.getInstance(mActivity).setOffset(
+ 0, slotViewTop);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.multiplyMatrix(mMatrix, 0);
+ super.render(canvas);
+ canvas.restore();
+ }
+ };
+
+ @Override
+ protected void onBackPressed() {
+ if (mShowDetails) {
+ hideDetails();
+ } else if (mSelectionManager.inSelectionMode()) {
+ mSelectionManager.leaveSelectionMode();
+ } else {
+ mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ super.onBackPressed();
+ }
+ }
+
+ public void onSingleTapUp(int slotIndex) {
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) {
+ Log.w(TAG, "item not ready yet, ignore the click");
+ return;
+ }
+ if (mShowDetails) {
+ mHighlightDrawer.setHighlightItem(item.getPath());
+ mDetailsWindow.reloadDetails(slotIndex);
+ } else if (!mSelectionManager.inSelectionMode()) {
+ if (mGetContent) {
+ onGetContent(item);
+ } else {
+ boolean playVideo =
+ (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0;
+ if (playVideo) {
+ // Play the video.
+ PhotoPage.playVideo((Activity) mActivity, item.getPlayUri(), item.getName());
+ } else {
+ // Get into the PhotoPage.
+ Bundle data = new Bundle();
+ mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ data.putInt(PhotoPage.KEY_INDEX_HINT, slotIndex);
+ data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
+ mMediaSetPath.toString());
+ data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH,
+ item.getPath().toString());
+ mActivity.getStateManager().startStateForResult(
+ PhotoPage.class, REQUEST_PHOTO, data);
+ }
+ }
+ } else {
+ mSelectionManager.toggle(item.getPath());
+ mAlbumView.invalidate();
+ }
+ }
+
+ private void onGetContent(final MediaItem item) {
+ DataManager dm = mActivity.getDataManager();
+ Activity activity = (Activity) mActivity;
+ if (mData.getString(Gallery.EXTRA_CROP) != null) {
+ // TODO: Handle MtpImagew
+ Uri uri = dm.getContentUri(item.getPath());
+ Intent intent = new Intent(CropImage.ACTION_CROP, uri)
+ .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
+ .putExtras(getData());
+ if (mData.getParcelable(MediaStore.EXTRA_OUTPUT) == null) {
+ intent.putExtra(CropImage.KEY_RETURN_DATA, true);
+ }
+ activity.startActivity(intent);
+ activity.finish();
+ } else {
+ activity.setResult(Activity.RESULT_OK,
+ new Intent(null, item.getContentUri()));
+ activity.finish();
+ }
+ }
+
+ public void onLongTap(int slotIndex) {
+ if (mGetContent) return;
+ if (mShowDetails) {
+ onSingleTapUp(slotIndex);
+ } else {
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) return;
+ mSelectionManager.setAutoLeaveSelectionMode(true);
+ mSelectionManager.toggle(item.getPath());
+ mAlbumView.invalidate();
+ }
+ }
+
+ 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);
+ }
+
+ public void doFilter(int filterType) {
+ String basePath = mMediaSet.getPath().toString();
+ String newPath = FilterUtils.switchFilterPath(basePath, filterType);
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumPage.KEY_MEDIA_PATH, newPath);
+ mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ mActivity.getStateManager().switchState(this, AlbumPage.class, data);
+ }
+
+ public void onOperationComplete() {
+ mAlbumView.invalidate();
+ // TODO: enable animation
+ }
+
+ @Override
+ protected void onCreate(Bundle data, Bundle 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);
+
+ startTransition(data);
+
+ // Enable auto-select-all for mtp album
+ if (data.getBoolean(KEY_AUTO_SELECT_ALL)) {
+ mSelectionManager.selectAll();
+ }
+ }
+
+ private void startTransition() {
+ final PositionRepository repository =
+ PositionRepository.getInstance(mActivity);
+ mAlbumView.startTransition(new PositionProvider() {
+ private Position mTempPosition = new Position();
+ public Position getPosition(long identity, Position target) {
+ Position p = repository.get(identity);
+ if (p != null) return p;
+ mTempPosition.set(target);
+ mTempPosition.z = 128;
+ return mTempPosition;
+ }
+ });
+ }
+
+ private void startTransition(Bundle data) {
+ final PositionRepository repository =
+ PositionRepository.getInstance(mActivity);
+ final int[] center = data == null
+ ? null
+ : data.getIntArray(KEY_SET_CENTER);
+ final Random random = new Random();
+ mAlbumView.startTransition(new PositionProvider() {
+ private Position mTempPosition = new Position();
+ public Position getPosition(long identity, Position target) {
+ Position p = repository.get(identity);
+ if (p != null) return p;
+ if (center != null) {
+ random.setSeed(identity);
+ mTempPosition.set(center[0], center[1],
+ 0, random.nextInt(60) - 30, 0);
+ } else {
+ mTempPosition.set(target);
+ mTempPosition.z = 128;
+ }
+ return mTempPosition;
+ }
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsActive = true;
+ setContentPane(mRootPane);
+ mAlbumDataAdapter.resume();
+ mAlbumView.resume();
+ mActionModeHandler.resume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mIsActive = false;
+ mAlbumDataAdapter.pause();
+ mAlbumView.pause();
+ if (mDetailsWindow != null) {
+ mDetailsWindow.pause();
+ }
+ Future<?> task = mPendingTask;
+ if (task != null) {
+ // cancel on going task
+ task.cancel();
+ task.waitDone();
+ if (mProgressDialog != null) {
+ mProgressDialog.dismiss();
+ mProgressDialog = null;
+ }
+ }
+ if (mSyncTask != null) {
+ mSyncTask.cancel();
+ mSyncTask = null;
+ }
+ mActionModeHandler.pause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (mAlbumDataAdapter != null) {
+ mAlbumDataAdapter.setLoadingListener(null);
+ }
+ }
+
+ private void initializeViews() {
+ mStaticBackground = new StaticBackground((Context) mActivity);
+ mRootPane.addComponent(mStaticBackground);
+
+ mSelectionManager = new SelectionManager(mActivity, false);
+ mSelectionManager.setSelectionListener(this);
+ mGridDrawer = new GridDrawer((Context) mActivity, mSelectionManager);
+ Config.AlbumPage config = Config.AlbumPage.get((Context) mActivity);
+ mAlbumView = new AlbumView(mActivity,
+ config.slotWidth, config.slotHeight, config.displayItemSize);
+ mAlbumView.setSelectionDrawer(mGridDrawer);
+ mRootPane.addComponent(mAlbumView);
+ mAlbumView.setListener(new SlotView.SimpleListener() {
+ @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() {
+ public boolean onActionItemClicked(MenuItem item) {
+ return onItemSelected(item);
+ }
+ });
+ mStaticBackground.setImage(R.drawable.background,
+ R.drawable.background_portrait);
+ }
+
+ private void initializeData(Bundle data) {
+ mMediaSetPath = Path.fromString(data.getString(KEY_MEDIA_PATH));
+ mMediaSet = mActivity.getDataManager().getMediaSet(mMediaSetPath);
+ Utils.assertTrue(mMediaSet != null,
+ "MediaSet is null. Path = %s", mMediaSetPath);
+ mSelectionManager.setSourceMediaSet(mMediaSet);
+ mAlbumDataAdapter = new AlbumDataAdapter(mActivity, mMediaSet);
+ mAlbumDataAdapter.setLoadingListener(new MyLoadingListener());
+ mAlbumView.setModel(mAlbumDataAdapter);
+ }
+
+ private void showDetails() {
+ mShowDetails = true;
+ if (mDetailsWindow == null) {
+ mHighlightDrawer = new HighlightDrawer(mActivity.getAndroidContext());
+ mDetailsWindow = new DetailsWindow(mActivity, new MyDetailsSource());
+ mDetailsWindow.setCloseListener(new CloseListener() {
+ public void onClose() {
+ hideDetails();
+ }
+ });
+ mRootPane.addComponent(mDetailsWindow);
+ }
+ mAlbumView.setSelectionDrawer(mHighlightDrawer);
+ mDetailsWindow.show();
+ }
+
+ private void hideDetails() {
+ mShowDetails = false;
+ mAlbumView.setSelectionDrawer(mGridDrawer);
+ mDetailsWindow.hide();
+ }
+
+ @Override
+ protected boolean onCreateActionBar(Menu menu) {
+ Activity activity = (Activity) mActivity;
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ MenuInflater inflater = activity.getMenuInflater();
+
+ if (mGetContent) {
+ inflater.inflate(R.menu.pickup, menu);
+ int typeBits = mData.getInt(Gallery.KEY_TYPE_BITS,
+ DataManager.INCLUDE_IMAGE);
+
+ actionBar.setTitle(GalleryUtils.getSelectionModePrompt(typeBits));
+ } else {
+ inflater.inflate(R.menu.album, menu);
+ actionBar.setTitle(mMediaSet.getName());
+ if (mMediaSet instanceof MtpDevice) {
+ menu.findItem(R.id.action_slideshow).setVisible(false);
+ } else {
+ menu.findItem(R.id.action_slideshow).setVisible(true);
+ }
+
+ MenuItem groupBy = menu.findItem(R.id.action_group_by);
+ FilterUtils.setupMenuItems(actionBar, mMediaSetPath, true);
+
+ if (groupBy != null) {
+ groupBy.setVisible(mShowClusterMenu);
+ }
+
+ actionBar.setTitle(mMediaSet.getName());
+ }
+ actionBar.setSubtitle(null);
+
+ return true;
+ }
+
+ @Override
+ protected boolean onItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ 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: {
+ 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;
+ }
+ 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);
+ mAlbumView.setCenterIndex(mFocusIndex);
+ break;
+ }
+ case REQUEST_PHOTO: {
+ if (data == null) return;
+ mFocusIndex = data.getIntExtra(PhotoPage.KEY_INDEX_HINT, 0);
+ mAlbumView.setCenterIndex(mFocusIndex);
+ startTransition();
+ break;
+ }
+ case REQUEST_DO_ANIMATION: {
+ startTransition(null);
+ break;
+ }
+ }
+ }
+
+ public void onSelectionModeChange(int mode) {
+ switch (mode) {
+ case SelectionManager.ENTER_SELECTION_MODE: {
+ mActionMode = mActionModeHandler.startActionMode();
+ break;
+ }
+ case SelectionManager.LEAVE_SELECTION_MODE: {
+ mActionMode.finish();
+ mRootPane.invalidate();
+ break;
+ }
+ case SelectionManager.SELECT_ALL_MODE: {
+ int count = mSelectionManager.getSelectedCount();
+ String format = mActivity.getResources().getQuantityString(
+ R.plurals.number_of_items_selected, count);
+ mActionModeHandler.setTitle(String.format(format, count));
+ mActionModeHandler.updateSupportedOperation();
+ mRootPane.invalidate();
+ break;
+ }
+ }
+ }
+
+ public void onSelectionChange(Path path, boolean selected) {
+ Utils.assertTrue(mActionMode != null);
+ 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);
+ }
+
+ private class MyLoadingListener implements LoadingListener {
+ @Override
+ public void onLoadingStarted() {
+ GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
+ }
+
+ @Override
+ public void onLoadingFinished() {
+ if (!mIsActive) return;
+ if (mAlbumDataAdapter.size() == 0) {
+ if (mSyncTask == null) {
+ mSyncTask = mMediaSet.requestSync();
+ }
+ if (mSyncTask.isDone()){
+ Toast.makeText((Context) mActivity,
+ R.string.empty_album, Toast.LENGTH_LONG).show();
+ mActivity.getStateManager().finishState(AlbumPage.this);
+ }
+ }
+ if (mSyncTask == null || mSyncTask.isDone()) {
+ GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
+ }
+ }
+ }
+
+ private class MyDetailsSource implements DetailsWindow.DetailsSource {
+ private int mIndex;
+ public int size() {
+ return mAlbumDataAdapter.size();
+ }
+
+ // If requested index is out of active window, suggest a valid index.
+ // If there is no valid index available, return -1.
+ public int findIndex(int indexHint) {
+ if (mAlbumDataAdapter.isActive(indexHint)) {
+ mIndex = indexHint;
+ } else {
+ mIndex = mAlbumDataAdapter.getActiveStart();
+ if (!mAlbumDataAdapter.isActive(mIndex)) {
+ return -1;
+ }
+ }
+ return mIndex;
+ }
+
+ public MediaDetails getDetails() {
+ MediaObject item = mAlbumDataAdapter.get(mIndex);
+ if (item != null) {
+ mHighlightDrawer.setHighlightItem(item.getPath());
+ return item.getDetails();
+ } else {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/AlbumPicker.java b/src/com/android/gallery3d/app/AlbumPicker.java
new file mode 100644
index 000000000..b86aee879
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumPicker.java
@@ -0,0 +1,68 @@
+/*
+ * 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 com.android.gallery3d.R;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRootView;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+public class AlbumPicker extends AbstractGalleryActivity
+ implements OnClickListener {
+
+ public static final String KEY_ALBUM_PATH = "album-path";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.dialog_picker);
+ ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true);
+ findViewById(R.id.cancel).setOnClickListener(this);
+ 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);
+ }
+
+ @Override
+ public void onBackPressed() {
+ // send the back event to the top sub-state
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ getStateManager().getTopState().onBackPressed();
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.cancel) finish();
+ }
+}
diff --git a/src/com/android/gallery3d/app/AlbumSetDataAdapter.java b/src/com/android/gallery3d/app/AlbumSetDataAdapter.java
new file mode 100644
index 000000000..9086ddbf4
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumSetDataAdapter.java
@@ -0,0 +1,384 @@
+/*
+ * 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.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.ui.AlbumSetView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+
+import android.os.Handler;
+import android.os.Message;
+
+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 AlbumSetDataAdapter implements AlbumSetView.Model {
+ @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 MAX_COVER_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;
+
+ private static final MediaItem[] EMPTY_MEDIA_ITEMS = new MediaItem[0];
+
+ private final MediaSet[] mData;
+ private final MediaItem[][] mCoverData;
+ 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 AlbumSetView.ModelListener mModelListener;
+ private LoadingListener mLoadingListener;
+ private ReloadTask mReloadTask;
+
+ private final Handler mMainHandler;
+
+ private MySourceListener mSourceListener = new MySourceListener();
+
+ public AlbumSetDataAdapter(GalleryActivity activity, MediaSet albumSet, int cacheSize) {
+ mSource = Utils.checkNotNull(albumSet);
+ mCoverData = new MediaItem[cacheSize][];
+ mData = new MediaSet[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();
+ return;
+ }
+ }
+ };
+ }
+
+ public void pause() {
+ mReloadTask.terminate();
+ mReloadTask = null;
+ mSource.removeContentListener(mSourceListener);
+ }
+
+ public void resume() {
+ mSource.addContentListener(mSourceListener);
+ mReloadTask = new ReloadTask();
+ mReloadTask.start();
+ }
+
+ public MediaSet getMediaSet(int index) {
+ if (index < mActiveStart && index >= mActiveEnd) {
+ throw new IllegalArgumentException(String.format(
+ "%s not in (%s, %s)", index, mActiveStart, mActiveEnd));
+ }
+ return mData[index % mData.length];
+ }
+
+ public MediaItem[] getCoverItems(int index) {
+ if (index < mActiveStart && index >= mActiveEnd) {
+ throw new IllegalArgumentException(String.format(
+ "%s not in (%s, %s)", index, mActiveStart, mActiveEnd));
+ }
+ MediaItem[] result = mCoverData[index % mCoverData.length];
+
+ // If the result is not ready yet, return an empty array
+ return result == null ? EMPTY_MEDIA_ITEMS : result;
+ }
+
+ public int getActiveStart() {
+ return mActiveStart;
+ }
+
+ public int getActiveEnd() {
+ return mActiveEnd;
+ }
+
+ public boolean isActive(int index) {
+ return index >= mActiveStart && index < mActiveEnd;
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ private void clearSlot(int slotIndex) {
+ mData[slotIndex] = null;
+ mCoverData[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;
+ MediaItem[][] data = mCoverData;
+ int length = data.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 <= mCoverData.length && end <= mSize);
+
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ int length = mCoverData.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 {
+ public void onContentDirty() {
+ mReloadTask.notifyDirty();
+ }
+ }
+
+ public void setModelListener(AlbumSetView.ModelListener listener) {
+ mModelListener = listener;
+ }
+
+ public void setLoadingListener(LoadingListener listener) {
+ mLoadingListener = listener;
+ }
+
+ private static void getRepresentativeItems(MediaSet set, int wanted,
+ ArrayList<MediaItem> result) {
+ if (set.getMediaItemCount() > 0) {
+ result.addAll(set.getMediaItem(0, wanted));
+ }
+
+ int n = set.getSubMediaSetCount();
+ for (int i = 0; i < n && wanted > result.size(); i++) {
+ MediaSet subset = set.getSubMediaSet(i);
+ double perSet = (double) (wanted - result.size()) / (n - i);
+ int m = (int) Math.ceil(perSet);
+ getRepresentativeItems(subset, m, result);
+ }
+ }
+
+ private static class UpdateInfo {
+ public long version;
+ public int index;
+
+ public int size;
+ public MediaSet item;
+ public MediaItem covers[];
+ }
+
+ 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 UpdateInfo mUpdateInfo;
+
+ public UpdateContent(UpdateInfo info) {
+ mUpdateInfo = info;
+ }
+
+ public Void call() {
+ UpdateInfo info = mUpdateInfo;
+ mSourceVersion = info.version;
+ if (mSize != info.size) {
+ mSize = info.size;
+ if (mModelListener != null) mModelListener.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 % mCoverData.length;
+ mSetVersion[pos] = info.version;
+ long itemVersion = info.item.getDataVersion();
+ if (mItemVersion[pos] == itemVersion) return null;
+ mItemVersion[pos] = itemVersion;
+ mData[pos] = info.item;
+ mCoverData[pos] = info.covers;
+ if (mModelListener != null
+ && info.index >= mActiveStart && info.index < mActiveEnd) {
+ mModelListener.onWindowContentChanged(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() {
+ boolean updateComplete = false;
+ while (mActive) {
+ synchronized (this) {
+ if (mActive && !mDirty && updateComplete) {
+ updateLoading(false);
+ Utils.waitWithoutInterrupt(this);
+ continue;
+ }
+ }
+ mDirty = false;
+ updateLoading(true);
+
+ long version;
+ synchronized (DataManager.LOCK) {
+ version = mSource.reload();
+ }
+ UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
+ updateComplete = info == null;
+ if (updateComplete) continue;
+
+ synchronized (DataManager.LOCK) {
+ if (info.version != version) {
+ info.version = version;
+ info.size = mSource.getSubMediaSetCount();
+ }
+ if (info.index != INDEX_NONE) {
+ info.item = mSource.getSubMediaSet(info.index);
+ if (info.item == null) continue;
+ ArrayList<MediaItem> covers = new ArrayList<MediaItem>();
+ getRepresentativeItems(info.item, MAX_COVER_COUNT, covers);
+ info.covers = covers.toArray(new MediaItem[covers.size()]);
+ }
+ }
+ 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..688ff81f2
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumSetPage.java
@@ -0,0 +1,586 @@
+/*
+ * 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.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+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.AlbumSetView;
+import com.android.gallery3d.ui.DetailsWindow;
+import com.android.gallery3d.ui.DetailsWindow.CloseListener;
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.GridDrawer;
+import com.android.gallery3d.ui.HighlightDrawer;
+import com.android.gallery3d.ui.PositionProvider;
+import com.android.gallery3d.ui.PositionRepository;
+import com.android.gallery3d.ui.PositionRepository.Position;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.StaticBackground;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View.MeasureSpec;
+import android.widget.Toast;
+
+public class AlbumSetPage extends ActivityState implements
+ SelectionManager.SelectionListener, GalleryActionBar.ClusterRunner,
+ EyePosition.EyePositionListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumSetPage";
+
+ 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";
+ private static final int DATA_CACHE_SIZE = 256;
+ private static final int REQUEST_DO_ANIMATION = 1;
+ private static final int MSG_GOTO_MANAGE_CACHE_PAGE = 1;
+
+ private boolean mIsActive = false;
+ private StaticBackground mStaticBackground;
+ private AlbumSetView mAlbumSetView;
+
+ private MediaSet mMediaSet;
+ private String mTitle;
+ private String mSubtitle;
+ private boolean mShowClusterTabs;
+
+ protected SelectionManager mSelectionManager;
+ private AlbumSetDataAdapter mAlbumSetDataAdapter;
+ private GridDrawer mGridDrawer;
+ private HighlightDrawer mHighlightDrawer;
+
+ private boolean mGetContent;
+ private boolean mGetAlbum;
+ private ActionMode mActionMode;
+ private ActionModeHandler mActionModeHandler;
+ private DetailsWindow mDetailsWindow;
+ private boolean mShowDetails;
+ 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 SynchronizedHandler mHandler;
+
+ private GLView mRootPane = new GLView() {
+ private float mMatrix[] = new float[16];
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ mStaticBackground.layout(0, 0, right - left, bottom - top);
+ mEyePosition.resetPosition();
+
+ int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity);
+ int slotViewBottom = bottom - top;
+ int slotViewRight = right - left;
+
+ if (mShowDetails) {
+ mDetailsWindow.measure(
+ MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int width = mDetailsWindow.getMeasuredWidth();
+ int detailLeft = right - left - width;
+ slotViewRight = detailLeft;
+ mDetailsWindow.layout(detailLeft, slotViewTop, detailLeft + width,
+ bottom - top);
+ } else {
+ mAlbumSetView.setSelectionDrawer(mGridDrawer);
+ }
+
+ mAlbumSetView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
+ PositionRepository.getInstance(mActivity).setOffset(
+ 0, slotViewTop);
+ }
+
+ @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 {
+ mAlbumSetView.savePositions(
+ PositionRepository.getInstance(mActivity));
+ super.onBackPressed();
+ }
+ }
+
+ private void savePositions(int slotIndex, int center[]) {
+ Rect offset = new Rect();
+ mRootPane.getBoundsOf(mAlbumSetView, offset);
+ mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity));
+ Rect r = mAlbumSetView.getSlotRect(slotIndex);
+ int scrollX = mAlbumSetView.getScrollX();
+ int scrollY = mAlbumSetView.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) {
+ MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+ if (targetSet == null) return; // Content is dirty, we shall reload soon
+
+ if (mShowDetails) {
+ Path path = targetSet.getPath();
+ mHighlightDrawer.setHighlightItem(path);
+ mDetailsWindow.reloadDetails(slotIndex);
+ } else if (!mSelectionManager.inSelectionMode()) {
+ Bundle data = new Bundle(getData());
+ String mediaPath = targetSet.getPath().toString();
+ int[] center = new int[2];
+ savePositions(slotIndex, center);
+ data.putIntArray(AlbumPage.KEY_SET_CENTER, center);
+ if (mGetAlbum && targetSet.isLeafAlbum()) {
+ Activity 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 && (targetSet.getSupportedOperations()
+ & MediaObject.SUPPORT_IMPORT) != 0) {
+ data.putBoolean(AlbumPage.KEY_AUTO_SELECT_ALL, true);
+ }
+ data.putString(AlbumPage.KEY_MEDIA_PATH, mediaPath);
+ boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
+ // We only show cluster menu in the first AlbumPage in stack
+ data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum);
+ mActivity.getStateManager().startStateForResult(
+ AlbumPage.class, REQUEST_DO_ANIMATION, data);
+ }
+ } else {
+ mSelectionManager.toggle(targetSet.getPath());
+ mAlbumSetView.invalidate();
+ }
+ }
+
+ public void onLongTap(int slotIndex) {
+ if (mGetContent || mGetAlbum) return;
+ if (mShowDetails) {
+ onSingleTapUp(slotIndex);
+ } else {
+ MediaSet set = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+ if (set == null) return;
+ mSelectionManager.setAutoLeaveSelectionMode(true);
+ mSelectionManager.toggle(set.getPath());
+ mAlbumSetView.invalidate();
+ }
+ }
+
+ 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);
+ mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity));
+ mActivity.getStateManager().switchState(this, AlbumSetPage.class, data);
+ }
+
+ public void doFilter(int filterType) {
+ String basePath = mMediaSet.getPath().toString();
+ String newPath = FilterUtils.switchFilterPath(basePath, filterType);
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath);
+ mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity));
+ mActivity.getStateManager().switchState(this, AlbumSetPage.class, data);
+ }
+
+ public void onOperationComplete() {
+ mAlbumSetView.invalidate();
+ // TODO: enable animation
+ }
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ Utils.assertTrue(message.what == MSG_GOTO_MANAGE_CACHE_PAGE);
+ 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);
+ }
+ };
+
+ initializeViews();
+ initializeData(data);
+ 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(mActivity.getAndroidContext(), this);
+
+ startTransition();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mIsActive = false;
+ mActionModeHandler.pause();
+ mAlbumSetDataAdapter.pause();
+ mAlbumSetView.pause();
+ mEyePosition.pause();
+ if (mDetailsWindow != null) {
+ mDetailsWindow.pause();
+ }
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ if (actionBar != null) actionBar.hideClusterTabs();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mIsActive = true;
+ setContentPane(mRootPane);
+ mAlbumSetDataAdapter.resume();
+ mAlbumSetView.resume();
+ mEyePosition.resume();
+ mActionModeHandler.resume();
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ if (mShowClusterTabs && actionBar != null) actionBar.showClusterTabs(this);
+ }
+
+ private void initializeData(Bundle data) {
+ String mediaPath = data.getString(AlbumSetPage.KEY_MEDIA_PATH);
+ mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
+ mSelectionManager.setSourceMediaSet(mMediaSet);
+ mAlbumSetDataAdapter = new AlbumSetDataAdapter(
+ mActivity, mMediaSet, DATA_CACHE_SIZE);
+ mAlbumSetDataAdapter.setLoadingListener(new MyLoadingListener());
+ mAlbumSetView.setModel(mAlbumSetDataAdapter);
+ }
+
+ private void initializeViews() {
+ mSelectionManager = new SelectionManager(mActivity, true);
+ mSelectionManager.setSelectionListener(this);
+ mStaticBackground = new StaticBackground(mActivity.getAndroidContext());
+ mRootPane.addComponent(mStaticBackground);
+
+ mGridDrawer = new GridDrawer((Context) mActivity, mSelectionManager);
+ Config.AlbumSetPage config = Config.AlbumSetPage.get((Context) mActivity);
+ mAlbumSetView = new AlbumSetView(mActivity, mGridDrawer,
+ config.slotWidth, config.slotHeight,
+ config.displayItemSize, config.labelFontSize,
+ config.labelOffsetY, config.labelMargin);
+ mAlbumSetView.setListener(new SlotView.SimpleListener() {
+ @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() {
+ public boolean onActionItemClicked(MenuItem item) {
+ return onItemSelected(item);
+ }
+ });
+ mRootPane.addComponent(mAlbumSetView);
+
+ mStaticBackground.setImage(R.drawable.background,
+ R.drawable.background_portrait);
+ }
+
+ @Override
+ protected boolean onCreateActionBar(Menu menu) {
+ Activity activity = (Activity) mActivity;
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ MenuInflater inflater = activity.getMenuInflater();
+
+ final boolean inAlbum = mActivity.getStateManager().hasStateClass(
+ AlbumPage.class);
+
+ if (mGetContent) {
+ inflater.inflate(R.menu.pickup, menu);
+ int typeBits = mData.getInt(
+ Gallery.KEY_TYPE_BITS, DataManager.INCLUDE_IMAGE);
+ int id = R.string.select_image;
+ if ((typeBits & DataManager.INCLUDE_VIDEO) != 0) {
+ id = (typeBits & DataManager.INCLUDE_IMAGE) == 0
+ ? R.string.select_video
+ : R.string.select_item;
+ }
+ actionBar.setTitle(id);
+ } else if (mGetAlbum) {
+ inflater.inflate(R.menu.pickup, menu);
+ actionBar.setTitle(R.string.select_album);
+ } else {
+ mShowClusterTabs = !inAlbum;
+ inflater.inflate(R.menu.albumset, menu);
+ if (mTitle != null) {
+ actionBar.setTitle(mTitle);
+ } else {
+ actionBar.setTitle(activity.getApplicationInfo().labelRes);
+ }
+ MenuItem selectItem = menu.findItem(R.id.action_select);
+
+ if (selectItem != null) {
+ boolean selectAlbums = !inAlbum &&
+ actionBar.getClusterTypeAction() == FilterUtils.CLUSTER_BY_ALBUM;
+ if (selectAlbums) {
+ selectItem.setTitle(R.string.select_album);
+ } else {
+ selectItem.setTitle(R.string.select_group);
+ }
+ }
+
+ MenuItem switchCamera = menu.findItem(R.id.action_camera);
+ if (switchCamera != null) {
+ switchCamera.setVisible(GalleryUtils.isCameraAvailable(activity));
+ }
+
+ actionBar.setSubtitle(mSubtitle);
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean onItemSelected(MenuItem item) {
+ Activity activity = (Activity) mActivity;
+ switch (item.getItemId()) {
+ 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: {
+ Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
+ .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_NEW_TASK);
+ activity.startActivity(intent);
+ return true;
+ }
+ case R.id.action_manage_offline: {
+ mHandler.sendEmptyMessage(MSG_GOTO_MANAGE_CACHE_PAGE);
+ 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) {
+ switch (requestCode) {
+ case REQUEST_DO_ANIMATION: {
+ startTransition();
+ }
+ }
+ }
+
+ private void startTransition() {
+ final PositionRepository repository =
+ PositionRepository.getInstance(mActivity);
+ mAlbumSetView.startTransition(new PositionProvider() {
+ private Position mTempPosition = new Position();
+ public Position getPosition(long identity, Position target) {
+ Position p = repository.get(identity);
+ if (p == null) {
+ p = mTempPosition;
+ p.set(target.x, target.y, 128, target.theta, 1);
+ }
+ return p;
+ }
+ });
+ }
+
+ private String getSelectedString() {
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ int count = mSelectionManager.getSelectedCount();
+ int action = actionBar.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);
+ }
+
+ public void onSelectionModeChange(int mode) {
+
+ switch (mode) {
+ case SelectionManager.ENTER_SELECTION_MODE: {
+ mActivity.getGalleryActionBar().hideClusterTabs();
+ mActionMode = mActionModeHandler.startActionMode();
+ break;
+ }
+ case SelectionManager.LEAVE_SELECTION_MODE: {
+ mActionMode.finish();
+ mActivity.getGalleryActionBar().showClusterTabs(this);
+ mRootPane.invalidate();
+ break;
+ }
+ case SelectionManager.SELECT_ALL_MODE: {
+ mActionModeHandler.setTitle(getSelectedString());
+ mRootPane.invalidate();
+ break;
+ }
+ }
+ }
+
+ public void onSelectionChange(Path path, boolean selected) {
+ Utils.assertTrue(mActionMode != null);
+ mActionModeHandler.setTitle(getSelectedString());
+ mActionModeHandler.updateSupportedOperation(path, selected);
+ }
+
+ private void hideDetails() {
+ mShowDetails = false;
+ mAlbumSetView.setSelectionDrawer(mGridDrawer);
+ mDetailsWindow.hide();
+ }
+
+ private void showDetails() {
+ mShowDetails = true;
+ if (mDetailsWindow == null) {
+ mHighlightDrawer = new HighlightDrawer(mActivity.getAndroidContext());
+ mDetailsWindow = new DetailsWindow(mActivity, new MyDetailsSource());
+ mDetailsWindow.setCloseListener(new CloseListener() {
+ public void onClose() {
+ hideDetails();
+ }
+ });
+ mRootPane.addComponent(mDetailsWindow);
+ }
+ mAlbumSetView.setSelectionDrawer(mHighlightDrawer);
+ mDetailsWindow.show();
+ }
+
+ private class MyLoadingListener implements LoadingListener {
+ public void onLoadingStarted() {
+ GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
+ }
+
+ public void onLoadingFinished() {
+ if (!mIsActive) return;
+ GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
+ if (mAlbumSetDataAdapter.size() == 0) {
+ Toast.makeText((Context) mActivity,
+ R.string.empty_album, Toast.LENGTH_LONG).show();
+ if (mActivity.getStateManager().getStateCount() > 1) {
+ mActivity.getStateManager().finishState(AlbumSetPage.this);
+ }
+ }
+ }
+ }
+
+ private class MyDetailsSource implements DetailsWindow.DetailsSource {
+ private int mIndex;
+ public int size() {
+ return mAlbumSetDataAdapter.size();
+ }
+
+ // If requested index is out of active window, suggest a valid index.
+ // If there is no valid index available, return -1.
+ public int findIndex(int indexHint) {
+ if (mAlbumSetDataAdapter.isActive(indexHint)) {
+ mIndex = indexHint;
+ } else {
+ mIndex = mAlbumSetDataAdapter.getActiveStart();
+ if (!mAlbumSetDataAdapter.isActive(mIndex)) {
+ return -1;
+ }
+ }
+ return mIndex;
+ }
+
+ public MediaDetails getDetails() {
+ MediaObject item = mAlbumSetDataAdapter.getMediaSet(mIndex);
+ if (item != null) {
+ mHighlightDrawer.setHighlightItem(item.getPath());
+ return item.getDetails();
+ } else {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/Config.java b/src/com/android/gallery3d/app/Config.java
new file mode 100644
index 000000000..4586235f6
--- /dev/null
+++ b/src/com/android/gallery3d/app/Config.java
@@ -0,0 +1,140 @@
+/*
+ * 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 android.content.Context;
+import android.content.res.Resources;
+
+final class Config {
+ public static class AlbumSetPage {
+ private static AlbumSetPage sInstance;
+
+ public final int slotWidth;
+ public final int slotHeight;
+ public final int displayItemSize;
+ public final int labelFontSize;
+ public final int labelOffsetY;
+ public final int labelMargin;
+
+ public static synchronized AlbumSetPage get(Context context) {
+ if (sInstance == null) {
+ sInstance = new AlbumSetPage(context);
+ }
+ return sInstance;
+ }
+
+ private AlbumSetPage(Context context) {
+ Resources r = context.getResources();
+ slotWidth = r.getDimensionPixelSize(R.dimen.albumset_slot_width);
+ slotHeight = r.getDimensionPixelSize(R.dimen.albumset_slot_height);
+ displayItemSize = r.getDimensionPixelSize(R.dimen.albumset_display_item_size);
+ labelFontSize = r.getDimensionPixelSize(R.dimen.albumset_label_font_size);
+ labelOffsetY = r.getDimensionPixelSize(R.dimen.albumset_label_offset_y);
+ labelMargin = r.getDimensionPixelSize(R.dimen.albumset_label_margin);
+ }
+ }
+
+ public static class AlbumPage {
+ private static AlbumPage sInstance;
+
+ public final int slotWidth;
+ public final int slotHeight;
+ public final int displayItemSize;
+
+ public static synchronized AlbumPage get(Context context) {
+ if (sInstance == null) {
+ sInstance = new AlbumPage(context);
+ }
+ return sInstance;
+ }
+
+ private AlbumPage(Context context) {
+ Resources r = context.getResources();
+ slotWidth = r.getDimensionPixelSize(R.dimen.album_slot_width);
+ slotHeight = r.getDimensionPixelSize(R.dimen.album_slot_height);
+ displayItemSize = r.getDimensionPixelSize(R.dimen.album_display_item_size);
+ }
+ }
+
+ public static class ManageCachePage extends AlbumSetPage {
+ private static ManageCachePage sInstance;
+
+ public final int cacheBarHeight;
+ public final int cacheBarPinLeftMargin;
+ public final int cacheBarPinRightMargin;
+ public final int cacheBarButtonRightMargin;
+ public final int cacheBarFontSize;
+
+ 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();
+ cacheBarHeight = r.getDimensionPixelSize(R.dimen.cache_bar_height);
+ cacheBarPinLeftMargin = r.getDimensionPixelSize(R.dimen.cache_bar_pin_left_margin);
+ cacheBarPinRightMargin = r.getDimensionPixelSize(
+ R.dimen.cache_bar_pin_right_margin);
+ cacheBarButtonRightMargin = r.getDimensionPixelSize(
+ R.dimen.cache_bar_button_right_margin);
+ cacheBarFontSize = r.getDimensionPixelSize(R.dimen.cache_bar_font_size);
+ }
+ }
+
+ public static class PhotoPage {
+ private static PhotoPage sInstance;
+
+ // These are all height values. See the comment in FilmStripView for
+ // the meaning of these values.
+ public final int filmstripTopMargin;
+ public final int filmstripMidMargin;
+ public final int filmstripBottomMargin;
+ public final int filmstripThumbSize;
+ public final int filmstripContentSize;
+ public final int filmstripGripSize;
+ public final int filmstripBarSize;
+
+ // These are width values.
+ public final int filmstripGripWidth;
+
+ public static synchronized PhotoPage get(Context context) {
+ if (sInstance == null) {
+ sInstance = new PhotoPage(context);
+ }
+ return sInstance;
+ }
+
+ public PhotoPage(Context context) {
+ Resources r = context.getResources();
+ filmstripTopMargin = r.getDimensionPixelSize(R.dimen.filmstrip_top_margin);
+ filmstripMidMargin = r.getDimensionPixelSize(R.dimen.filmstrip_mid_margin);
+ filmstripBottomMargin = r.getDimensionPixelSize(R.dimen.filmstrip_bottom_margin);
+ filmstripThumbSize = r.getDimensionPixelSize(R.dimen.filmstrip_thumb_size);
+ filmstripContentSize = r.getDimensionPixelSize(R.dimen.filmstrip_content_size);
+ filmstripGripSize = r.getDimensionPixelSize(R.dimen.filmstrip_grip_size);
+ filmstripBarSize = r.getDimensionPixelSize(R.dimen.filmstrip_bar_size);
+ filmstripGripWidth = r.getDimensionPixelSize(R.dimen.filmstrip_grip_width);
+ }
+ }
+}
+
diff --git a/src/com/android/gallery3d/app/CropImage.java b/src/com/android/gallery3d/app/CropImage.java
new file mode 100644
index 000000000..6c0a0c7eb
--- /dev/null
+++ b/src/com/android/gallery3d/app/CropImage.java
@@ -0,0 +1,850 @@
+/*
+ * 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.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.LocalImage;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.ui.BitmapTileProvider;
+import com.android.gallery3d.ui.CropView;
+import com.android.gallery3d.ui.GLRoot;
+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.GalleryUtils;
+import com.android.gallery3d.util.InterruptableOutputStream;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.app.ProgressDialog;
+import android.app.WallpaperManager;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.Window;
+import android.widget.Toast;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * The activity can crop specific region of interest from an image.
+ */
+public class CropImage extends AbstractGalleryActivity {
+ private static final String TAG = "CropImage";
+ public static final String ACTION_CROP = "com.android.camera.action.CROP";
+
+ private static final int MAX_PIXEL_COUNT = 5 * 1000000; // 5M pixels
+ private static final int MAX_FILE_INDEX = 1000;
+ private static final int TILE_SIZE = 512;
+ private static final int BACKUP_PIXEL_COUNT = 480000; // around 800x600
+
+ private static final int MSG_LARGE_BITMAP = 1;
+ private static final int MSG_BITMAP = 2;
+ private static final int MSG_SAVE_COMPLETE = 3;
+
+ private static final int MAX_BACKUP_IMAGE_SIZE = 320;
+ private static final int DEFAULT_COMPRESS_QUALITY = 90;
+
+ public static final String KEY_RETURN_DATA = "return-data";
+ public static final String KEY_CROPPED_RECT = "cropped-rect";
+ public static final String KEY_ASPECT_X = "aspectX";
+ public static final String KEY_ASPECT_Y = "aspectY";
+ public static final String KEY_SPOTLIGHT_X = "spotlightX";
+ public static final String KEY_SPOTLIGHT_Y = "spotlightY";
+ public static final String KEY_OUTPUT_X = "outputX";
+ public static final String KEY_OUTPUT_Y = "outputY";
+ public static final String KEY_SCALE = "scale";
+ public static final String KEY_DATA = "data";
+ public static final String KEY_SCALE_UP_IF_NEEDED = "scaleUpIfNeeded";
+ public static final String KEY_OUTPUT_FORMAT = "outputFormat";
+ public static final String KEY_SET_AS_WALLPAPER = "set-as-wallpaper";
+ public static final String KEY_NO_FACE_DETECTION = "noFaceDetection";
+
+ private static final String KEY_STATE = "state";
+
+ private static final int STATE_INIT = 0;
+ private static final int STATE_LOADED = 1;
+ private static final int STATE_SAVING = 2;
+
+ public static final String DOWNLOAD_STRING = "download";
+ public static final File DOWNLOAD_BUCKET = new File(
+ Environment.getExternalStorageDirectory(), DOWNLOAD_STRING);
+
+ public static final String CROP_ACTION = "com.android.camera.action.CROP";
+
+ private int mState = STATE_INIT;
+
+ private CropView mCropView;
+
+ private boolean mDoFaceDetection = true;
+
+ private Handler mMainHandler;
+
+ // We keep the following members so that we can free them
+
+ // mBitmap is the unrotated bitmap we pass in to mCropView for detect faces.
+ // mCropView is responsible for rotating it to the way that it is viewed by users.
+ private Bitmap mBitmap;
+ private BitmapTileProvider mBitmapTileProvider;
+ private BitmapRegionDecoder mRegionDecoder;
+ private Bitmap mBitmapInIntent;
+ private boolean mUseRegionDecoder = false;
+
+ private ProgressDialog mProgressDialog;
+ private Future<BitmapRegionDecoder> mLoadTask;
+ private Future<Bitmap> mLoadBitmapTask;
+ private Future<Intent> mSaveTask;
+
+ private MediaItem mMediaItem;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+
+ // Initialize UI
+ setContentView(R.layout.cropimage);
+ mCropView = new CropView(this);
+ getGLRoot().setContentPane(mCropView);
+
+ mMainHandler = new SynchronizedHandler(getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_LARGE_BITMAP: {
+ mProgressDialog.dismiss();
+ onBitmapRegionDecoderAvailable((BitmapRegionDecoder) message.obj);
+ break;
+ }
+ case MSG_BITMAP: {
+ mProgressDialog.dismiss();
+ onBitmapAvailable((Bitmap) message.obj);
+ break;
+ }
+ case MSG_SAVE_COMPLETE: {
+ mProgressDialog.dismiss();
+ setResult(RESULT_OK, (Intent) message.obj);
+ finish();
+ break;
+ }
+ }
+ }
+ };
+
+ setCropParameters();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle saveState) {
+ saveState.putInt(KEY_STATE, mState);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.crop, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.cancel: {
+ setResult(RESULT_CANCELED);
+ finish();
+ break;
+ }
+ case R.id.save: {
+ onSaveClicked();
+ break;
+ }
+ }
+ return true;
+ }
+
+ private class SaveOutput implements Job<Intent> {
+ private RectF mCropRect;
+
+ public SaveOutput(RectF cropRect) {
+ mCropRect = cropRect;
+ }
+
+ public Intent run(JobContext jc) {
+ RectF cropRect = mCropRect;
+ Bundle extra = getIntent().getExtras();
+
+ Rect rect = new Rect(
+ Math.round(cropRect.left), Math.round(cropRect.top),
+ Math.round(cropRect.right), Math.round(cropRect.bottom));
+
+ Intent result = new Intent();
+ result.putExtra(KEY_CROPPED_RECT, rect);
+ Bitmap cropped = null;
+ boolean outputted = false;
+ if (extra != null) {
+ Uri uri = (Uri) extra.getParcelable(MediaStore.EXTRA_OUTPUT);
+ if (uri != null) {
+ if (jc.isCancelled()) return null;
+ outputted = true;
+ cropped = getCroppedImage(rect);
+ if (!saveBitmapToUri(jc, cropped, uri)) return null;
+ }
+ if (extra.getBoolean(KEY_RETURN_DATA, false)) {
+ if (jc.isCancelled()) return null;
+ outputted = true;
+ if (cropped == null) cropped = getCroppedImage(rect);
+ result.putExtra(KEY_DATA, cropped);
+ }
+ if (extra.getBoolean(KEY_SET_AS_WALLPAPER, false)) {
+ if (jc.isCancelled()) return null;
+ outputted = true;
+ if (cropped == null) cropped = getCroppedImage(rect);
+ if (!setAsWallpaper(jc, cropped)) return null;
+ }
+ }
+ if (!outputted) {
+ if (jc.isCancelled()) return null;
+ if (cropped == null) cropped = getCroppedImage(rect);
+ Uri data = saveToMediaProvider(jc, cropped);
+ if (data != null) result.setData(data);
+ }
+ return result;
+ }
+ }
+
+ public static String determineCompressFormat(MediaObject obj) {
+ String compressFormat = "JPEG";
+ if (obj instanceof MediaItem) {
+ String mime = ((MediaItem) obj).getMimeType();
+ if (mime.contains("png") || mime.contains("gif")) {
+ // Set the compress format to PNG for png and gif images
+ // because they may contain alpha values.
+ compressFormat = "PNG";
+ }
+ }
+ return compressFormat;
+ }
+
+ private boolean setAsWallpaper(JobContext jc, Bitmap wallpaper) {
+ try {
+ WallpaperManager.getInstance(this).setBitmap(wallpaper);
+ } catch (IOException e) {
+ Log.w(TAG, "fail to set wall paper", e);
+ }
+ return true;
+ }
+
+ private File saveMedia(
+ JobContext jc, Bitmap cropped, File directory, String filename) {
+ // Try file-1.jpg, file-2.jpg, ... until we find a filename
+ // which does not exist yet.
+ File candidate = null;
+ String fileExtension = getFileExtension();
+ for (int i = 1; i < MAX_FILE_INDEX; ++i) {
+ candidate = new File(directory, filename + "-" + i + "."
+ + fileExtension);
+ try {
+ if (candidate.createNewFile()) break;
+ } catch (IOException e) {
+ Log.e(TAG, "fail to create new file: "
+ + candidate.getAbsolutePath(), e);
+ return null;
+ }
+ }
+ if (!candidate.exists() || !candidate.isFile()) {
+ throw new RuntimeException("cannot create file: " + filename);
+ }
+
+ candidate.setReadable(true, false);
+ candidate.setWritable(true, false);
+
+ try {
+ FileOutputStream fos = new FileOutputStream(candidate);
+ try {
+ saveBitmapToOutputStream(jc, cropped,
+ convertExtensionToCompressFormat(fileExtension), fos);
+ } finally {
+ fos.close();
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "fail to save image: "
+ + candidate.getAbsolutePath(), e);
+ candidate.delete();
+ return null;
+ }
+
+ if (jc.isCancelled()) {
+ candidate.delete();
+ return null;
+ }
+
+ return candidate;
+ }
+
+ private Uri saveToMediaProvider(JobContext jc, Bitmap cropped) {
+ if (PicasaSource.isPicasaImage(mMediaItem)) {
+ return savePicasaImage(jc, cropped);
+ } else if (mMediaItem instanceof LocalImage) {
+ return saveLocalImage(jc, cropped);
+ } else {
+ Log.w(TAG, "no output for crop image " + mMediaItem);
+ return null;
+ }
+ }
+
+ private Uri savePicasaImage(JobContext jc, Bitmap cropped) {
+ if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) {
+ throw new RuntimeException("cannot create download folder");
+ }
+
+ String filename = PicasaSource.getImageTitle(mMediaItem);
+ int pos = filename.lastIndexOf('.');
+ if (pos >= 0) filename = filename.substring(0, pos);
+ File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename);
+ if (output == null) return null;
+
+ long now = System.currentTimeMillis() / 1000;
+ ContentValues values = new ContentValues();
+ values.put(Images.Media.TITLE, PicasaSource.getImageTitle(mMediaItem));
+ values.put(Images.Media.DISPLAY_NAME, output.getName());
+ values.put(Images.Media.DATE_TAKEN, PicasaSource.getDateTaken(mMediaItem));
+ values.put(Images.Media.DATE_MODIFIED, now);
+ values.put(Images.Media.DATE_ADDED, now);
+ values.put(Images.Media.MIME_TYPE, "image/jpeg");
+ values.put(Images.Media.ORIENTATION, 0);
+ values.put(Images.Media.DATA, output.getAbsolutePath());
+ values.put(Images.Media.SIZE, output.length());
+
+ double latitude = PicasaSource.getLatitude(mMediaItem);
+ double longitude = PicasaSource.getLongitude(mMediaItem);
+ if (GalleryUtils.isValidLocation(latitude, longitude)) {
+ values.put(Images.Media.LATITUDE, latitude);
+ values.put(Images.Media.LONGITUDE, longitude);
+ }
+ return getContentResolver().insert(
+ Images.Media.EXTERNAL_CONTENT_URI, values);
+ }
+
+ private Uri saveLocalImage(JobContext jc, Bitmap cropped) {
+ LocalImage localImage = (LocalImage) mMediaItem;
+
+ File oldPath = new File(localImage.filePath);
+ File directory = new File(oldPath.getParent());
+
+ String filename = oldPath.getName();
+ int pos = filename.lastIndexOf('.');
+ if (pos >= 0) filename = filename.substring(0, pos);
+ File output = saveMedia(jc, cropped, directory, filename);
+ if (output == null) return null;
+
+ long now = System.currentTimeMillis() / 1000;
+ ContentValues values = new ContentValues();
+ values.put(Images.Media.TITLE, localImage.caption);
+ values.put(Images.Media.DISPLAY_NAME, output.getName());
+ values.put(Images.Media.DATE_TAKEN, localImage.dateTakenInMs);
+ values.put(Images.Media.DATE_MODIFIED, now);
+ values.put(Images.Media.DATE_ADDED, now);
+ values.put(Images.Media.MIME_TYPE, "image/jpeg");
+ values.put(Images.Media.ORIENTATION, 0);
+ values.put(Images.Media.DATA, output.getAbsolutePath());
+ values.put(Images.Media.SIZE, output.length());
+
+ if (GalleryUtils.isValidLocation(localImage.latitude, localImage.longitude)) {
+ values.put(Images.Media.LATITUDE, localImage.latitude);
+ values.put(Images.Media.LONGITUDE, localImage.longitude);
+ }
+ return getContentResolver().insert(
+ Images.Media.EXTERNAL_CONTENT_URI, values);
+ }
+
+ private boolean saveBitmapToOutputStream(
+ JobContext jc, Bitmap bitmap, CompressFormat format, OutputStream os) {
+ // We wrap the OutputStream so that it can be interrupted.
+ final InterruptableOutputStream ios = new InterruptableOutputStream(os);
+ jc.setCancelListener(new CancelListener() {
+ public void onCancel() {
+ ios.interrupt();
+ }
+ });
+ try {
+ bitmap.compress(format, DEFAULT_COMPRESS_QUALITY, os);
+ if (!jc.isCancelled()) return false;
+ } finally {
+ jc.setCancelListener(null);
+ Utils.closeSilently(os);
+ }
+ return false;
+ }
+
+ private boolean saveBitmapToUri(JobContext jc, Bitmap bitmap, Uri uri) {
+ try {
+ return saveBitmapToOutputStream(jc, bitmap,
+ convertExtensionToCompressFormat(getFileExtension()),
+ getContentResolver().openOutputStream(uri));
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "cannot write output", e);
+ }
+ return true;
+ }
+
+ private CompressFormat convertExtensionToCompressFormat(String extension) {
+ return extension.equals("png")
+ ? CompressFormat.PNG
+ : CompressFormat.JPEG;
+ }
+
+ private String getFileExtension() {
+ String requestFormat = getIntent().getStringExtra(KEY_OUTPUT_FORMAT);
+ String outputFormat = (requestFormat == null)
+ ? determineCompressFormat(mMediaItem)
+ : requestFormat;
+
+ outputFormat = outputFormat.toLowerCase();
+ return (outputFormat.equals("png") || outputFormat.equals("gif"))
+ ? "png" // We don't support gif compression.
+ : "jpg";
+ }
+
+ private void onSaveClicked() {
+ Bundle extra = getIntent().getExtras();
+ RectF cropRect = mCropView.getCropRectangle();
+ if (cropRect == null) return;
+ mState = STATE_SAVING;
+ int messageId = extra != null && extra.getBoolean(KEY_SET_AS_WALLPAPER)
+ ? R.string.wallpaper
+ : R.string.saving_image;
+ mProgressDialog = ProgressDialog.show(
+ this, null, getString(messageId), true, false);
+ mSaveTask = getThreadPool().submit(new SaveOutput(cropRect),
+ new FutureListener<Intent>() {
+ public void onFutureDone(Future<Intent> future) {
+ mSaveTask = null;
+ if (future.get() == null) return;
+ mMainHandler.sendMessage(mMainHandler.obtainMessage(
+ MSG_SAVE_COMPLETE, future.get()));
+ }
+ });
+ }
+
+ private Bitmap getCroppedImage(Rect rect) {
+ Utils.assertTrue(rect.width() > 0 && rect.height() > 0);
+
+ Bundle extras = getIntent().getExtras();
+ // (outputX, outputY) = the width and height of the returning bitmap.
+ int outputX = rect.width();
+ int outputY = rect.height();
+ if (extras != null) {
+ outputX = extras.getInt(KEY_OUTPUT_X, outputX);
+ outputY = extras.getInt(KEY_OUTPUT_Y, outputY);
+ }
+
+ if (outputX * outputY > MAX_PIXEL_COUNT) {
+ float scale = (float) Math.sqrt(
+ (double) MAX_PIXEL_COUNT / outputX / outputY);
+ Log.w(TAG, "scale down the cropped image: " + scale);
+ outputX = Math.round(scale * outputX);
+ outputY = Math.round(scale * outputY);
+ }
+
+ // (rect.width() * scaleX, rect.height() * scaleY) =
+ // the size of drawing area in output bitmap
+ float scaleX = 1;
+ float scaleY = 1;
+ Rect dest = new Rect(0, 0, outputX, outputY);
+ if (extras == null || extras.getBoolean(KEY_SCALE, true)) {
+ scaleX = (float) outputX / rect.width();
+ scaleY = (float) outputY / rect.height();
+ if (extras == null || !extras.getBoolean(
+ KEY_SCALE_UP_IF_NEEDED, false)) {
+ if (scaleX > 1f) scaleX = 1;
+ if (scaleY > 1f) scaleY = 1;
+ }
+ }
+
+ // Keep the content in the center (or crop the content)
+ int rectWidth = Math.round(rect.width() * scaleX);
+ int rectHeight = Math.round(rect.height() * scaleY);
+ dest.set(Math.round((outputX - rectWidth) / 2f),
+ Math.round((outputY - rectHeight) / 2f),
+ Math.round((outputX + rectWidth) / 2f),
+ Math.round((outputY + rectHeight) / 2f));
+
+ if (mBitmapInIntent != null) {
+ Bitmap source = mBitmapInIntent;
+ Bitmap result = Bitmap.createBitmap(
+ outputX, outputY, Config.ARGB_8888);
+ Canvas canvas = new Canvas(result);
+ canvas.drawBitmap(source, rect, dest, null);
+ return result;
+ }
+
+ int rotation = mMediaItem.getRotation();
+ rotateRectangle(rect, mCropView.getImageWidth(),
+ mCropView.getImageHeight(), 360 - rotation);
+ rotateRectangle(dest, outputX, outputY, 360 - rotation);
+ if (mUseRegionDecoder) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ int sample = BitmapUtils.computeSampleSizeLarger(
+ Math.max(scaleX, scaleY));
+ options.inSampleSize = sample;
+ if ((rect.width() / sample) == dest.width()
+ && (rect.height() / sample) == dest.height()
+ && rotation == 0) {
+ // To prevent concurrent access in GLThread
+ synchronized (mRegionDecoder) {
+ return mRegionDecoder.decodeRegion(rect, options);
+ }
+ }
+ Bitmap result = Bitmap.createBitmap(
+ outputX, outputY, Config.ARGB_8888);
+ Canvas canvas = new Canvas(result);
+ rotateCanvas(canvas, outputX, outputY, rotation);
+ drawInTiles(canvas, mRegionDecoder, rect, dest, sample);
+ return result;
+ } else {
+ Bitmap result = Bitmap.createBitmap(outputX, outputY, Config.ARGB_8888);
+ Canvas canvas = new Canvas(result);
+ rotateCanvas(canvas, outputX, outputY, rotation);
+ canvas.drawBitmap(mBitmap,
+ rect, dest, new Paint(Paint.FILTER_BITMAP_FLAG));
+ return result;
+ }
+ }
+
+ private static void rotateCanvas(
+ Canvas canvas, int width, int height, int rotation) {
+ canvas.translate(width / 2, height / 2);
+ canvas.rotate(rotation);
+ if (((rotation / 90) & 0x01) == 0) {
+ canvas.translate(-width / 2, -height / 2);
+ } else {
+ canvas.translate(-height / 2, -width / 2);
+ }
+ }
+
+ private static void rotateRectangle(
+ Rect rect, int width, int height, int rotation) {
+ if (rotation == 0 || rotation == 360) return;
+
+ int w = rect.width();
+ int h = rect.height();
+ switch (rotation) {
+ case 90: {
+ rect.top = rect.left;
+ rect.left = height - rect.bottom;
+ rect.right = rect.left + h;
+ rect.bottom = rect.top + w;
+ return;
+ }
+ case 180: {
+ rect.left = width - rect.right;
+ rect.top = height - rect.bottom;
+ rect.right = rect.left + w;
+ rect.bottom = rect.top + h;
+ return;
+ }
+ case 270: {
+ rect.left = rect.top;
+ rect.top = width - rect.right;
+ rect.right = rect.left + h;
+ rect.bottom = rect.top + w;
+ return;
+ }
+ default: throw new AssertionError();
+ }
+ }
+
+ private void drawInTiles(Canvas canvas,
+ BitmapRegionDecoder decoder, Rect rect, Rect dest, int sample) {
+ int tileSize = TILE_SIZE * sample;
+ Rect tileRect = new Rect();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Config.ARGB_8888;
+ options.inSampleSize = sample;
+ canvas.translate(dest.left, dest.top);
+ canvas.scale((float) sample * dest.width() / rect.width(),
+ (float) sample * dest.height() / rect.height());
+ Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
+ for (int tx = rect.left, x = 0;
+ tx < rect.right; tx += tileSize, x += TILE_SIZE) {
+ for (int ty = rect.top, y = 0;
+ ty < rect.bottom; ty += tileSize, y += TILE_SIZE) {
+ tileRect.set(tx, ty, tx + tileSize, ty + tileSize);
+ if (tileRect.intersect(rect)) {
+ Bitmap bitmap;
+
+ // To prevent concurrent access in GLThread
+ synchronized (decoder) {
+ bitmap = decoder.decodeRegion(tileRect, options);
+ }
+ canvas.drawBitmap(bitmap, x, y, paint);
+ bitmap.recycle();
+ }
+ }
+ }
+ }
+
+ private void onBitmapRegionDecoderAvailable(
+ BitmapRegionDecoder regionDecoder) {
+
+ if (regionDecoder == null) {
+ Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+ mRegionDecoder = regionDecoder;
+ mUseRegionDecoder = true;
+ mState = STATE_LOADED;
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ int width = regionDecoder.getWidth();
+ int height = regionDecoder.getHeight();
+ options.inSampleSize = BitmapUtils.computeSampleSize(width, height,
+ BitmapUtils.UNCONSTRAINED, BACKUP_PIXEL_COUNT);
+ mBitmap = regionDecoder.decodeRegion(
+ new Rect(0, 0, width, height), options);
+ mCropView.setDataModel(new TileImageViewAdapter(
+ mBitmap, regionDecoder), mMediaItem.getRotation());
+ if (mDoFaceDetection) {
+ mCropView.detectFaces(mBitmap);
+ } else {
+ mCropView.initializeHighlightRectangle();
+ }
+ }
+
+ private void onBitmapAvailable(Bitmap bitmap) {
+ if (bitmap == null) {
+ Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+ mUseRegionDecoder = false;
+ mState = STATE_LOADED;
+
+ mBitmap = bitmap;
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ mCropView.setDataModel(new BitmapTileProvider(bitmap, 512),
+ mMediaItem.getRotation());
+ if (mDoFaceDetection) {
+ mCropView.detectFaces(bitmap);
+ } else {
+ mCropView.initializeHighlightRectangle();
+ }
+ }
+
+ private void setCropParameters() {
+ Bundle extras = getIntent().getExtras();
+ if (extras == null)
+ return;
+ int aspectX = extras.getInt(KEY_ASPECT_X, 0);
+ int aspectY = extras.getInt(KEY_ASPECT_Y, 0);
+ if (aspectX != 0 && aspectY != 0) {
+ mCropView.setAspectRatio((float) aspectX / aspectY);
+ }
+
+ float spotlightX = extras.getFloat(KEY_SPOTLIGHT_X, 0);
+ float spotlightY = extras.getFloat(KEY_SPOTLIGHT_Y, 0);
+ if (spotlightX != 0 && spotlightY != 0) {
+ mCropView.setSpotlightRatio(spotlightX, spotlightY);
+ }
+ }
+
+ private void initializeData() {
+ Bundle extras = getIntent().getExtras();
+
+ if (extras != null) {
+ if (extras.containsKey(KEY_NO_FACE_DETECTION)) {
+ mDoFaceDetection = !extras.getBoolean(KEY_NO_FACE_DETECTION);
+ }
+
+ mBitmapInIntent = extras.getParcelable(KEY_DATA);
+
+ if (mBitmapInIntent != null) {
+ mBitmapTileProvider =
+ new BitmapTileProvider(mBitmapInIntent, MAX_BACKUP_IMAGE_SIZE);
+ mCropView.setDataModel(mBitmapTileProvider, 0);
+ if (mDoFaceDetection) {
+ mCropView.detectFaces(mBitmapInIntent);
+ } else {
+ mCropView.initializeHighlightRectangle();
+ }
+ mState = STATE_LOADED;
+ return;
+ }
+ }
+
+ mProgressDialog = ProgressDialog.show(
+ this, null, getString(R.string.loading_image), true, false);
+
+ mMediaItem = getMediaItemFromIntentData();
+ if (mMediaItem == null) return;
+
+ boolean supportedByBitmapRegionDecoder =
+ (mMediaItem.getSupportedOperations() & MediaItem.SUPPORT_FULL_IMAGE) != 0;
+ if (supportedByBitmapRegionDecoder) {
+ mLoadTask = getThreadPool().submit(new LoadDataTask(mMediaItem),
+ new FutureListener<BitmapRegionDecoder>() {
+ public void onFutureDone(Future<BitmapRegionDecoder> future) {
+ mLoadTask = null;
+ BitmapRegionDecoder decoder = future.get();
+ if (future.isCancelled()) {
+ if (decoder != null) decoder.recycle();
+ return;
+ }
+ mMainHandler.sendMessage(mMainHandler.obtainMessage(
+ MSG_LARGE_BITMAP, decoder));
+ }
+ });
+ } else {
+ mLoadBitmapTask = getThreadPool().submit(new LoadBitmapDataTask(mMediaItem),
+ new FutureListener<Bitmap>() {
+ public void onFutureDone(Future<Bitmap> future) {
+ mLoadBitmapTask = null;
+ Bitmap bitmap = future.get();
+ if (future.isCancelled()) {
+ if (bitmap != null) bitmap.recycle();
+ return;
+ }
+ mMainHandler.sendMessage(mMainHandler.obtainMessage(
+ MSG_BITMAP, bitmap));
+ }
+ });
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (mState == STATE_INIT) initializeData();
+ if (mState == STATE_SAVING) onSaveClicked();
+
+ // TODO: consider to do it in GLView system
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ mCropView.resume();
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ Future<BitmapRegionDecoder> loadTask = mLoadTask;
+ if (loadTask != null && !loadTask.isDone()) {
+ // load in progress, try to cancel it
+ loadTask.cancel();
+ loadTask.waitDone();
+ mProgressDialog.dismiss();
+ }
+
+ Future<Bitmap> loadBitmapTask = mLoadBitmapTask;
+ if (loadBitmapTask != null && !loadBitmapTask.isDone()) {
+ // load in progress, try to cancel it
+ loadBitmapTask.cancel();
+ loadBitmapTask.waitDone();
+ mProgressDialog.dismiss();
+ }
+
+ Future<Intent> saveTask = mSaveTask;
+ if (saveTask != null && !saveTask.isDone()) {
+ // save in progress, try to cancel it
+ saveTask.cancel();
+ saveTask.waitDone();
+ mProgressDialog.dismiss();
+ }
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ mCropView.pause();
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ private MediaItem getMediaItemFromIntentData() {
+ Uri uri = getIntent().getData();
+ DataManager manager = getDataManager();
+ if (uri == null) {
+ Log.w(TAG, "no data given");
+ return null;
+ }
+ Path path = manager.findPathByUri(uri);
+ if (path == null) {
+ Log.w(TAG, "cannot get path for: " + uri);
+ return null;
+ }
+ return (MediaItem) manager.getMediaObject(path);
+ }
+
+ private class LoadDataTask implements Job<BitmapRegionDecoder> {
+ MediaItem mItem;
+
+ public LoadDataTask(MediaItem item) {
+ mItem = item;
+ }
+
+ public BitmapRegionDecoder run(JobContext jc) {
+ return mItem == null ? null : mItem.requestLargeImage().run(jc);
+ }
+ }
+
+ private class LoadBitmapDataTask implements Job<Bitmap> {
+ MediaItem mItem;
+
+ public LoadBitmapDataTask(MediaItem item) {
+ mItem = item;
+ }
+ public Bitmap run(JobContext jc) {
+ return mItem == null
+ ? null
+ : mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/DialogPicker.java b/src/com/android/gallery3d/app/DialogPicker.java
new file mode 100644
index 000000000..ebfc52158
--- /dev/null
+++ b/src/com/android/gallery3d/app/DialogPicker.java
@@ -0,0 +1,68 @@
+/*
+ * 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.ui.GLRoot;
+import com.android.gallery3d.ui.GLRootView;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+public class DialogPicker extends AbstractGalleryActivity
+ implements OnClickListener {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.dialog_picker);
+ ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true);
+ findViewById(R.id.cancel).setOnClickListener(this);
+
+ 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);
+ }
+
+ @Override
+ public void onBackPressed() {
+ // send the back event to the top sub-state
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ getStateManager().getTopState().onBackPressed();
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.cancel) finish();
+ }
+}
diff --git a/src/com/android/gallery3d/app/EyePosition.java b/src/com/android/gallery3d/app/EyePosition.java
new file mode 100644
index 000000000..1c3aa60bb
--- /dev/null
+++ b/src/com/android/gallery3d/app/EyePosition.java
@@ -0,0 +1,218 @@
+/*
+ * 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.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+
+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.view.Display;
+import android.view.Surface;
+import android.view.WindowManager;
+
+public class EyePosition {
+ 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 double USER_ANGEL = Math.toRadians(10);
+ private static final float USER_ANGEL_COS = (float) Math.cos(USER_ANGEL);
+ private static final float USER_ANGEL_SIN = (float) Math.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();
+
+ 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 = (float) Math.sqrt(tx * tx + ty * ty + tz * tz);
+ float glength = (float) Math.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 = (float) -Math.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 = (float) -Math.sqrt(
+ mUserDistance * mUserDistance - mX * mX - mY * mY);
+ mListener.onEyePositionChanged(mX, mY, mZ);
+ }
+
+ private class PositionListener implements SensorEventListener {
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+
+ 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/FilterUtils.java b/src/com/android/gallery3d/app/FilterUtils.java
new file mode 100644
index 000000000..9b8ea2d62
--- /dev/null
+++ b/src/com/android/gallery3d/app/FilterUtils.java
@@ -0,0 +1,296 @@
+/*
+ * 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 {
+ 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 model, 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(model, CLUSTER_BY_TIME,
+ (ctype & CLUSTER_BY_TIME) != 0, (ccurrent & CLUSTER_BY_TIME) != 0);
+ setMenuItemApplied(model, CLUSTER_BY_LOCATION,
+ (ctype & CLUSTER_BY_LOCATION) != 0, (ccurrent & CLUSTER_BY_LOCATION) != 0);
+ setMenuItemApplied(model, CLUSTER_BY_TAG,
+ (ctype & CLUSTER_BY_TAG) != 0, (ccurrent & CLUSTER_BY_TAG) != 0);
+ setMenuItemApplied(model, CLUSTER_BY_FACE,
+ (ctype & CLUSTER_BY_FACE) != 0, (ccurrent & CLUSTER_BY_FACE) != 0);
+
+ model.setClusterItemVisibility(CLUSTER_BY_ALBUM, !inAlbum || ctype == 0);
+
+ setMenuItemApplied(model, 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(model, R.string.show_images_only,
+ (ftype & FILTER_IMAGE_ONLY) != 0,
+ (ftype & FILTER_IMAGE_ONLY) == 0 && ftypef == 0,
+ (fcurrent & FILTER_IMAGE_ONLY) != 0);
+ setMenuItemAppliedEnabled(model, R.string.show_videos_only,
+ (ftype & FILTER_VIDEO_ONLY) != 0,
+ (ftype & FILTER_VIDEO_ONLY) == 0 && ftypef == 0,
+ (fcurrent & FILTER_VIDEO_ONLY) != 0);
+ setMenuItemAppliedEnabled(model, 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 filter to the specified type.
+ public static String switchFilterPath(String base, int filterType) {
+ return newFilterPath(removeOneFilterFromPath(base), filterType);
+ }
+
+ // 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();
+ }
+
+ // Remove the topmost filter (if any) from the path.
+ private static String removeOneFilterFromPath(String base) {
+ boolean[] done = new boolean[1];
+ return removeOneFilterFromPath(base, done);
+ }
+
+ private static String removeOneFilterFromPath(String base, boolean[] done) {
+ if (done[0]) return base;
+
+ String[] segments = Path.split(base);
+ if (segments[0].equals("filter") && segments[1].equals("mediatype")) {
+ done[0] = true;
+ return Path.splitSequence(segments[3])[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(removeOneFilterFromPath(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..2c5263b03
--- /dev/null
+++ b/src/com/android/gallery3d/app/Gallery.java
@@ -0,0 +1,232 @@
+/*
+ * 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 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.ui.GLRoot;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.app.ActionBar;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.Window;
+import android.widget.Toast;
+
+public final class Gallery extends AbstractGalleryActivity {
+ public static final String EXTRA_SLIDESHOW = "slideshow";
+ 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";
+
+ private static final String TAG = "Gallery";
+ private GalleryActionBar mActionBar;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+
+ setContentView(R.layout.main);
+ mActionBar = new GalleryActionBar(this);
+
+ 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() {
+ Bundle data = new Bundle();
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+ getDataManager().getTopSetPath(DataManager.INCLUDE_ALL));
+ getStateManager().startState(AlbumSetPage.class, data);
+ }
+
+ 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 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());
+ 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);
+ 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 (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 albumPath = dm.findPathByUri(uri);
+ if (albumPath != null) {
+ MediaSet mediaSet = (MediaSet) dm.getMediaObject(albumPath);
+ data.putString(AlbumPage.KEY_MEDIA_PATH, albumPath.toString());
+ getStateManager().startState(AlbumPage.class, data);
+ } else {
+ startDefaultPage();
+ }
+ } else {
+ Path itemPath = dm.findPathByUri(uri);
+ Path albumPath = dm.getDefaultSetOf(itemPath);
+ if (albumPath != null) {
+ data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
+ albumPath.toString());
+ }
+ data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, itemPath.toString());
+ getStateManager().startState(PhotoPage.class, data);
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ return getStateManager().createOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ return getStateManager().itemSelected(item);
+ } finally {
+ root.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();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ getStateManager().destroy();
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ Utils.assertTrue(getStateManager().getStateCount() > 0);
+ super.onResume();
+ }
+
+ @Override
+ public GalleryActionBar getGalleryActionBar() {
+ return mActionBar;
+ }
+}
diff --git a/src/com/android/gallery3d/app/GalleryActionBar.java b/src/com/android/gallery3d/app/GalleryActionBar.java
new file mode 100644
index 000000000..b9b59ee39
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryActionBar.java
@@ -0,0 +1,218 @@
+/*
+ * 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 java.util.ArrayList;
+
+import com.android.gallery3d.R;
+
+import android.app.ActionBar;
+import android.app.ActionBar.Tab;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.ShareActionProvider;
+
+public class GalleryActionBar implements ActionBar.TabListener {
+ private static final String TAG = "GalleryActionBar";
+
+ public interface ClusterRunner {
+ public void doCluster(int id);
+ }
+
+ private static class ActionItem {
+ public int action;
+ public boolean enabled;
+ public boolean visible;
+ public int tabTitle;
+ 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 tabTitle,
+ int dialogTitle, int clusterBy) {
+ this.action = action;
+ this.enabled = enabled;
+ this.tabTitle = tabTitle;
+ 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 ClusterRunner mClusterRunner;
+ private CharSequence[] mTitles;
+ private ArrayList<Integer> mActions;
+ private Context mContext;
+ private ActionBar mActionBar;
+ // We need this because ActionBar.getSelectedTab() doesn't work when
+ // ActionBar is hidden.
+ private Tab mCurrentTab;
+
+ public GalleryActionBar(Activity activity) {
+ mActionBar = activity.getActionBar();
+ mContext = activity;
+
+ for (ActionItem item : sClusterItems) {
+ mActionBar.addTab(mActionBar.newTab().setText(item.tabTitle).
+ setTag(item).setTabListener(this));
+ }
+ }
+
+ public static int getHeight(Activity activity) {
+ ActionBar actionBar = activity.getActionBar();
+ return actionBar != null ? actionBar.getHeight() : 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 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() {
+ if (mCurrentTab != null) {
+ ActionItem item = (ActionItem) mCurrentTab.getTag();
+ return item.action;
+ }
+ // By default, it's group-by-album
+ return FilterUtils.CLUSTER_BY_ALBUM;
+ }
+
+ public static String getClusterByTypeString(Context context, int type) {
+ for (ActionItem item : sClusterItems) {
+ if (item.action == type) {
+ return context.getString(item.clusterBy);
+ }
+ }
+ return null;
+ }
+
+ public static ShareActionProvider initializeShareActionProvider(Menu menu) {
+ MenuItem item = menu.findItem(R.id.action_share);
+ ShareActionProvider shareActionProvider = null;
+ if (item != null) {
+ shareActionProvider = (ShareActionProvider) item.getActionProvider();
+ shareActionProvider.setShareHistoryFileName(
+ ShareActionProvider.DEFAULT_SHARE_HISTORY_FILE_NAME);
+ }
+ return shareActionProvider;
+ }
+
+ public void showClusterTabs(ClusterRunner runner) {
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ mClusterRunner = runner;
+ }
+
+ public void hideClusterTabs() {
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ mClusterRunner = null;
+ }
+
+ 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() {
+ public void onClick(DialogInterface dialog, int which) {
+ clusterRunner.doCluster(actions.get(which).intValue());
+ }
+ }).create().show();
+ }
+
+ public void setTitle(String title) {
+ if (mActionBar != null) mActionBar.setTitle(title);
+ }
+
+ public void setTitle(int titleId) {
+ if (mActionBar != null) mActionBar.setTitle(titleId);
+ }
+
+ public void setSubtitle(String title) {
+ if (mActionBar != null) mActionBar.setSubtitle(title);
+ }
+
+ public void setNavigationMode(int mode) {
+ if (mActionBar != null) mActionBar.setNavigationMode(mode);
+ }
+
+ public int getHeight() {
+ return mActionBar == null ? 0 : mActionBar.getHeight();
+ }
+
+ @Override
+ public void onTabSelected(Tab tab, FragmentTransaction ft) {
+ if (mCurrentTab == tab) return;
+ mCurrentTab = tab;
+ ActionItem item = (ActionItem) tab.getTag();
+ if (mClusterRunner != null) mClusterRunner.doCluster(item.action);
+ }
+
+ @Override
+ public void onTabUnselected(Tab tab, FragmentTransaction ft) {
+ }
+
+ @Override
+ public void onTabReselected(Tab tab, FragmentTransaction ft) {
+ }
+}
diff --git a/src/com/android/gallery3d/app/GalleryActivity.java b/src/com/android/gallery3d/app/GalleryActivity.java
new file mode 100644
index 000000000..02f2f72f3
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryActivity.java
@@ -0,0 +1,28 @@
+/*
+ * 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.ui.GLRoot;
+import com.android.gallery3d.ui.PositionRepository;
+
+public interface GalleryActivity extends GalleryContext {
+ public StateManager getStateManager();
+ public GLRoot getGLRoot();
+ public PositionRepository getPositionRepository();
+ public GalleryApp getGalleryApplication();
+ public GalleryActionBar getGalleryActionBar();
+}
diff --git a/src/com/android/gallery3d/app/GalleryApp.java b/src/com/android/gallery3d/app/GalleryApp.java
new file mode 100644
index 000000000..b3a305e53
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryApp.java
@@ -0,0 +1,39 @@
+/*
+ * 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.data.DataManager;
+import com.android.gallery3d.data.DownloadCache;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.util.ThreadPool;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Looper;
+
+public interface GalleryApp {
+ public DataManager getDataManager();
+ 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..a11d92017
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryAppImpl.java
@@ -0,0 +1,90 @@
+/*
+ * 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.data.DataManager;
+import com.android.gallery3d.data.DownloadCache;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.widget.WidgetUtils;
+
+import android.app.Application;
+import android.content.Context;
+
+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 DataManager mDataManager;
+ private ThreadPool mThreadPool;
+ private DownloadCache mDownloadCache;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ GalleryUtils.initialize(this);
+ WidgetUtils.initialize(this);
+ PicasaSource.initialize(this);
+ }
+
+ public Context getAndroidContext() {
+ return this;
+ }
+
+ public synchronized DataManager getDataManager() {
+ if (mDataManager == null) {
+ mDataManager = new DataManager(this);
+ mDataManager.initializeSourceMap();
+ }
+ return mDataManager;
+ }
+
+ public synchronized ImageCacheService getImageCacheService() {
+ if (mImageCacheService == null) {
+ mImageCacheService = new ImageCacheService(getAndroidContext());
+ }
+ return mImageCacheService;
+ }
+
+ public synchronized ThreadPool getThreadPool() {
+ if (mThreadPool == null) {
+ mThreadPool = new ThreadPool();
+ }
+ return mThreadPool;
+ }
+
+ 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;
+ }
+}
diff --git a/src/com/android/gallery3d/app/GalleryContext.java b/src/com/android/gallery3d/app/GalleryContext.java
new file mode 100644
index 000000000..022b4a704
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryContext.java
@@ -0,0 +1,38 @@
+/*
+ * 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.data.DataManager;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.util.ThreadPool;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Looper;
+
+public interface GalleryContext {
+ public ImageCacheService getImageCacheService();
+ public DataManager getDataManager();
+
+ public Context getAndroidContext();
+
+ public Looper getMainLooper();
+ public Resources getResources();
+ public ContentResolver getContentResolver();
+ 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..ecbd798d2
--- /dev/null
+++ b/src/com/android/gallery3d/app/LoadingListener.java
@@ -0,0 +1,22 @@
+/*
+ * 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();
+ public void onLoadingFinished();
+}
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..a0190db77
--- /dev/null
+++ b/src/com/android/gallery3d/app/ManageCachePage.java
@@ -0,0 +1,271 @@
+/*
+ * 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.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.AlbumSetView;
+import com.android.gallery3d.ui.CacheBarView;
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.ManageCacheDrawer;
+import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.SelectionDrawer;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.StaticBackground;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+
+public class ManageCachePage extends ActivityState implements
+ SelectionManager.SelectionListener, CacheBarView.Listener,
+ MenuExecutor.ProgressListener, EyePosition.EyePositionListener {
+ public static final String KEY_MEDIA_PATH = "media-path";
+ private static final String TAG = "ManageCachePage";
+
+ private static final float USER_DISTANCE_METER = 0.3f;
+ private static final int DATA_CACHE_SIZE = 256;
+
+ private StaticBackground mStaticBackground;
+ private AlbumSetView mAlbumSetView;
+
+ private MediaSet mMediaSet;
+
+ protected SelectionManager mSelectionManager;
+ protected SelectionDrawer mSelectionDrawer;
+ private AlbumSetDataAdapter mAlbumSetDataAdapter;
+ private float mUserDistance; // in pixel
+
+ private CacheBarView mCacheBar;
+
+ 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 GLView mRootPane = new GLView() {
+ private float mMatrix[] = new float[16];
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ mStaticBackground.layout(0, 0, right - left, bottom - top);
+ mEyePosition.resetPosition();
+
+ Config.ManageCachePage config = Config.ManageCachePage.get((Context) mActivity);
+
+ ActionBar actionBar = ((Activity) mActivity).getActionBar();
+ int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity);
+ int slotViewBottom = bottom - top - config.cacheBarHeight;
+
+ mAlbumSetView.layout(0, slotViewTop, right - left, slotViewBottom);
+ mCacheBar.layout(0, bottom - top - config.cacheBarHeight,
+ right - left, bottom - top);
+ }
+
+ @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();
+ }
+ };
+
+ public void onEyePositionChanged(float x, float y, float z) {
+ mRootPane.lockRendering();
+ mX = x;
+ mY = y;
+ mZ = z;
+ mRootPane.unlockRendering();
+ mRootPane.invalidate();
+ }
+
+ 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();
+ if (isFullyCached ^ isSelected) {
+ mCacheBar.increaseTargetCacheSize(-sizeOfTarget);
+ } else {
+ mCacheBar.increaseTargetCacheSize(sizeOfTarget);
+ }
+
+ mSelectionManager.toggle(path);
+ mAlbumSetView.invalidate();
+ }
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ initializeViews();
+ initializeData(data);
+ mEyePosition = new EyePosition(mActivity.getAndroidContext(), this);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mAlbumSetDataAdapter.pause();
+ mAlbumSetView.pause();
+ mCacheBar.pause();
+ mEyePosition.pause();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ setContentPane(mRootPane);
+ mAlbumSetDataAdapter.resume();
+ mAlbumSetView.resume();
+ mCacheBar.resume();
+ mEyePosition.resume();
+ }
+
+ private void initializeData(Bundle data) {
+ mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER);
+ 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 AlbumSetDataAdapter(
+ mActivity, mMediaSet, DATA_CACHE_SIZE);
+ mAlbumSetView.setModel(mAlbumSetDataAdapter);
+ }
+
+ private void initializeViews() {
+ mSelectionManager = new SelectionManager(mActivity, true);
+ mSelectionManager.setSelectionListener(this);
+ mStaticBackground = new StaticBackground(mActivity.getAndroidContext());
+ mRootPane.addComponent(mStaticBackground);
+
+ mSelectionDrawer = new ManageCacheDrawer(
+ (Context) mActivity, mSelectionManager);
+ Config.ManageCachePage config = Config.ManageCachePage.get((Context) mActivity);
+ mAlbumSetView = new AlbumSetView(mActivity, mSelectionDrawer,
+ config.slotWidth, config.slotHeight,
+ config.displayItemSize, config.labelFontSize,
+ config.labelOffsetY, config.labelMargin);
+ mAlbumSetView.setListener(new SlotView.SimpleListener() {
+ @Override
+ public void onSingleTapUp(int slotIndex) {
+ ManageCachePage.this.onSingleTapUp(slotIndex);
+ }
+ });
+ mRootPane.addComponent(mAlbumSetView);
+
+ mCacheBar = new CacheBarView(mActivity, R.drawable.manage_bar,
+ config.cacheBarHeight,
+ config.cacheBarPinLeftMargin,
+ config.cacheBarPinRightMargin,
+ config.cacheBarButtonRightMargin,
+ config.cacheBarFontSize);
+
+ mCacheBar.setListener(this);
+ mRootPane.addComponent(mCacheBar);
+
+ mStaticBackground.setImage(R.drawable.background,
+ R.drawable.background_portrait);
+ }
+
+ public void onDoneClicked() {
+ 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);
+ }
+
+ private void showToast() {
+ if (mAlbumCountToMakeAvailableOffline > 0) {
+ Activity 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 = (Activity) mActivity;
+ Toast.makeText(activity, activity.getResources().getString(
+ R.string.try_to_set_local_album_available_offline),
+ Toast.LENGTH_SHORT).show();
+ }
+
+ public void onProgressComplete(int result) {
+ onBackPressed();
+ }
+
+ public void onProgressUpdate(int index) {
+ }
+
+ public void onSelectionModeChange(int mode) {
+ }
+
+ public void onSelectionChange(Path path, boolean selected) {
+ }
+}
diff --git a/src/com/android/gallery3d/app/MovieActivity.java b/src/com/android/gallery3d/app/MovieActivity.java
new file mode 100644
index 000000000..fea364e85
--- /dev/null
+++ b/src/com/android/gallery3d/app/MovieActivity.java
@@ -0,0 +1,129 @@
+/*
+ * 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 com.android.gallery3d.R;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Video.VideoColumns;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+/**
+ * This activity plays a video from a specified URI.
+ */
+public class MovieActivity extends Activity {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MovieActivity";
+
+ private MoviePlayer mPlayer;
+ private boolean mFinishOnCompletion;
+
+ @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.root);
+ Intent intent = getIntent();
+ setVideoTitle(intent);
+ mPlayer = new MoviePlayer(rootView, this, intent.getData()) {
+ @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);
+ }
+ }
+ mFinishOnCompletion = intent.getBooleanExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, true);
+ Window win = getWindow();
+ WindowManager.LayoutParams winParams = win.getAttributes();
+ winParams.buttonBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF;
+ win.setAttributes(winParams);
+
+ }
+
+ private void setVideoTitle(Intent intent) {
+ String title = intent.getStringExtra(Intent.EXTRA_TITLE);
+ if (title == null) {
+ Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(intent.getData(),
+ new String[] {VideoColumns.TITLE}, null, null, null);
+ if (cursor != null && cursor.moveToNext()) {
+ title = cursor.getString(0);
+ }
+ } catch (Throwable t) {
+ Log.w(TAG, "cannot get title from: " + intent.getDataString(), t);
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ }
+ if (title != null) getActionBar().setTitle(title);
+ }
+
+ @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 onDestroy() {
+ mPlayer.onDestroy();
+ super.onDestroy();
+ }
+}
diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java
new file mode 100644
index 000000000..423994485
--- /dev/null
+++ b/src/com/android/gallery3d/app/MoviePlayer.java
@@ -0,0 +1,291 @@
+/*
+ * 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 com.android.gallery3d.R;
+import com.android.gallery3d.common.BlobCache;
+import com.android.gallery3d.util.CacheManager;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.app.ActionBar;
+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.Handler;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.MediaController;
+import android.widget.VideoView;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+
+public class MoviePlayer implements
+ MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MoviePlayer";
+
+ // 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 Context mContext;
+ private final VideoView mVideoView;
+ private final View mProgressView;
+ private final Bookmarker mBookmarker;
+ private final Uri mUri;
+ private final Handler mHandler = new Handler();
+ private final AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver;
+ private final ActionBar mActionBar;
+
+ private boolean mHasPaused;
+
+ private final Runnable mPlayingChecker = new Runnable() {
+ public void run() {
+ if (mVideoView.isPlaying()) {
+ mProgressView.setVisibility(View.GONE);
+ } else {
+ mHandler.postDelayed(mPlayingChecker, 250);
+ }
+ }
+ };
+
+ public MoviePlayer(View rootView, final MovieActivity movieActivity, Uri videoUri) {
+ mContext = movieActivity.getApplicationContext();
+ mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
+ mProgressView = rootView.findViewById(R.id.progress_indicator);
+ mBookmarker = new Bookmarker(movieActivity);
+ mActionBar = movieActivity.getActionBar();
+ mUri = videoUri;
+
+ // 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)) {
+ mHandler.postDelayed(mPlayingChecker, 250);
+ } else {
+ mProgressView.setVisibility(View.GONE);
+ }
+
+ mVideoView.setOnErrorListener(this);
+ mVideoView.setOnCompletionListener(this);
+ mVideoView.setVideoURI(mUri);
+
+ MediaController mediaController = new MediaController(movieActivity) {
+ @Override
+ public void show() {
+ super.show();
+ mActionBar.show();
+ }
+
+ @Override
+ public void hide() {
+ super.hide();
+ mActionBar.hide();
+ }
+ };
+ mVideoView.setMediaController(mediaController);
+ mediaController.setOnKeyListener(new View.OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+ movieActivity.onBackPressed();
+ }
+ return true;
+ }
+ return false;
+ }
+ });
+
+ mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver();
+ mAudioBecomingNoisyReceiver.register();
+
+ // make the video view handle keys for seeking and pausing
+ mVideoView.requestFocus();
+
+ Intent i = new Intent(SERVICECMD);
+ i.putExtra(CMDNAME, CMDPAUSE);
+ movieActivity.sendBroadcast(i);
+
+ final Integer bookmark = mBookmarker.getBookmark(mUri);
+ if (bookmark != null) {
+ showResumeDialog(movieActivity, bookmark);
+ } else {
+ mVideoView.start();
+ }
+ }
+
+ 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() {
+ public void onCancel(DialogInterface dialog) {
+ onCompletion();
+ }
+ });
+ builder.setPositiveButton(
+ R.string.resume_playing_resume, new OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mVideoView.seekTo(bookmark);
+ mVideoView.start();
+ }
+ });
+ builder.setNegativeButton(
+ R.string.resume_playing_restart, new OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mVideoView.start();
+ }
+ });
+ builder.show();
+ }
+
+ public void onPause() {
+ mHandler.removeCallbacksAndMessages(null);
+ mBookmarker.setBookmark(mUri, mVideoView.getCurrentPosition(),
+ mVideoView.getDuration());
+ mVideoView.suspend();
+ mHasPaused = true;
+ }
+
+ public void onResume() {
+ if (mHasPaused) {
+ Integer bookmark = mBookmarker.getBookmark(mUri);
+ if (bookmark != null) {
+ mVideoView.seekTo(bookmark);
+ }
+ }
+ mVideoView.resume();
+ }
+
+ public void onDestroy() {
+ mVideoView.stopPlayback();
+ mAudioBecomingNoisyReceiver.unregister();
+ }
+
+ public boolean onError(MediaPlayer player, int arg1, int arg2) {
+ mHandler.removeCallbacksAndMessages(null);
+ mProgressView.setVisibility(View.GONE);
+ return false;
+ }
+
+ public void onCompletion(MediaPlayer mp) {
+ onCompletion();
+ }
+
+ public void onCompletion() {
+ }
+
+ 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()) {
+ mVideoView.pause();
+ }
+ }
+ }
+}
+
+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 = dis.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/PackagesMonitor.java b/src/com/android/gallery3d/app/PackagesMonitor.java
new file mode 100644
index 000000000..cb202a31c
--- /dev/null
+++ b/src/com/android/gallery3d/app/PackagesMonitor.java
@@ -0,0 +1,50 @@
+/*
+ * 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.picasasource.PicasaSource;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+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(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);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java
new file mode 100644
index 000000000..c05c89a0d
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -0,0 +1,794 @@
+/*
+ * 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.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+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.PhotoView;
+import com.android.gallery3d.ui.PhotoView.ImageData;
+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;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+import android.os.Handler;
+import android.os.Message;
+
+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 MIN_LOAD_COUNT = 8;
+ private static final int DATA_CACHE_SIZE = 32;
+ private static final int IMAGE_CACHE_SIZE = 5;
+
+ private static final int BIT_SCREEN_NAIL = 1;
+ private static final int BIT_FULL_IMAGE = 2;
+
+ private static final long VERSION_OUT_OF_RANGE = MediaObject.nextVersionNumber();
+
+ // 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 version-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<Long, ImageEntry> mImageCache = new HashMap<Long, 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 previous,
+ // current, and next image. If the version number changes, we invalidate
+ // the model. This is used after a database reload or mCurrentIndex changes.
+ private final long mChanges[] = new long[3];
+
+ 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 boolean mIsActive;
+
+ public interface DataListener extends LoadingListener {
+ public void onPhotoChanged(int index, Path item);
+ }
+
+ private DataListener mDataListener;
+
+ private final SourceListener mSourceListener = new SourceListener();
+
+ // 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.
+ public PhotoDataAdapter(GalleryActivity activity,
+ PhotoView view, MediaSet mediaSet, Path itemPath, int indexHint) {
+ mSource = Utils.checkNotNull(mediaSet);
+ mPhotoView = Utils.checkNotNull(view);
+ mItemPath = Utils.checkNotNull(itemPath);
+ mCurrentIndex = indexHint;
+ mThreadPool = activity.getThreadPool();
+
+ Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION);
+
+ 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();
+ return;
+ }
+ default: throw new AssertionError();
+ }
+ }
+ };
+
+ updateSlidingWindow();
+ }
+
+ private long getVersion(int index) {
+ if (index < 0 || index >= mSize) return VERSION_OUT_OF_RANGE;
+ if (index >= mContentStart && index < mContentEnd) {
+ MediaItem item = mData[index % DATA_CACHE_SIZE];
+ if (item != null) return item.getDataVersion();
+ }
+ return MediaObject.INVALID_DATA_VERSION;
+ }
+
+ private void fireModelInvalidated() {
+ for (int i = -1; i <= 1; ++i) {
+ long current = getVersion(mCurrentIndex + i);
+ long change = mChanges[i + 1];
+ if (current != change) {
+ mPhotoView.notifyImageInvalidated(i);
+ mChanges[i + 1] = current;
+ }
+ }
+ }
+
+ public void setDataListener(DataListener listener) {
+ mDataListener = listener;
+ }
+
+ private void updateScreenNail(long version, Future<Bitmap> future) {
+ ImageEntry entry = mImageCache.get(version);
+ if (entry == null || entry.screenNailTask == null) {
+ Bitmap screenNail = future.get();
+ if (screenNail != null) screenNail.recycle();
+ return;
+ }
+ entry.screenNailTask = null;
+ entry.screenNail = future.get();
+
+ if (entry.screenNail == null) {
+ entry.failToLoad = true;
+ } else {
+ for (int i = -1; i <=1; ++i) {
+ if (version == getVersion(mCurrentIndex + i)) {
+ if (i == 0) updateTileProvider(entry);
+ mPhotoView.notifyImageInvalidated(i);
+ }
+ }
+ }
+ updateImageRequests();
+ }
+
+ private void updateFullImage(long version, Future<BitmapRegionDecoder> future) {
+ ImageEntry entry = mImageCache.get(version);
+ if (entry == null || entry.fullImageTask == null) {
+ BitmapRegionDecoder fullImage = future.get();
+ if (fullImage != null) fullImage.recycle();
+ return;
+ }
+ entry.fullImageTask = null;
+ entry.fullImage = future.get();
+ if (entry.fullImage != null) {
+ if (version == getVersion(mCurrentIndex)) {
+ updateTileProvider(entry);
+ mPhotoView.notifyImageInvalidated(0);
+ }
+ }
+ updateImageRequests();
+ }
+
+ public void resume() {
+ mIsActive = true;
+ mSource.addContentListener(mSourceListener);
+ updateImageCache();
+ updateImageRequests();
+
+ mReloadTask = new ReloadTask();
+ mReloadTask.start();
+
+ mPhotoView.notifyModelInvalidated();
+ }
+
+ 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();
+ }
+ mImageCache.clear();
+ mTileProvider.clear();
+ }
+
+ private ImageData getImage(int index) {
+ if (index < 0 || index >= mSize || !mIsActive) return null;
+ Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
+
+ ImageEntry entry = mImageCache.get(getVersion(index));
+ Bitmap screennail = entry == null ? null : entry.screenNail;
+ if (screennail != null) {
+ return new ImageData(screennail, entry.rotation);
+ } else {
+ return new ImageData(null, 0);
+ }
+ }
+
+ public ImageData getPreviousImage() {
+ return getImage(mCurrentIndex - 1);
+ }
+
+ public ImageData getNextImage() {
+ return getImage(mCurrentIndex + 1);
+ }
+
+ private void updateCurrentIndex(int index) {
+ mCurrentIndex = index;
+ updateSlidingWindow();
+
+ MediaItem item = mData[index % DATA_CACHE_SIZE];
+ mItemPath = item == null ? null : item.getPath();
+
+ updateImageCache();
+ updateImageRequests();
+ updateTileProvider();
+ mPhotoView.notifyOnNewImage();
+
+ if (mDataListener != null) {
+ mDataListener.onPhotoChanged(index, mItemPath);
+ }
+ fireModelInvalidated();
+ }
+
+ public void next() {
+ updateCurrentIndex(mCurrentIndex + 1);
+ }
+
+ public void previous() {
+ updateCurrentIndex(mCurrentIndex - 1);
+ }
+
+ public void jumpTo(int index) {
+ if (mCurrentIndex == index) return;
+ updateCurrentIndex(index);
+ }
+
+ public Bitmap getBackupImage() {
+ return mTileProvider.getBackupImage();
+ }
+
+ public int getImageHeight() {
+ return mTileProvider.getImageHeight();
+ }
+
+ public int getImageWidth() {
+ return mTileProvider.getImageWidth();
+ }
+
+ public int getImageRotation() {
+ ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex));
+ return entry == null ? 0 : entry.rotation;
+ }
+
+ public int getLevelCount() {
+ return mTileProvider.getLevelCount();
+ }
+
+ public Bitmap getTile(int level, int x, int y, int tileSize) {
+ return mTileProvider.getTile(level, x, y, tileSize);
+ }
+
+ public boolean isFailedToLoad() {
+ return mTileProvider.isFailedToLoad();
+ }
+
+ public boolean isEmpty() {
+ return mSize == 0;
+ }
+
+ public int getCurrentIndex() {
+ return mCurrentIndex;
+ }
+
+ public MediaItem getCurrentMediaItem() {
+ return mData[mCurrentIndex % DATA_CACHE_SIZE];
+ }
+
+ public void setCurrentPhoto(Path path, int indexHint) {
+ if (mItemPath == path) return;
+ mItemPath = path;
+ mCurrentIndex = indexHint;
+ updateSlidingWindow();
+ updateImageCache();
+ fireModelInvalidated();
+
+ // We need to reload content if the path doesn't match.
+ MediaItem item = getCurrentMediaItem();
+ if (item != null && item.getPath() != path) {
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ private void updateTileProvider() {
+ ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex));
+ if (entry == null) { // in loading
+ mTileProvider.clear();
+ } else {
+ updateTileProvider(entry);
+ }
+ }
+
+ private void updateTileProvider(ImageEntry entry) {
+ Bitmap screenNail = entry.screenNail;
+ BitmapRegionDecoder fullImage = entry.fullImage;
+ if (screenNail != null) {
+ if (fullImage != null) {
+ mTileProvider.setBackupImage(screenNail,
+ fullImage.getWidth(), fullImage.getHeight());
+ mTileProvider.setRegionDecoder(fullImage);
+ } else {
+ int width = screenNail.getWidth();
+ int height = screenNail.getHeight();
+ mTileProvider.setBackupImage(screenNail, width, height);
+ }
+ } else {
+ mTileProvider.clear();
+ if (entry.failToLoad) mTileProvider.setFailedToLoad();
+ }
+ }
+
+ 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;
+ 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.requestedBits &= ~BIT_SCREEN_NAIL;
+ }
+ if (entry.fullImageTask != null && entry.fullImageTask != task) {
+ entry.fullImageTask.cancel();
+ entry.fullImageTask = null;
+ entry.requestedBits &= ~BIT_FULL_IMAGE;
+ }
+ }
+ }
+
+ // 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(getVersion(index));
+ if (entry == null) return null;
+
+ if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null) {
+ return entry.screenNailTask;
+ } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null) {
+ return entry.fullImageTask;
+ }
+
+ MediaItem item = mData[index % DATA_CACHE_SIZE];
+ Utils.assertTrue(item != null);
+
+ if (which == BIT_SCREEN_NAIL
+ && (entry.requestedBits & BIT_SCREEN_NAIL) == 0) {
+ entry.requestedBits |= BIT_SCREEN_NAIL;
+ entry.screenNailTask = mThreadPool.submit(
+ item.requestImage(MediaItem.TYPE_THUMBNAIL),
+ new ScreenNailListener(item.getDataVersion()));
+ // request screen nail
+ return entry.screenNailTask;
+ }
+ if (which == BIT_FULL_IMAGE
+ && (entry.requestedBits & BIT_FULL_IMAGE) == 0
+ && (item.getSupportedOperations()
+ & MediaItem.SUPPORT_FULL_IMAGE) != 0) {
+ entry.requestedBits |= BIT_FULL_IMAGE;
+ entry.fullImageTask = mThreadPool.submit(
+ item.requestLargeImage(),
+ new FullImageListener(item.getDataVersion()));
+ // request full image
+ return entry.fullImageTask;
+ }
+ return null;
+ }
+
+ private void updateImageCache() {
+ HashSet<Long> toBeRemoved = new HashSet<Long>(mImageCache.keySet());
+ for (int i = mActiveStart; i < mActiveEnd; ++i) {
+ MediaItem item = mData[i % DATA_CACHE_SIZE];
+ long version = item == null
+ ? MediaObject.INVALID_DATA_VERSION
+ : item.getDataVersion();
+ if (version == MediaObject.INVALID_DATA_VERSION) continue;
+ ImageEntry entry = mImageCache.get(version);
+ toBeRemoved.remove(version);
+ if (entry != null) {
+ if (Math.abs(i - mCurrentIndex) > 1) {
+ if (entry.fullImageTask != null) {
+ entry.fullImageTask.cancel();
+ entry.fullImageTask = null;
+ }
+ entry.fullImage = null;
+ entry.requestedBits &= ~BIT_FULL_IMAGE;
+ }
+ } else {
+ entry = new ImageEntry();
+ entry.rotation = item.getRotation();
+ mImageCache.put(version, entry);
+ }
+ }
+
+ // Clear the data and requests for ImageEntries outside the new window.
+ for (Long version : toBeRemoved) {
+ ImageEntry entry = mImageCache.remove(version);
+ if (entry.fullImageTask != null) entry.fullImageTask.cancel();
+ if (entry.screenNailTask != null) entry.screenNailTask.cancel();
+ }
+ }
+
+ private class FullImageListener
+ implements Runnable, FutureListener<BitmapRegionDecoder> {
+ private final long mVersion;
+ private Future<BitmapRegionDecoder> mFuture;
+
+ public FullImageListener(long version) {
+ mVersion = version;
+ }
+
+ public void onFutureDone(Future<BitmapRegionDecoder> future) {
+ mFuture = future;
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
+ }
+
+ public void run() {
+ updateFullImage(mVersion, mFuture);
+ }
+ }
+
+ private class ScreenNailListener
+ implements Runnable, FutureListener<Bitmap> {
+ private final long mVersion;
+ private Future<Bitmap> mFuture;
+
+ public ScreenNailListener(long version) {
+ mVersion = version;
+ }
+
+ public void onFutureDone(Future<Bitmap> future) {
+ mFuture = future;
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
+ }
+
+ public void run() {
+ updateScreenNail(mVersion, mFuture);
+ }
+ }
+
+ private static class ImageEntry {
+ public int requestedBits = 0;
+ public int rotation;
+ public BitmapRegionDecoder fullImage;
+ public Bitmap screenNail;
+ public Future<Bitmap> screenNailTask;
+ public Future<BitmapRegionDecoder> fullImageTask;
+ public boolean failToLoad = false;
+ }
+
+ private class SourceListener implements ContentListener {
+ 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 {
+ 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;
+ }
+
+ if (info.indexHint == MediaSet.INDEX_NOT_FOUND) {
+ // The image has been deleted, clear mItemPath, the
+ // mCurrentIndex will be updated in the updateCurrentItem().
+ mItemPath = null;
+ updateCurrentItem();
+ } else {
+ 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;
+ }
+ }
+ if (mItemPath == null) {
+ MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
+ mItemPath = current == null ? null : current.getPath();
+ }
+ updateImageCache();
+ updateTileProvider();
+ updateImageRequests();
+ fireModelInvalidated();
+ return null;
+ }
+
+ private void updateCurrentItem() {
+ if (mSize == 0) return;
+ if (mCurrentIndex >= mSize) {
+ mCurrentIndex = mSize - 1;
+ mPhotoView.notifyOnNewImage();
+ mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_LEFT);
+ } else {
+ mPhotoView.notifyOnNewImage();
+ mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_RIGHT);
+ }
+ }
+ }
+
+ 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());
+ synchronized (DataManager.LOCK) {
+ 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);
+ MediaItem item = findCurrentMediaItem(info);
+ if (item == null || item.getPath() != info.target) {
+ info.indexHint = findIndexOfTarget(info);
+ }
+ }
+ 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) {
+ for (int i = 0, n = items.size(); i < n; ++i) {
+ if (items.get(i).getPath() == info.target) return i + info.contentStart;
+ }
+ }
+
+ // Not found, find it in mSource.
+ return mSource.getIndexOfItem(info.target, info.indexHint);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
new file mode 100644
index 000000000..f28eb221d
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -0,0 +1,581 @@
+/*
+ * 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.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.MtpDevice;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.ui.DetailsWindow;
+import com.android.gallery3d.ui.DetailsWindow.CloseListener;
+import com.android.gallery3d.ui.DetailsWindow.DetailsSource;
+import com.android.gallery3d.ui.FilmStripView;
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.ImportCompleteListener;
+import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.PositionRepository;
+import com.android.gallery3d.ui.PositionRepository.Position;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.ui.UserInteractionListener;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.app.ActionBar;
+import android.app.ActionBar.OnMenuVisibilityListener;
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.WindowManager;
+import android.widget.ShareActionProvider;
+import android.widget.Toast;
+
+public class PhotoPage extends ActivityState
+ implements PhotoView.PhotoTapListener, FilmStripView.Listener,
+ UserInteractionListener {
+ private static final String TAG = "PhotoPage";
+
+ private static final int MSG_HIDE_BARS = 1;
+ private static final int HIDE_BARS_TIMEOUT = 3500;
+
+ private static final int REQUEST_SLIDESHOW = 1;
+ private static final int REQUEST_CROP = 2;
+ private static final int REQUEST_CROP_PICASA = 3;
+
+ 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";
+
+ private GalleryApp mApplication;
+ private SelectionManager mSelectionManager;
+
+ private PhotoView mPhotoView;
+ private PhotoPage.Model mModel;
+ private FilmStripView mFilmStripView;
+ private DetailsWindow mDetailsWindow;
+ 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 MediaSet mMediaSet;
+ private Menu mMenu;
+
+ private Intent mResultIntent = new Intent();
+ private int mCurrentIndex = 0;
+ private Handler mHandler;
+ private boolean mShowBars;
+ private ActionBar mActionBar;
+ private MyMenuVisibilityListener mMenuVisibilityListener;
+ private boolean mIsMenuVisible;
+ private boolean mIsInteracting;
+ private MediaItem mCurrentPhoto = null;
+ private MenuExecutor mMenuExecutor;
+ private boolean mIsActive;
+ private ShareActionProvider mShareActionProvider;
+
+ public static interface Model extends PhotoView.Model {
+ public void resume();
+ public void pause();
+ public boolean isEmpty();
+ public MediaItem getCurrentMediaItem();
+ public int getCurrentIndex();
+ public void setCurrentPhoto(Path path, int indexHint);
+ }
+
+ private class MyMenuVisibilityListener implements OnMenuVisibilityListener {
+ public void onMenuVisibilityChanged(boolean isVisible) {
+ mIsMenuVisible = isVisible;
+ refreshHidingMessage();
+ }
+ }
+
+ private GLView mRootPane = new GLView() {
+
+ @Override
+ protected void renderBackground(GLCanvas view) {
+ view.clearBuffer();
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ mPhotoView.layout(0, 0, right - left, bottom - top);
+ PositionRepository.getInstance(mActivity).setOffset(0, 0);
+ int filmStripHeight = 0;
+ if (mFilmStripView != null) {
+ mFilmStripView.measure(
+ MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY),
+ MeasureSpec.UNSPECIFIED);
+ filmStripHeight = mFilmStripView.getMeasuredHeight();
+ mFilmStripView.layout(0, bottom - top - filmStripHeight,
+ right - left, bottom - top);
+ }
+ if (mShowDetails) {
+ mDetailsWindow.measure(
+ MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int width = mDetailsWindow.getMeasuredWidth();
+ int viewTop = GalleryActionBar.getHeight((Activity) mActivity);
+ mDetailsWindow.layout(
+ 0, viewTop, width, bottom - top - filmStripHeight);
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ mActionBar = ((Activity) mActivity).getActionBar();
+ mSelectionManager = new SelectionManager(mActivity, false);
+ mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager);
+
+ mPhotoView = new PhotoView(mActivity);
+ mPhotoView.setPhotoTapListener(this);
+ mRootPane.addComponent(mPhotoView);
+ mApplication = (GalleryApp)((Activity) mActivity).getApplication();
+
+ String setPathString = data.getString(KEY_MEDIA_SET_PATH);
+ Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH));
+
+ if (setPathString != null) {
+ mMediaSet = mActivity.getDataManager().getMediaSet(setPathString);
+ mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
+ mMediaSet = (MediaSet)
+ mActivity.getDataManager().getMediaObject(setPathString);
+ if (mMediaSet == null) {
+ Log.w(TAG, "failed to restore " + setPathString);
+ }
+ PhotoDataAdapter pda = new PhotoDataAdapter(
+ mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex);
+ mModel = pda;
+ mPhotoView.setModel(mModel);
+
+ Config.PhotoPage config = Config.PhotoPage.get((Context) mActivity);
+
+ mFilmStripView = new FilmStripView(mActivity, mMediaSet,
+ config.filmstripTopMargin, config.filmstripMidMargin, config.filmstripBottomMargin,
+ config.filmstripContentSize, config.filmstripThumbSize, config.filmstripBarSize,
+ config.filmstripGripSize, config.filmstripGripWidth);
+ mRootPane.addComponent(mFilmStripView);
+ mFilmStripView.setListener(this);
+ mFilmStripView.setUserInteractionListener(this);
+ mFilmStripView.setFocusIndex(mCurrentIndex);
+ mFilmStripView.setStartIndex(mCurrentIndex);
+
+ mResultIntent.putExtra(KEY_INDEX_HINT, mCurrentIndex);
+ setStateResult(Activity.RESULT_OK, mResultIntent);
+
+ pda.setDataListener(new PhotoDataAdapter.DataListener() {
+
+ public void onPhotoChanged(int index, Path item) {
+ mFilmStripView.setFocusIndex(index);
+ mCurrentIndex = index;
+ mResultIntent.putExtra(KEY_INDEX_HINT, index);
+ if (item != null) {
+ mResultIntent.putExtra(KEY_MEDIA_ITEM_PATH, item.toString());
+ MediaItem photo = mModel.getCurrentMediaItem();
+ if (photo != null) updateCurrentPhoto(photo);
+ } else {
+ mResultIntent.removeExtra(KEY_MEDIA_ITEM_PATH);
+ }
+ setStateResult(Activity.RESULT_OK, mResultIntent);
+ }
+
+ @Override
+ public void onLoadingFinished() {
+ GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
+ if (!mModel.isEmpty()) {
+ MediaItem photo = mModel.getCurrentMediaItem();
+ if (photo != null) updateCurrentPhoto(photo);
+ } else if (mIsActive) {
+ mActivity.getStateManager().finishState(PhotoPage.this);
+ }
+ }
+
+
+ @Override
+ public void onLoadingStarted() {
+ GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
+ }
+ });
+ } 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);
+ }
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_HIDE_BARS: {
+ hideBars();
+ break;
+ }
+ default: throw new AssertionError(message.what);
+ }
+ }
+ };
+
+ // start the opening animation
+ mPhotoView.setOpenedItem(itemPath);
+ }
+
+ private void updateCurrentPhoto(MediaItem photo) {
+ if (mCurrentPhoto == photo) return;
+ mCurrentPhoto = photo;
+ if (mCurrentPhoto == null) return;
+ updateMenuOperations();
+ if (mShowDetails) {
+ mDetailsWindow.reloadDetails(mModel.getCurrentIndex());
+ }
+ String title = photo.getName();
+ if (title != null) mActionBar.setTitle(title);
+ mPhotoView.showVideoPlayIcon(photo.getMediaType()
+ == MediaObject.MEDIA_TYPE_VIDEO);
+
+ // If we have an ActionBar then we update the share intent
+ if (mShareActionProvider != null) {
+ Path path = photo.getPath();
+ DataManager manager = mActivity.getDataManager();
+ int type = manager.getMediaType(path);
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType(MenuExecutor.getMimeType(type));
+ intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path));
+ mShareActionProvider.setShareIntent(intent);
+ }
+ }
+
+ private void updateMenuOperations() {
+ if (mCurrentPhoto == null || mMenu == null) return;
+ int supportedOperations = mCurrentPhoto.getSupportedOperations();
+ if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) {
+ supportedOperations &= ~MediaObject.SUPPORT_EDIT;
+ }
+ MenuExecutor.updateMenuOperation(mMenu, supportedOperations);
+ }
+
+ private void showBars() {
+ if (mShowBars) return;
+ mShowBars = true;
+ mActionBar.show();
+ WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
+ params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE;
+ ((Activity) mActivity).getWindow().setAttributes(params);
+ if (mFilmStripView != null) {
+ mFilmStripView.show();
+ }
+ }
+
+ private void hideBars() {
+ if (!mShowBars) return;
+ mShowBars = false;
+ mActionBar.hide();
+ WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
+ params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE;
+ ((Activity) mActivity).getWindow().setAttributes(params);
+ if (mFilmStripView != null) {
+ mFilmStripView.hide();
+ }
+ }
+
+ private void refreshHidingMessage() {
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ if (!mIsMenuVisible && !mIsInteracting) {
+ mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
+ }
+ }
+
+ public void onUserInteraction() {
+ showBars();
+ refreshHidingMessage();
+ }
+
+ public void onUserInteractionTap() {
+ if (mShowBars) {
+ hideBars();
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ } else {
+ showBars();
+ refreshHidingMessage();
+ }
+ }
+
+ public void onUserInteractionBegin() {
+ showBars();
+ mIsInteracting = true;
+ refreshHidingMessage();
+ }
+
+ public void onUserInteractionEnd() {
+ mIsInteracting = false;
+ refreshHidingMessage();
+ }
+
+ @Override
+ protected void onBackPressed() {
+ if (mShowDetails) {
+ hideDetails();
+ } else {
+ PositionRepository repository = PositionRepository.getInstance(mActivity);
+ repository.clear();
+ if (mCurrentPhoto != null) {
+ Position position = new Position();
+ position.x = mRootPane.getWidth() / 2;
+ position.y = mRootPane.getHeight() / 2;
+ position.z = -1000;
+ repository.putPosition(
+ Long.valueOf(System.identityHashCode(mCurrentPhoto.getPath())),
+ position);
+ }
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ protected boolean onCreateActionBar(Menu menu) {
+ MenuInflater inflater = ((Activity) mActivity).getMenuInflater();
+ inflater.inflate(R.menu.photo, menu);
+ menu.findItem(R.id.action_slideshow).setVisible(
+ mMediaSet != null && !(mMediaSet instanceof MtpDevice));
+ mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu);
+ mMenu = menu;
+ mShowBars = true;
+ updateMenuOperations();
+ return true;
+ }
+
+ @Override
+ protected boolean onItemSelected(MenuItem item) {
+ MediaItem current = mModel.getCurrentMediaItem();
+
+ 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();
+ switch (action) {
+ case R.id.action_slideshow: {
+ Bundle data = new Bundle();
+ data.putString(SlideshowPage.KEY_SET_PATH,
+ mMediaSet.getPath().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 = (Activity) mActivity;
+ Intent intent = new Intent(CropImage.CROP_ACTION);
+ intent.setClass(activity, CropImage.class);
+ intent.setData(manager.getContentUri(path));
+ activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
+ ? REQUEST_CROP_PICASA
+ : REQUEST_CROP);
+ return true;
+ }
+ case R.id.action_details: {
+ if (mShowDetails) {
+ hideDetails();
+ } else {
+ showDetails(currentIndex);
+ }
+ return true;
+ }
+ case R.id.action_setas:
+ case R.id.action_confirm_delete:
+ case R.id.action_rotate_ccw:
+ case R.id.action_rotate_cw:
+ case R.id.action_show_on_map:
+ case R.id.action_edit:
+ mSelectionManager.deSelectAll();
+ mSelectionManager.toggle(path);
+ mMenuExecutor.onMenuClicked(item, null);
+ return true;
+ case R.id.action_import:
+ mSelectionManager.deSelectAll();
+ mSelectionManager.toggle(path);
+ mMenuExecutor.onMenuClicked(item,
+ new ImportCompleteListener(mActivity));
+ return true;
+ default :
+ return false;
+ }
+ }
+
+ private void hideDetails() {
+ mShowDetails = false;
+ mDetailsWindow.hide();
+ }
+
+ private void showDetails(int index) {
+ mShowDetails = true;
+ if (mDetailsWindow == null) {
+ mDetailsWindow = new DetailsWindow(mActivity, new MyDetailsSource());
+ mDetailsWindow.setCloseListener(new CloseListener() {
+ public void onClose() {
+ hideDetails();
+ }
+ });
+ mRootPane.addComponent(mDetailsWindow);
+ }
+ mDetailsWindow.reloadDetails(index);
+ mDetailsWindow.show();
+ }
+
+ public void onSingleTapUp(int x, int y) {
+ MediaItem item = mModel.getCurrentMediaItem();
+ if (item == null) {
+ // item is not ready, ignore
+ return;
+ }
+
+ boolean playVideo =
+ (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 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) {
+ playVideo((Activity) mActivity, item.getPlayUri(), item.getName());
+ } else {
+ onUserInteractionTap();
+ }
+ }
+
+ public static void playVideo(Activity activity, Uri uri, String title) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW)
+ .setDataAndType(uri, "video/*");
+ intent.putExtra(Intent.EXTRA_TITLE, title);
+ activity.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(activity, activity.getString(R.string.video_err),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ // Called by FileStripView
+ public void onSlotSelected(int slotIndex) {
+ ((PhotoDataAdapter) mModel).jumpTo(slotIndex);
+ }
+
+ @Override
+ protected void onStateResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_CROP:
+ if (resultCode == Activity.RESULT_OK) {
+ if (data == null) break;
+ Path path = mApplication
+ .getDataManager().findPathByUri(data.getData());
+ if (path != null) {
+ mModel.setCurrentPhoto(path, mCurrentIndex);
+ }
+ }
+ break;
+ case REQUEST_CROP_PICASA: {
+ int message = resultCode == Activity.RESULT_OK
+ ? R.string.crop_saved
+ : R.string.crop_not_saved;
+ Toast.makeText(mActivity.getAndroidContext(),
+ 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;
+ if (mFilmStripView != null) {
+ mFilmStripView.pause();
+ }
+ if (mDetailsWindow != null) {
+ mDetailsWindow.pause();
+ }
+ mPhotoView.pause();
+ mModel.pause();
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsActive = true;
+ setContentPane(mRootPane);
+ mModel.resume();
+ mPhotoView.resume();
+ if (mFilmStripView != null) {
+ mFilmStripView.resume();
+ }
+ if (mMenuVisibilityListener == null) {
+ mMenuVisibilityListener = new MyMenuVisibilityListener();
+ }
+ mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
+ onUserInteraction();
+ }
+
+ private class MyDetailsSource implements DetailsSource {
+ public MediaDetails getDetails() {
+ return mModel.getCurrentMediaItem().getDetails();
+ }
+ public int size() {
+ return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1;
+ }
+ public int findIndex(int indexHint) {
+ return indexHint;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
new file mode 100644
index 000000000..11e0013cc
--- /dev/null
+++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
@@ -0,0 +1,181 @@
+/*
+ * 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.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.PhotoView;
+import com.android.gallery3d.ui.PhotoView.ImageData;
+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;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
+
+public class SinglePhotoDataAdapter extends TileImageViewAdapter
+ implements PhotoPage.Model {
+
+ private static final String TAG = "SinglePhotoDataAdapter";
+ private static final int SIZE_BACKUP = 640;
+ private static final int MSG_UPDATE_IMAGE = 1;
+
+ private MediaItem mItem;
+ private boolean mHasFullImage;
+ private Future<?> mTask;
+ private BitmapRegionDecoder mDecoder;
+ private Bitmap mBackup;
+ private Handler mHandler;
+
+ private PhotoView mPhotoView;
+ private ThreadPool mThreadPool;
+
+ public SinglePhotoDataAdapter(
+ GalleryActivity 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((Future<BitmapRegionDecoder>)
+ message.obj);
+ } else {
+ onDecodeThumbComplete((Future<Bitmap>) message.obj);
+ }
+ }
+ };
+ mThreadPool = activity.getThreadPool();
+ }
+
+ private FutureListener<BitmapRegionDecoder> mLargeListener =
+ new FutureListener<BitmapRegionDecoder>() {
+ public void onFutureDone(Future<BitmapRegionDecoder> future) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_UPDATE_IMAGE, future));
+ }
+ };
+
+ private FutureListener<Bitmap> mThumbListener =
+ new FutureListener<Bitmap>() {
+ public void onFutureDone(Future<Bitmap> future) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_UPDATE_IMAGE, future));
+ }
+ };
+
+ public boolean isEmpty() {
+ return false;
+ }
+
+ public int getImageRotation() {
+ return mItem.getRotation();
+ }
+
+ private void onDecodeLargeComplete(Future<BitmapRegionDecoder> future) {
+ try {
+ mDecoder = future.get();
+ if (mDecoder == null) return;
+ int width = mDecoder.getWidth();
+ int height = mDecoder.getHeight();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = BitmapUtils.computeSampleSizeLarger(
+ width, height, SIZE_BACKUP);
+ mBackup = mDecoder.decodeRegion(
+ new Rect(0, 0, width, height), options);
+ setBackupImage(mBackup, width, height);
+ setRegionDecoder(mDecoder);
+ mPhotoView.notifyImageInvalidated(0);
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to decode large", t);
+ }
+ }
+
+ private void onDecodeThumbComplete(Future<Bitmap> future) {
+ try {
+ mBackup = future.get();
+ if (mBackup == null) return;
+ setBackupImage(mBackup, mBackup.getWidth(), mBackup.getHeight());
+ mPhotoView.notifyOnNewImage();
+ mPhotoView.notifyImageInvalidated(0); // the current image
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to decode thumb", t);
+ }
+ }
+
+ public void resume() {
+ if (mTask == null) {
+ if (mHasFullImage) {
+ mTask = mThreadPool.submit(
+ mItem.requestLargeImage(), mLargeListener);
+ } else {
+ mTask = mThreadPool.submit(
+ mItem.requestImage(MediaItem.TYPE_THUMBNAIL),
+ mThumbListener);
+ }
+ }
+ }
+
+ public void pause() {
+ Future<?> task = mTask;
+ task.cancel();
+ task.waitDone();
+ if (task.get() == null) {
+ mTask = null;
+ }
+ }
+
+ public ImageData getNextImage() {
+ return null;
+ }
+
+ public ImageData getPreviousImage() {
+ return null;
+ }
+
+ public void next() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void previous() {
+ throw new UnsupportedOperationException();
+ }
+
+ public MediaItem getCurrentMediaItem() {
+ return mItem;
+ }
+
+ public int getCurrentIndex() {
+ return 0;
+ }
+
+ public void setCurrentPhoto(Path path, int indexHint) {
+ // ignore
+ }
+}
diff --git a/src/com/android/gallery3d/app/SlideshowDataAdapter.java b/src/com/android/gallery3d/app/SlideshowDataAdapter.java
new file mode 100644
index 000000000..6f9b98e8e
--- /dev/null
+++ b/src/com/android/gallery3d/app/SlideshowDataAdapter.java
@@ -0,0 +1,187 @@
+/*
+ * 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.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.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 android.graphics.Bitmap;
+
+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);
+ }
+
+ private final SlideshowSource mSource;
+
+ private int mLoadIndex = 0;
+ private int mNextOutput = 0;
+ private boolean mIsActive = false;
+ private boolean mNeedReset;
+ private boolean mDataReady;
+
+ 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();
+
+ public SlideshowDataAdapter(GalleryContext context, SlideshowSource source, int index) {
+ mSource = source;
+ mLoadIndex = index;
+ mNextOutput = index;
+ mThreadPool = context.getThreadPool();
+ }
+
+ public MediaItem loadItem() {
+ if (mNeedReload.compareAndSet(true, false)) {
+ long v = mSource.reload();
+ if (v != mDataVersion) {
+ mDataVersion = v;
+ mNeedReset = true;
+ return null;
+ }
+ }
+ return mSource.getMediaItem(mLoadIndex);
+ }
+
+ private class ReloadTask implements Job<Void> {
+ 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 {
+ 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();
+ }
+
+ public Future<Slide> nextSlide(FutureListener<Slide> listener) {
+ return mThreadPool.submit(new Job<Slide>() {
+ public Slide run(JobContext jc) {
+ jc.setMode(ThreadPool.MODE_NONE);
+ return innerNextBitmap();
+ }
+ }, listener);
+ }
+
+ public void pause() {
+ synchronized (this) {
+ mIsActive = false;
+ notifyAll();
+ }
+ mSource.removeContentListener(mSourceListener);
+ mReloadTask.cancel();
+ mReloadTask.waitDone();
+ mReloadTask = null;
+ }
+
+ 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/SlideshowDream.java b/src/com/android/gallery3d/app/SlideshowDream.java
new file mode 100644
index 000000000..f4abe86ab
--- /dev/null
+++ b/src/com/android/gallery3d/app/SlideshowDream.java
@@ -0,0 +1,28 @@
+package com.android.gallery3d.app;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.support.v13.dreams.BasicDream;
+import android.graphics.Canvas;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ViewFlipper;
+
+public class SlideshowDream extends BasicDream {
+ @Override
+ public void onCreate(Bundle bndl) {
+ super.onCreate(bndl);
+ Intent i = new Intent(
+ Intent.ACTION_VIEW,
+ android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
+// Uri.fromFile(Environment.getExternalStoragePublicDirectory(
+// Environment.DIRECTORY_PICTURES)))
+ .putExtra(Gallery.EXTRA_SLIDESHOW, true)
+ .setFlags(getIntent().getFlags());
+ startActivity(i);
+ finish();
+ }
+}
diff --git a/src/com/android/gallery3d/app/SlideshowPage.java b/src/com/android/gallery3d/app/SlideshowPage.java
new file mode 100644
index 000000000..cdf9308ec
--- /dev/null
+++ b/src/com/android/gallery3d/app/SlideshowPage.java
@@ -0,0 +1,338 @@
+/*
+ * 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.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.ui.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 android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.view.MotionEvent;
+
+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";
+
+ 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 WakeLock mWakeLock;
+ private Intent mResultIntent = new Intent();
+
+ private 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();
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ mFlags |= (FLAG_HIDE_ACTION_BAR | FLAG_HIDE_STATUS_BAR);
+
+ PowerManager pm = (PowerManager) mActivity.getAndroidContext()
+ .getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK
+ | PowerManager.ON_AFTER_RELEASE, TAG);
+
+ 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>() {
+ 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();
+ mWakeLock.release();
+ mIsActive = false;
+ mModel.pause();
+ mSlideshowView.release();
+
+ mHandler.removeMessages(MSG_LOAD_NEXT_BITMAP);
+ mHandler.removeMessages(MSG_SHOW_PENDING_BITMAP);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mWakeLock.acquire();
+ mIsActive = true;
+ mModel.resume();
+
+ if (mPendingSlide != null) {
+ showPendingBitmap();
+ } else {
+ loadNextBitmap();
+ }
+ }
+
+ private void initializeData(Bundle data) {
+ String mediaPath = data.getString(KEY_SET_PATH);
+ boolean random = data.getBoolean(KEY_RANDOM_ORDER, false);
+ MediaSet mediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
+
+ if (random) {
+ boolean repeat = data.getBoolean(KEY_REPEAT);
+ mModel = new SlideshowDataAdapter(
+ mActivity, new ShuffleSource(mediaSet, repeat), 0);
+ setStateResult(Activity.RESULT_OK,
+ mResultIntent.putExtra(KEY_PHOTO_INDEX, 0));
+ } else {
+ int index = data.getInt(KEY_PHOTO_INDEX);
+ boolean repeat = data.getBoolean(KEY_REPEAT);
+ mModel = new SlideshowDataAdapter(mActivity,
+ new SequentialSource(mediaSet, repeat), index);
+ 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 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;
+ }
+
+ public MediaItem getMediaItem(int index) {
+ if (!mRepeat && index >= mOrder.length) 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;
+ }
+
+ 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);
+ }
+ }
+
+ public void addContentListener(ContentListener listener) {
+ mMediaSet.addContentListener(listener);
+ }
+
+ 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;
+ }
+
+ public MediaItem getMediaItem(int index) {
+ int dataEnd = mDataStart + mData.size();
+
+ if (mRepeat) {
+ index = index % mMediaSet.getMediaItemCount();
+ }
+ 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);
+ }
+
+ public long reload() {
+ long version = mMediaSet.reload();
+ if (version != mDataVersion) {
+ mDataVersion = version;
+ mData.clear();
+ }
+ return mDataVersion;
+ }
+
+ public void addContentListener(ContentListener listener) {
+ mMediaSet.addContentListener(listener);
+ }
+
+ 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..b551f693a
--- /dev/null
+++ b/src/com/android/gallery3d/app/StateManager.java
@@ -0,0 +1,277 @@
+/*
+ * 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.common.Utils;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.Menu;
+import android.view.MenuItem;
+
+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 GalleryActivity mContext;
+ private Stack<StateEntry> mStack = new Stack<StateEntry>();
+ private ActivityState.ResultEntry mResult;
+
+ public StateManager(GalleryActivity context) {
+ mContext = context;
+ }
+
+ 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();
+ if (mIsResumed) top.onPause();
+ }
+ state.initialize(mContext, 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(mContext, data);
+ state.mResult = new ActivityState.ResultEntry();
+ state.mResult.requestCode = requestCode;
+
+ if (!mStack.isEmpty()) {
+ ActivityState as = getTopState();
+ as.mReceivedResults = state.mResult;
+ if (mIsResumed) as.onPause();
+ } else {
+ mResult = state.mResult;
+ }
+
+ mStack.push(new StateEntry(data, state));
+ state.onCreate(data, null);
+ if (mIsResumed) state.resume();
+ }
+
+ public boolean createOptionsMenu(Menu menu) {
+ if (!mStack.isEmpty()) {
+ ((Activity) mContext).setProgressBarIndeterminateVisibility(false);
+ return getTopState().onCreateActionBar(menu);
+ } else {
+ return false;
+ }
+ }
+
+ 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 int getStateCount() {
+ return mStack.size();
+ }
+
+ public boolean itemSelected(MenuItem item) {
+ if (!mStack.isEmpty()) {
+ if (mStack.size() > 1 && item.getItemId() == android.R.id.home) {
+ getTopState().onBackPressed();
+ return true;
+ } else {
+ return getTopState().onItemSelected(item);
+ }
+ }
+ return false;
+ }
+
+ public void onBackPressed() {
+ if (!mStack.isEmpty()) {
+ getTopState().onBackPressed();
+ }
+ }
+
+ void finishState(ActivityState state) {
+ Log.v(TAG, "finishState " + state.getClass());
+ if (state != mStack.peek().activityState) {
+ throw new IllegalArgumentException("The stateview to be finished"
+ + " is not at the top of the stack: " + state + ", "
+ + mStack.peek().activityState);
+ }
+
+ // Remove the top state.
+ mStack.pop();
+ if (mIsResumed) state.onPause();
+ mContext.getGLRoot().setContentPane(null);
+ state.onDestroy();
+
+ if (mStack.isEmpty()) {
+ Log.v(TAG, "no more state, finish activity");
+ Activity activity = (Activity) mContext.getAndroidContext();
+ if (mResult != null) {
+ activity.setResult(mResult.resultCode, mResult.resultData);
+ }
+ activity.finish();
+
+ // The finish() request is rejected (only happens under Monkey),
+ // so we start the default page instead.
+ if (!activity.isFinishing()) {
+ Log.v(TAG, "finish() failed, start default page");
+ ((Gallery) mContext).startDefaultPage();
+ }
+ } else {
+ // Restore the immediately previous state
+ ActivityState top = mStack.peek().activityState;
+ if (mIsResumed) top.resume();
+ }
+ }
+
+ 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 (mIsResumed) oldState.onPause();
+ oldState.onDestroy();
+
+ // Create new state.
+ ActivityState state = null;
+ try {
+ state = klass.newInstance();
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ state.initialize(mContext, data);
+ mStack.push(new StateEntry(data, state));
+ state.onCreate(data, null);
+ if (mIsResumed) state.resume();
+ }
+
+ 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);
+
+ 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(mContext, data);
+ activityState.onCreate(data, state);
+ mStack.push(new StateEntry(data, activityState));
+ }
+ }
+
+ 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/UsbDeviceActivity.java b/src/com/android/gallery3d/app/UsbDeviceActivity.java
new file mode 100644
index 000000000..28bd667e3
--- /dev/null
+++ b/src/com/android/gallery3d/app/UsbDeviceActivity.java
@@ -0,0 +1,47 @@
+/*
+ * 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.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+/* This Activity does nothing but receive USB_DEVICE_ATTACHED events from the
+ * USB service and springboards to the main Gallery activity
+ */
+public final class UsbDeviceActivity extends Activity {
+
+ static final String TAG = "UsbDeviceActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ //
+ Intent intent = new Intent(this, Gallery.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ try {
+ startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "unable to start Gallery activity", e);
+ }
+ finish();
+ }
+}
diff --git a/src/com/android/gallery3d/app/Wallpaper.java b/src/com/android/gallery3d/app/Wallpaper.java
new file mode 100644
index 000000000..07a3d5313
--- /dev/null
+++ b/src/com/android/gallery3d/app/Wallpaper.java
@@ -0,0 +1,116 @@
+/*
+ * 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.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.Display;
+
+/**
+ * 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("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();
+ Display display = getWindowManager().getDefaultDisplay();
+ float spotlightX = (float) display.getWidth() / width;
+ float spotlightY = (float) display.getHeight() / height;
+ Intent request = new Intent(CropImage.ACTION_CROP)
+ .setDataAndType(mPickedItem, IMAGE_TYPE)
+ .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
+ .putExtra(CropImage.KEY_OUTPUT_X, width)
+ .putExtra(CropImage.KEY_OUTPUT_Y, height)
+ .putExtra(CropImage.KEY_ASPECT_X, width)
+ .putExtra(CropImage.KEY_ASPECT_Y, height)
+ .putExtra(CropImage.KEY_SPOTLIGHT_X, spotlightX)
+ .putExtra(CropImage.KEY_SPOTLIGHT_Y, spotlightY)
+ .putExtra(CropImage.KEY_SCALE, true)
+ .putExtra(CropImage.KEY_NO_FACE_DETECTION, true)
+ .putExtra(CropImage.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
+ }
+}