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