| /* |
| * 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.app.AlertDialog; |
| 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.Toolbar; |
| |
| 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.MediaSet; |
| 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); |
| } |
| |
| public static boolean isThreadComplete = false; |
| |
| 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 SelectionMenu mSelectionMenu; |
| private ActionModeListener mListener; |
| private Future<?> mMenuTask; |
| private final Handler mMainHandler; |
| private ActionMode mActionMode; |
| private boolean mShareMaxDialog = false; |
| private Toolbar mToolbar; |
| private Intent mShareIntent; |
| private Intent mSharePanoramaIntent; |
| |
| 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()); |
| mToolbar = mActivity.getToolbar(); |
| } |
| |
| public void startActionMode() { |
| mMenuExecutor.setLeaving(false); |
| Activity a = mActivity; |
| // mActionMode = a.startActionMode(this); |
| mActionMode = mActivity.getToolbar().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; |
| } else if (action == R.id.action_share) { |
| String shareTitle = mActivity.getResources(). |
| getString(R.string.share_dialogue_title); |
| mActivity.startActivity(Intent.createChooser( |
| mShareIntent, shareTitle)); |
| return true; |
| } |
| 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); |
| mSelectionMenu.setLeaving(mMenuExecutor.isLeaving()); |
| } |
| 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)); |
| if (count == 0) { |
| mShareMaxDialog = false; |
| } |
| |
| // For clients who call SelectionManager.selectAll() directly, we need to ensure the |
| // menu status is consistent with selection manager. |
| mSelectionMenu.updateSelectAllMode(mSelectionManager.inSelectAllMode()); |
| } |
| |
| @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); |
| mActivity.getToolbar().setVisibility(View.INVISIBLE); |
| |
| mMenu = menu; |
| mSharePanoramaMenuItem = menu.findItem(R.id.action_share_panorama); |
| if (mSharePanoramaMenuItem != null) { |
| mSharePanoramaMenuItem.setOnMenuItemClickListener( |
| new MenuItem.OnMenuItemClickListener() { |
| @Override |
| public boolean onMenuItemClick(MenuItem item) { |
| mSelectionManager.leaveSelectionMode(); |
| if (mSharePanoramaIntent != null) { |
| Intent intent = Intent.createChooser(mSharePanoramaIntent, null); |
| intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| mActivity.startActivity(intent); |
| } |
| return true; |
| } |
| }); |
| } |
| mShareMenuItem = menu.findItem(R.id.action_share); |
| return true; |
| } |
| |
| @Override |
| public void onDestroyActionMode(ActionMode mode) { |
| mSelectionManager.leaveSelectionMode(); |
| mActivity.getToolbar().setVisibility(View.VISIBLE); |
| |
| } |
| |
| 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() || !mSelectionManager.inSelectionMode()) { |
| return null; |
| } |
| MediaObject mediaObject = manager.getMediaObject(path); |
| if (mediaObject != null && mediaObject.isSelectable()) { |
| selected.add(mediaObject); |
| } |
| } |
| 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 = computeMenuOptionsMultiple(operation, selected.size()); |
| } |
| return operation; |
| } |
| |
| private int computeMenuOptionsMultiple(int operationBefore, int selectedSize) { |
| int operation = operationBefore; |
| if (selectedSize > 1) { |
| operation &= SUPPORT_MULTIPLE_MASK; |
| } |
| return operation; |
| } |
| |
| private boolean computeCanShare(ArrayList<MediaObject> selected, int max) { |
| int numSelected = selected.size(); |
| boolean ret = computeCanShare(numSelected, max); |
| if (!ret) return false; |
| |
| numSelected = 0; |
| for (MediaObject mediaObject : selected) { |
| if (mediaObject instanceof MediaSet) { |
| numSelected = numSelected + ((MediaSet) mediaObject).getTotalMediaItemCount(); |
| } else { |
| numSelected = numSelected + 1; |
| } |
| ret = computeCanShare(numSelected, max); |
| if (!ret) return false; |
| } |
| return true; |
| } |
| |
| private boolean computeCanShare(int size, int max) { |
| return size <= max; |
| } |
| |
| @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; |
| Uri uri = manager.getContentUri(path); |
| if (uri != null) { |
| uris.add(uri); |
| } |
| } |
| |
| 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.setDataAndType(uris.get(0), 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) { |
| Uri uri = manager.getContentUri(path); |
| if (uri != null) { |
| uris.add(uri); |
| } |
| } |
| } |
| |
| 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).setDataAndType(uris.get(0), 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); |
| isThreadComplete = 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) { |
| // use selected count to disable menu options and share items if need |
| // to enhance performance. |
| int multipleOperation = SUPPORT_MULTIPLE_MASK; |
| int count = mSelectionManager.getSelectedCount(); |
| final int operationPre = computeMenuOptionsMultiple(multipleOperation, count); |
| final boolean canSharePanoramasPre = computeCanShare(count, |
| MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT); |
| final boolean canSharePre = computeCanShare(count, |
| MAX_SELECTED_ITEMS_FOR_SHARE_INTENT); |
| |
| mMainHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| if (jc.isCancelled()) return; |
| MenuExecutor.updateMenuOperation(mMenu, operationPre); |
| if (mShareMenuItem != null && !canSharePre) { |
| mShareMenuItem.setEnabled(false); |
| showShareMaxDialogIfNeed(false); |
| } |
| } |
| }); |
| |
| // 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; |
| } |
| |
| // use selected items to compute menu options and share items again. |
| final boolean canShare = canSharePre |
| && computeCanShare(selected, MAX_SELECTED_ITEMS_FOR_SHARE_INTENT); |
| final boolean canSharePanoramas = canSharePanoramasPre |
| && computeCanShare(selected, MAX_SELECTED_ITEMS_FOR_PANORAMA_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)); |
| } |
| mSharePanoramaIntent = share_panorama_intent; |
| } |
| if (mShareMenuItem != null) { |
| showShareMaxDialogIfNeed(canShare); |
| |
| mShareMenuItem.setEnabled(canShare); |
| isThreadComplete = true; |
| mShareIntent = share_intent; |
| } |
| } |
| }); |
| return null; |
| } |
| }); |
| } |
| |
| private void showShareMaxDialogIfNeed(boolean canShare) { |
| if (!canShare && !mShareMaxDialog) { |
| AlertDialog.Builder shareMaxDialog = new AlertDialog.Builder(mActivity); |
| shareMaxDialog |
| .setMessage(R.string.cannot_share_items) |
| .setPositiveButton(R.string.ok, null) |
| .create(); |
| shareMaxDialog.show(); |
| mShareMaxDialog = true; |
| } |
| if (canShare && mShareMaxDialog) { |
| mShareMaxDialog = false; |
| } |
| } |
| |
| public void pause() { |
| if (mMenuTask != null) { |
| mMenuTask.cancel(); |
| mMenuTask = null; |
| } |
| mMenuExecutor.pause(); |
| } |
| |
| public void destroy() { |
| mMenuExecutor.destroy(); |
| } |
| |
| public void resume() { |
| if (mSelectionManager.inSelectionMode()) { |
| mActivity.getToolbar().setVisibility(View.INVISIBLE); |
| updateSupportedOperation(); |
| } |
| mMenuExecutor.resume(); |
| } |
| } |