diff options
Diffstat (limited to 'src/com/android/gallery3d/ui/ActionModeHandler.java')
-rw-r--r-- | src/com/android/gallery3d/ui/ActionModeHandler.java | 501 |
1 files changed, 501 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java new file mode 100644 index 000000000..6b4f10312 --- /dev/null +++ b/src/com/android/gallery3d/ui/ActionModeHandler.java @@ -0,0 +1,501 @@ +/* + * 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.ui; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.nfc.NfcAdapter; +import android.os.Handler; +import android.view.ActionMode; +import android.view.ActionMode.Callback; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.ShareActionProvider; +import android.widget.ShareActionProvider.OnShareTargetSelectedListener; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.MenuExecutor.ProgressListener; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.util.ArrayList; + +public class ActionModeHandler implements Callback, PopupList.OnPopupItemClickListener { + + @SuppressWarnings("unused") + private static final String TAG = "ActionModeHandler"; + + private static final int MAX_SELECTED_ITEMS_FOR_SHARE_INTENT = 300; + private static final int MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT = 10; + + private static final int SUPPORT_MULTIPLE_MASK = MediaObject.SUPPORT_DELETE + | MediaObject.SUPPORT_ROTATE | MediaObject.SUPPORT_SHARE + | MediaObject.SUPPORT_CACHE; + + public interface ActionModeListener { + public boolean onActionItemClicked(MenuItem item); + } + + private final AbstractGalleryActivity mActivity; + private final MenuExecutor mMenuExecutor; + private final SelectionManager mSelectionManager; + private final NfcAdapter mNfcAdapter; + private Menu mMenu; + private MenuItem mSharePanoramaMenuItem; + private MenuItem mShareMenuItem; + private ShareActionProvider mSharePanoramaActionProvider; + private ShareActionProvider mShareActionProvider; + private SelectionMenu mSelectionMenu; + private ActionModeListener mListener; + private Future<?> mMenuTask; + private final Handler mMainHandler; + private ActionMode mActionMode; + + private static class GetAllPanoramaSupports implements PanoramaSupportCallback { + private int mNumInfoRequired; + private JobContext mJobContext; + public boolean mAllPanoramas = true; + public boolean mAllPanorama360 = true; + public boolean mHasPanorama360 = false; + private Object mLock = new Object(); + + public GetAllPanoramaSupports(ArrayList<MediaObject> mediaObjects, JobContext jc) { + mJobContext = jc; + mNumInfoRequired = mediaObjects.size(); + for (MediaObject mediaObject : mediaObjects) { + mediaObject.getPanoramaSupport(this); + } + } + + @Override + public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama, + boolean isPanorama360) { + synchronized (mLock) { + mNumInfoRequired--; + mAllPanoramas = isPanorama && mAllPanoramas; + mAllPanorama360 = isPanorama360 && mAllPanorama360; + mHasPanorama360 = mHasPanorama360 || isPanorama360; + if (mNumInfoRequired == 0 || mJobContext.isCancelled()) { + mLock.notifyAll(); + } + } + } + + public void waitForPanoramaSupport() { + synchronized (mLock) { + while (mNumInfoRequired != 0 && !mJobContext.isCancelled()) { + try { + mLock.wait(); + } catch (InterruptedException e) { + // May be a cancelled job context + } + } + } + } + } + + public ActionModeHandler( + AbstractGalleryActivity activity, SelectionManager selectionManager) { + mActivity = Utils.checkNotNull(activity); + mSelectionManager = Utils.checkNotNull(selectionManager); + mMenuExecutor = new MenuExecutor(activity, selectionManager); + mMainHandler = new Handler(activity.getMainLooper()); + mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext()); + } + + public void startActionMode() { + Activity a = mActivity; + mActionMode = a.startActionMode(this); + View customView = LayoutInflater.from(a).inflate( + R.layout.action_mode, null); + mActionMode.setCustomView(customView); + mSelectionMenu = new SelectionMenu(a, + (Button) customView.findViewById(R.id.selection_menu), this); + updateSelectionMenu(); + } + + public void finishActionMode() { + mActionMode.finish(); + } + + public void setTitle(String title) { + mSelectionMenu.setTitle(title); + } + + public void setActionModeListener(ActionModeListener listener) { + mListener = listener; + } + + private WakeLockHoldingProgressListener mDeleteProgressListener; + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + GLRoot root = mActivity.getGLRoot(); + root.lockRenderThread(); + try { + boolean result; + // Give listener a chance to process this command before it's routed to + // ActionModeHandler, which handles command only based on the action id. + // Sometimes the listener may have more background information to handle + // an action command. + if (mListener != null) { + result = mListener.onActionItemClicked(item); + if (result) { + mSelectionManager.leaveSelectionMode(); + return result; + } + } + ProgressListener listener = null; + String confirmMsg = null; + int action = item.getItemId(); + if (action == R.id.action_delete) { + confirmMsg = mActivity.getResources().getQuantityString( + R.plurals.delete_selection, mSelectionManager.getSelectedCount()); + if (mDeleteProgressListener == null) { + mDeleteProgressListener = new WakeLockHoldingProgressListener(mActivity, + "Gallery Delete Progress Listener"); + } + listener = mDeleteProgressListener; + } + mMenuExecutor.onMenuClicked(item, confirmMsg, listener); + } finally { + root.unlockRenderThread(); + } + return true; + } + + @Override + public boolean onPopupItemClick(int itemId) { + GLRoot root = mActivity.getGLRoot(); + root.lockRenderThread(); + try { + if (itemId == R.id.action_select_all) { + updateSupportedOperation(); + mMenuExecutor.onMenuClicked(itemId, null, false, true); + } + return true; + } finally { + root.unlockRenderThread(); + } + } + + private void updateSelectionMenu() { + // update title + int count = mSelectionManager.getSelectedCount(); + String format = mActivity.getResources().getQuantityString( + R.plurals.number_of_items_selected, count); + setTitle(String.format(format, count)); + + // For clients who call SelectionManager.selectAll() directly, we need to ensure the + // menu status is consistent with selection manager. + mSelectionMenu.updateSelectAllMode(mSelectionManager.inSelectAllMode()); + } + + private final OnShareTargetSelectedListener mShareTargetSelectedListener = + new OnShareTargetSelectedListener() { + @Override + public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) { + mSelectionManager.leaveSelectionMode(); + return false; + } + }; + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mode.getMenuInflater().inflate(R.menu.operation, menu); + + mMenu = menu; + mSharePanoramaMenuItem = menu.findItem(R.id.action_share_panorama); + if (mSharePanoramaMenuItem != null) { + mSharePanoramaActionProvider = (ShareActionProvider) mSharePanoramaMenuItem + .getActionProvider(); + mSharePanoramaActionProvider.setOnShareTargetSelectedListener( + mShareTargetSelectedListener); + mSharePanoramaActionProvider.setShareHistoryFileName("panorama_share_history.xml"); + } + mShareMenuItem = menu.findItem(R.id.action_share); + if (mShareMenuItem != null) { + mShareActionProvider = (ShareActionProvider) mShareMenuItem + .getActionProvider(); + mShareActionProvider.setOnShareTargetSelectedListener( + mShareTargetSelectedListener); + mShareActionProvider.setShareHistoryFileName("share_history.xml"); + } + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mSelectionManager.leaveSelectionMode(); + } + + private ArrayList<MediaObject> getSelectedMediaObjects(JobContext jc) { + ArrayList<Path> unexpandedPaths = mSelectionManager.getSelected(false); + if (unexpandedPaths.isEmpty()) { + // This happens when starting selection mode from overflow menu + // (instead of long press a media object) + return null; + } + ArrayList<MediaObject> selected = new ArrayList<MediaObject>(); + DataManager manager = mActivity.getDataManager(); + for (Path path : unexpandedPaths) { + if (jc.isCancelled()) { + return null; + } + selected.add(manager.getMediaObject(path)); + } + + return selected; + } + // Menu options are determined by selection set itself. + // We cannot expand it because MenuExecuter executes it based on + // the selection set instead of the expanded result. + // e.g. LocalImage can be rotated but collections of them (LocalAlbum) can't. + private int computeMenuOptions(ArrayList<MediaObject> selected) { + int operation = MediaObject.SUPPORT_ALL; + int type = 0; + for (MediaObject mediaObject: selected) { + int support = mediaObject.getSupportedOperations(); + type |= mediaObject.getMediaType(); + operation &= support; + } + + switch (selected.size()) { + case 1: + final String mimeType = MenuExecutor.getMimeType(type); + if (!GalleryUtils.isEditorAvailable(mActivity, mimeType)) { + operation &= ~MediaObject.SUPPORT_EDIT; + } + break; + default: + operation &= SUPPORT_MULTIPLE_MASK; + } + + return operation; + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private void setNfcBeamPushUris(Uri[] uris) { + if (mNfcAdapter != null && ApiHelper.HAS_SET_BEAM_PUSH_URIS) { + mNfcAdapter.setBeamPushUrisCallback(null, mActivity); + mNfcAdapter.setBeamPushUris(uris, mActivity); + } + } + + // Share intent needs to expand the selection set so we can get URI of + // each media item + private Intent computePanoramaSharingIntent(JobContext jc, int maxItems) { + ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true, maxItems); + if (expandedPaths == null || expandedPaths.size() == 0) { + return new Intent(); + } + final ArrayList<Uri> uris = new ArrayList<Uri>(); + DataManager manager = mActivity.getDataManager(); + final Intent intent = new Intent(); + for (Path path : expandedPaths) { + if (jc.isCancelled()) return null; + uris.add(manager.getContentUri(path)); + } + + final int size = uris.size(); + if (size > 0) { + if (size > 1) { + intent.setAction(Intent.ACTION_SEND_MULTIPLE); + intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + } else { + intent.setAction(Intent.ACTION_SEND); + intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360); + intent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); + } + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + + return intent; + } + + private Intent computeSharingIntent(JobContext jc, int maxItems) { + ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true, maxItems); + if (expandedPaths == null || expandedPaths.size() == 0) { + setNfcBeamPushUris(null); + return new Intent(); + } + final ArrayList<Uri> uris = new ArrayList<Uri>(); + DataManager manager = mActivity.getDataManager(); + int type = 0; + final Intent intent = new Intent(); + for (Path path : expandedPaths) { + if (jc.isCancelled()) return null; + int support = manager.getSupportedOperations(path); + type |= manager.getMediaType(path); + + if ((support & MediaObject.SUPPORT_SHARE) != 0) { + uris.add(manager.getContentUri(path)); + } + } + + final int size = uris.size(); + if (size > 0) { + final String mimeType = MenuExecutor.getMimeType(type); + if (size > 1) { + intent.setAction(Intent.ACTION_SEND_MULTIPLE).setType(mimeType); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + } else { + intent.setAction(Intent.ACTION_SEND).setType(mimeType); + intent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); + } + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + setNfcBeamPushUris(uris.toArray(new Uri[uris.size()])); + } else { + setNfcBeamPushUris(null); + } + + return intent; + } + + public void updateSupportedOperation(Path path, boolean selected) { + // TODO: We need to improve the performance + updateSupportedOperation(); + } + + public void updateSupportedOperation() { + // Interrupt previous unfinished task, mMenuTask is only accessed in main thread + if (mMenuTask != null) mMenuTask.cancel(); + + updateSelectionMenu(); + + // Disable share actions until share intent is in good shape + if (mSharePanoramaMenuItem != null) mSharePanoramaMenuItem.setEnabled(false); + if (mShareMenuItem != null) mShareMenuItem.setEnabled(false); + + // Generate sharing intent and update supported operations in the background + // The task can take a long time and be canceled in the mean time. + mMenuTask = mActivity.getThreadPool().submit(new Job<Void>() { + @Override + public Void run(final JobContext jc) { + // Pass1: Deal with unexpanded media object list for menu operation. + ArrayList<MediaObject> selected = getSelectedMediaObjects(jc); + if (selected == null) { + mMainHandler.post(new Runnable() { + @Override + public void run() { + mMenuTask = null; + if (jc.isCancelled()) return; + // Disable all the operations when no item is selected + MenuExecutor.updateMenuOperation(mMenu, 0); + } + }); + return null; + } + final int operation = computeMenuOptions(selected); + if (jc.isCancelled()) { + return null; + } + int numSelected = selected.size(); + final boolean canSharePanoramas = + numSelected < MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT; + final boolean canShare = + numSelected < MAX_SELECTED_ITEMS_FOR_SHARE_INTENT; + + final GetAllPanoramaSupports supportCallback = canSharePanoramas ? + new GetAllPanoramaSupports(selected, jc) + : null; + + // Pass2: Deal with expanded media object list for sharing operation. + final Intent share_panorama_intent = canSharePanoramas ? + computePanoramaSharingIntent(jc, MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT) + : new Intent(); + final Intent share_intent = canShare ? + computeSharingIntent(jc, MAX_SELECTED_ITEMS_FOR_SHARE_INTENT) + : new Intent(); + + if (canSharePanoramas) { + supportCallback.waitForPanoramaSupport(); + } + if (jc.isCancelled()) { + return null; + } + mMainHandler.post(new Runnable() { + @Override + public void run() { + mMenuTask = null; + if (jc.isCancelled()) return; + MenuExecutor.updateMenuOperation(mMenu, operation); + MenuExecutor.updateMenuForPanorama(mMenu, + canSharePanoramas && supportCallback.mAllPanorama360, + canSharePanoramas && supportCallback.mHasPanorama360); + if (mSharePanoramaMenuItem != null) { + mSharePanoramaMenuItem.setEnabled(true); + if (canSharePanoramas && supportCallback.mAllPanorama360) { + mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + mShareMenuItem.setTitle( + mActivity.getResources().getString(R.string.share_as_photo)); + } else { + mSharePanoramaMenuItem.setVisible(false); + mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + mShareMenuItem.setTitle( + mActivity.getResources().getString(R.string.share)); + } + mSharePanoramaActionProvider.setShareIntent(share_panorama_intent); + } + if (mShareMenuItem != null) { + mShareMenuItem.setEnabled(canShare); + mShareActionProvider.setShareIntent(share_intent); + } + } + }); + return null; + } + }); + } + + public void pause() { + if (mMenuTask != null) { + mMenuTask.cancel(); + mMenuTask = null; + } + mMenuExecutor.pause(); + } + + public void destroy() { + mMenuExecutor.destroy(); + } + + public void resume() { + if (mSelectionManager.inSelectionMode()) updateSupportedOperation(); + mMenuExecutor.resume(); + } +} |