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