1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.gallery3d.ui; 18 19 import android.annotation.TargetApi; 20 import android.app.Activity; 21 import android.content.Intent; 22 import android.net.Uri; 23 import android.os.Handler; 24 import android.view.ActionMode; 25 import android.view.ActionMode.Callback; 26 import android.view.LayoutInflater; 27 import android.view.Menu; 28 import android.view.MenuItem; 29 import android.view.View; 30 import android.widget.Button; 31 import android.widget.ShareActionProvider; 32 import android.widget.ShareActionProvider.OnShareTargetSelectedListener; 33 34 import com.android.gallery3d.R; 35 import com.android.gallery3d.app.AbstractGalleryActivity; 36 import com.android.gallery3d.common.ApiHelper; 37 import com.android.gallery3d.common.Utils; 38 import com.android.gallery3d.data.DataManager; 39 import com.android.gallery3d.data.MediaObject; 40 import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback; 41 import com.android.gallery3d.data.Path; 42 import com.android.gallery3d.ui.MenuExecutor.ProgressListener; 43 import com.android.gallery3d.util.Future; 44 import com.android.gallery3d.util.GalleryUtils; 45 import com.android.gallery3d.util.ThreadPool.Job; 46 import com.android.gallery3d.util.ThreadPool.JobContext; 47 48 import java.util.ArrayList; 49 50 public class ActionModeHandler implements Callback, PopupList.OnPopupItemClickListener { 51 52 @SuppressWarnings("unused") 53 private static final String TAG = "ActionModeHandler"; 54 55 private static final int MAX_SELECTED_ITEMS_FOR_SHARE_INTENT = 300; 56 private static final int MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT = 10; 57 58 private static final int SUPPORT_MULTIPLE_MASK = MediaObject.SUPPORT_DELETE 59 | MediaObject.SUPPORT_ROTATE | MediaObject.SUPPORT_SHARE 60 | MediaObject.SUPPORT_CACHE; 61 62 public interface ActionModeListener { onActionItemClicked(MenuItem item)63 public boolean onActionItemClicked(MenuItem item); 64 } 65 66 private final AbstractGalleryActivity mActivity; 67 private final MenuExecutor mMenuExecutor; 68 private final SelectionManager mSelectionManager; 69 private Menu mMenu; 70 private MenuItem mSharePanoramaMenuItem; 71 private MenuItem mShareMenuItem; 72 private ShareActionProvider mSharePanoramaActionProvider; 73 private ShareActionProvider mShareActionProvider; 74 private SelectionMenu mSelectionMenu; 75 private ActionModeListener mListener; 76 private Future<?> mMenuTask; 77 private final Handler mMainHandler; 78 private ActionMode mActionMode; 79 80 private static class GetAllPanoramaSupports implements PanoramaSupportCallback { 81 private int mNumInfoRequired; 82 private JobContext mJobContext; 83 public boolean mAllPanoramas = true; 84 public boolean mAllPanorama360 = true; 85 public boolean mHasPanorama360 = false; 86 private Object mLock = new Object(); 87 GetAllPanoramaSupports(ArrayList<MediaObject> mediaObjects, JobContext jc)88 public GetAllPanoramaSupports(ArrayList<MediaObject> mediaObjects, JobContext jc) { 89 mJobContext = jc; 90 mNumInfoRequired = mediaObjects.size(); 91 for (MediaObject mediaObject : mediaObjects) { 92 mediaObject.getPanoramaSupport(this); 93 } 94 } 95 96 @Override panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama, boolean isPanorama360)97 public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama, 98 boolean isPanorama360) { 99 synchronized (mLock) { 100 mNumInfoRequired--; 101 mAllPanoramas = isPanorama && mAllPanoramas; 102 mAllPanorama360 = isPanorama360 && mAllPanorama360; 103 mHasPanorama360 = mHasPanorama360 || isPanorama360; 104 if (mNumInfoRequired == 0 || mJobContext.isCancelled()) { 105 mLock.notifyAll(); 106 } 107 } 108 } 109 waitForPanoramaSupport()110 public void waitForPanoramaSupport() { 111 synchronized (mLock) { 112 while (mNumInfoRequired != 0 && !mJobContext.isCancelled()) { 113 try { 114 mLock.wait(); 115 } catch (InterruptedException e) { 116 // May be a cancelled job context 117 } 118 } 119 } 120 } 121 } 122 ActionModeHandler( AbstractGalleryActivity activity, SelectionManager selectionManager)123 public ActionModeHandler( 124 AbstractGalleryActivity activity, SelectionManager selectionManager) { 125 mActivity = Utils.checkNotNull(activity); 126 mSelectionManager = Utils.checkNotNull(selectionManager); 127 mMenuExecutor = new MenuExecutor(activity, selectionManager); 128 mMainHandler = new Handler(activity.getMainLooper()); 129 } 130 startActionMode()131 public void startActionMode() { 132 Activity a = mActivity; 133 mActionMode = a.startActionMode(this); 134 View customView = LayoutInflater.from(a).inflate( 135 R.layout.action_mode, null); 136 mActionMode.setCustomView(customView); 137 mSelectionMenu = new SelectionMenu(a, 138 (Button) customView.findViewById(R.id.selection_menu), this); 139 updateSelectionMenu(); 140 } 141 finishActionMode()142 public void finishActionMode() { 143 mActionMode.finish(); 144 } 145 setTitle(String title)146 public void setTitle(String title) { 147 mSelectionMenu.setTitle(title); 148 } 149 setActionModeListener(ActionModeListener listener)150 public void setActionModeListener(ActionModeListener listener) { 151 mListener = listener; 152 } 153 154 private WakeLockHoldingProgressListener mDeleteProgressListener; 155 156 @Override onActionItemClicked(ActionMode mode, MenuItem item)157 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 158 GLRoot root = mActivity.getGLRoot(); 159 root.lockRenderThread(); 160 try { 161 boolean result; 162 // Give listener a chance to process this command before it's routed to 163 // ActionModeHandler, which handles command only based on the action id. 164 // Sometimes the listener may have more background information to handle 165 // an action command. 166 if (mListener != null) { 167 result = mListener.onActionItemClicked(item); 168 if (result) { 169 mSelectionManager.leaveSelectionMode(); 170 return result; 171 } 172 } 173 ProgressListener listener = null; 174 String confirmMsg = null; 175 int action = item.getItemId(); 176 if (action == R.id.action_delete) { 177 confirmMsg = mActivity.getResources().getQuantityString( 178 R.plurals.delete_selection, mSelectionManager.getSelectedCount()); 179 if (mDeleteProgressListener == null) { 180 mDeleteProgressListener = new WakeLockHoldingProgressListener(mActivity, 181 "Gallery Delete Progress Listener"); 182 } 183 listener = mDeleteProgressListener; 184 } 185 mMenuExecutor.onMenuClicked(item, confirmMsg, listener); 186 } finally { 187 root.unlockRenderThread(); 188 } 189 return true; 190 } 191 192 @Override onPopupItemClick(int itemId)193 public boolean onPopupItemClick(int itemId) { 194 GLRoot root = mActivity.getGLRoot(); 195 root.lockRenderThread(); 196 try { 197 if (itemId == R.id.action_select_all) { 198 updateSupportedOperation(); 199 mMenuExecutor.onMenuClicked(itemId, null, false, true); 200 } 201 return true; 202 } finally { 203 root.unlockRenderThread(); 204 } 205 } 206 updateSelectionMenu()207 private void updateSelectionMenu() { 208 // update title 209 int count = mSelectionManager.getSelectedCount(); 210 String format = mActivity.getResources().getQuantityString( 211 R.plurals.number_of_items_selected, count); 212 setTitle(String.format(format, count)); 213 214 // For clients who call SelectionManager.selectAll() directly, we need to ensure the 215 // menu status is consistent with selection manager. 216 mSelectionMenu.updateSelectAllMode(mSelectionManager.inSelectAllMode()); 217 } 218 219 private final OnShareTargetSelectedListener mShareTargetSelectedListener = 220 new OnShareTargetSelectedListener() { 221 @Override 222 public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) { 223 mSelectionManager.leaveSelectionMode(); 224 return false; 225 } 226 }; 227 228 @Override onPrepareActionMode(ActionMode mode, Menu menu)229 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 230 return false; 231 } 232 233 @Override onCreateActionMode(ActionMode mode, Menu menu)234 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 235 mode.getMenuInflater().inflate(R.menu.operation, menu); 236 237 mMenu = menu; 238 mSharePanoramaMenuItem = menu.findItem(R.id.action_share_panorama); 239 if (mSharePanoramaMenuItem != null) { 240 mSharePanoramaActionProvider = (ShareActionProvider) mSharePanoramaMenuItem 241 .getActionProvider(); 242 mSharePanoramaActionProvider.setOnShareTargetSelectedListener( 243 mShareTargetSelectedListener); 244 mSharePanoramaActionProvider.setShareHistoryFileName("panorama_share_history.xml"); 245 } 246 mShareMenuItem = menu.findItem(R.id.action_share); 247 if (mShareMenuItem != null) { 248 mShareActionProvider = (ShareActionProvider) mShareMenuItem 249 .getActionProvider(); 250 mShareActionProvider.setOnShareTargetSelectedListener( 251 mShareTargetSelectedListener); 252 mShareActionProvider.setShareHistoryFileName("share_history.xml"); 253 } 254 return true; 255 } 256 257 @Override onDestroyActionMode(ActionMode mode)258 public void onDestroyActionMode(ActionMode mode) { 259 mSelectionManager.leaveSelectionMode(); 260 } 261 getSelectedMediaObjects(JobContext jc)262 private ArrayList<MediaObject> getSelectedMediaObjects(JobContext jc) { 263 ArrayList<Path> unexpandedPaths = mSelectionManager.getSelected(false); 264 if (unexpandedPaths.isEmpty()) { 265 // This happens when starting selection mode from overflow menu 266 // (instead of long press a media object) 267 return null; 268 } 269 ArrayList<MediaObject> selected = new ArrayList<MediaObject>(); 270 DataManager manager = mActivity.getDataManager(); 271 for (Path path : unexpandedPaths) { 272 if (jc.isCancelled()) { 273 return null; 274 } 275 selected.add(manager.getMediaObject(path)); 276 } 277 278 return selected; 279 } 280 // Menu options are determined by selection set itself. 281 // We cannot expand it because MenuExecuter executes it based on 282 // the selection set instead of the expanded result. 283 // e.g. LocalImage can be rotated but collections of them (LocalAlbum) can't. computeMenuOptions(ArrayList<MediaObject> selected)284 private int computeMenuOptions(ArrayList<MediaObject> selected) { 285 int operation = MediaObject.SUPPORT_ALL; 286 int type = 0; 287 for (MediaObject mediaObject: selected) { 288 int support = mediaObject.getSupportedOperations(); 289 type |= mediaObject.getMediaType(); 290 operation &= support; 291 } 292 293 switch (selected.size()) { 294 case 1: 295 final String mimeType = MenuExecutor.getMimeType(type); 296 if (!GalleryUtils.isEditorAvailable(mActivity, mimeType)) { 297 operation &= ~MediaObject.SUPPORT_EDIT; 298 } 299 break; 300 default: 301 operation &= SUPPORT_MULTIPLE_MASK; 302 } 303 304 return operation; 305 } 306 307 // Share intent needs to expand the selection set so we can get URI of 308 // each media item computePanoramaSharingIntent(JobContext jc, int maxItems)309 private Intent computePanoramaSharingIntent(JobContext jc, int maxItems) { 310 ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true, maxItems); 311 if (expandedPaths == null || expandedPaths.size() == 0) { 312 return new Intent(); 313 } 314 final ArrayList<Uri> uris = new ArrayList<Uri>(); 315 DataManager manager = mActivity.getDataManager(); 316 final Intent intent = new Intent(); 317 for (Path path : expandedPaths) { 318 if (jc.isCancelled()) return null; 319 uris.add(manager.getContentUri(path)); 320 } 321 322 final int size = uris.size(); 323 if (size > 0) { 324 if (size > 1) { 325 intent.setAction(Intent.ACTION_SEND_MULTIPLE); 326 intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360); 327 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); 328 } else { 329 intent.setAction(Intent.ACTION_SEND); 330 intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360); 331 intent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); 332 } 333 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 334 } 335 336 return intent; 337 } 338 computeSharingIntent(JobContext jc, int maxItems)339 private Intent computeSharingIntent(JobContext jc, int maxItems) { 340 ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true, maxItems); 341 if (expandedPaths == null || expandedPaths.size() == 0) { 342 return new Intent(); 343 } 344 final ArrayList<Uri> uris = new ArrayList<Uri>(); 345 DataManager manager = mActivity.getDataManager(); 346 int type = 0; 347 final Intent intent = new Intent(); 348 for (Path path : expandedPaths) { 349 if (jc.isCancelled()) return null; 350 int support = manager.getSupportedOperations(path); 351 type |= manager.getMediaType(path); 352 353 if ((support & MediaObject.SUPPORT_SHARE) != 0) { 354 uris.add(manager.getContentUri(path)); 355 } 356 } 357 358 final int size = uris.size(); 359 if (size > 0) { 360 final String mimeType = MenuExecutor.getMimeType(type); 361 if (size > 1) { 362 intent.setAction(Intent.ACTION_SEND_MULTIPLE).setType(mimeType); 363 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); 364 } else { 365 intent.setAction(Intent.ACTION_SEND).setType(mimeType); 366 intent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); 367 } 368 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 369 } 370 371 return intent; 372 } 373 updateSupportedOperation(Path path, boolean selected)374 public void updateSupportedOperation(Path path, boolean selected) { 375 // TODO: We need to improve the performance 376 updateSupportedOperation(); 377 } 378 updateSupportedOperation()379 public void updateSupportedOperation() { 380 // Interrupt previous unfinished task, mMenuTask is only accessed in main thread 381 if (mMenuTask != null) mMenuTask.cancel(); 382 383 updateSelectionMenu(); 384 385 // Disable share actions until share intent is in good shape 386 if (mSharePanoramaMenuItem != null) mSharePanoramaMenuItem.setEnabled(false); 387 if (mShareMenuItem != null) mShareMenuItem.setEnabled(false); 388 389 // Generate sharing intent and update supported operations in the background 390 // The task can take a long time and be canceled in the mean time. 391 mMenuTask = mActivity.getThreadPool().submit(new Job<Void>() { 392 @Override 393 public Void run(final JobContext jc) { 394 // Pass1: Deal with unexpanded media object list for menu operation. 395 ArrayList<MediaObject> selected = getSelectedMediaObjects(jc); 396 if (selected == null) { 397 mMainHandler.post(new Runnable() { 398 @Override 399 public void run() { 400 mMenuTask = null; 401 if (jc.isCancelled()) return; 402 // Disable all the operations when no item is selected 403 MenuExecutor.updateMenuOperation(mMenu, 0); 404 } 405 }); 406 return null; 407 } 408 final int operation = computeMenuOptions(selected); 409 if (jc.isCancelled()) { 410 return null; 411 } 412 int numSelected = selected.size(); 413 final boolean canSharePanoramas = 414 numSelected < MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT; 415 final boolean canShare = 416 numSelected < MAX_SELECTED_ITEMS_FOR_SHARE_INTENT; 417 418 final GetAllPanoramaSupports supportCallback = canSharePanoramas ? 419 new GetAllPanoramaSupports(selected, jc) 420 : null; 421 422 // Pass2: Deal with expanded media object list for sharing operation. 423 final Intent share_panorama_intent = canSharePanoramas ? 424 computePanoramaSharingIntent(jc, MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT) 425 : new Intent(); 426 final Intent share_intent = canShare ? 427 computeSharingIntent(jc, MAX_SELECTED_ITEMS_FOR_SHARE_INTENT) 428 : new Intent(); 429 430 if (canSharePanoramas) { 431 supportCallback.waitForPanoramaSupport(); 432 } 433 if (jc.isCancelled()) { 434 return null; 435 } 436 mMainHandler.post(new Runnable() { 437 @Override 438 public void run() { 439 mMenuTask = null; 440 if (jc.isCancelled()) return; 441 MenuExecutor.updateMenuOperation(mMenu, operation); 442 MenuExecutor.updateMenuForPanorama(mMenu, 443 canSharePanoramas && supportCallback.mAllPanorama360, 444 canSharePanoramas && supportCallback.mHasPanorama360); 445 if (mSharePanoramaMenuItem != null) { 446 mSharePanoramaMenuItem.setEnabled(true); 447 if (canSharePanoramas && supportCallback.mAllPanorama360) { 448 mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); 449 mShareMenuItem.setTitle( 450 mActivity.getResources().getString(R.string.share_as_photo)); 451 } else { 452 mSharePanoramaMenuItem.setVisible(false); 453 mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 454 mShareMenuItem.setTitle( 455 mActivity.getResources().getString(R.string.share)); 456 } 457 mSharePanoramaActionProvider.setShareIntent(share_panorama_intent); 458 } 459 if (mShareMenuItem != null) { 460 mShareMenuItem.setEnabled(canShare); 461 mShareActionProvider.setShareIntent(share_intent); 462 } 463 } 464 }); 465 return null; 466 } 467 }); 468 } 469 pause()470 public void pause() { 471 if (mMenuTask != null) { 472 mMenuTask.cancel(); 473 mMenuTask = null; 474 } 475 mMenuExecutor.pause(); 476 } 477 destroy()478 public void destroy() { 479 mMenuExecutor.destroy(); 480 } 481 resume()482 public void resume() { 483 if (mSelectionManager.inSelectionMode()) updateSupportedOperation(); 484 mMenuExecutor.resume(); 485 } 486 } 487