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