ActionModeHandler.java revision 4b4dbd225685502f4249c2bf25bf74f7ce526645
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 17package com.android.gallery3d.ui; 18 19import android.annotation.TargetApi; 20import android.app.Activity; 21import android.content.Intent; 22import android.net.Uri; 23import android.nfc.NfcAdapter; 24import android.os.Handler; 25import android.view.ActionMode; 26import android.view.ActionMode.Callback; 27import android.view.LayoutInflater; 28import android.view.Menu; 29import android.view.MenuItem; 30import android.view.View; 31import android.widget.Button; 32import android.widget.ShareActionProvider; 33import android.widget.ShareActionProvider.OnShareTargetSelectedListener; 34 35import com.android.gallery3d.R; 36import com.android.gallery3d.app.AbstractGalleryActivity; 37import com.android.gallery3d.common.ApiHelper; 38import com.android.gallery3d.common.Utils; 39import com.android.gallery3d.data.DataManager; 40import com.android.gallery3d.data.MediaObject; 41import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback; 42import com.android.gallery3d.data.Path; 43import com.android.gallery3d.ui.MenuExecutor.ProgressListener; 44import com.android.gallery3d.util.Future; 45import com.android.gallery3d.util.GalleryUtils; 46import com.android.gallery3d.util.ThreadPool.Job; 47import com.android.gallery3d.util.ThreadPool.JobContext; 48 49import java.util.ArrayList; 50 51public 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 { 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 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 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 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 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 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 142 public void finishActionMode() { 143 mActionMode.finish(); 144 } 145 146 public void setTitle(String title) { 147 mSelectionMenu.setTitle(title); 148 } 149 150 public void setActionModeListener(ActionModeListener listener) { 151 mListener = listener; 152 } 153 154 @Override 155 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 156 GLRoot root = mActivity.getGLRoot(); 157 root.lockRenderThread(); 158 try { 159 boolean result; 160 // Give listener a chance to process this command before it's routed to 161 // ActionModeHandler, which handles command only based on the action id. 162 // Sometimes the listener may have more background information to handle 163 // an action command. 164 if (mListener != null) { 165 result = mListener.onActionItemClicked(item); 166 if (result) { 167 mSelectionManager.leaveSelectionMode(); 168 return result; 169 } 170 } 171 ProgressListener listener = null; 172 String confirmMsg = null; 173 int action = item.getItemId(); 174 if (action == R.id.action_import) { 175 listener = new ImportCompleteListener(mActivity); 176 } else if (item.getItemId() == R.id.action_delete) { 177 confirmMsg = mActivity.getResources().getQuantityString( 178 R.plurals.delete_selection, mSelectionManager.getSelectedCount()); 179 } 180 mMenuExecutor.onMenuClicked(item, confirmMsg, listener); 181 } finally { 182 root.unlockRenderThread(); 183 } 184 return true; 185 } 186 187 @Override 188 public boolean onPopupItemClick(int itemId) { 189 GLRoot root = mActivity.getGLRoot(); 190 root.lockRenderThread(); 191 try { 192 if (itemId == R.id.action_select_all) { 193 updateSupportedOperation(); 194 mMenuExecutor.onMenuClicked(itemId, null, false, true); 195 } 196 return true; 197 } finally { 198 root.unlockRenderThread(); 199 } 200 } 201 202 private void updateSelectionMenu() { 203 // update title 204 int count = mSelectionManager.getSelectedCount(); 205 String format = mActivity.getResources().getQuantityString( 206 R.plurals.number_of_items_selected, count); 207 setTitle(String.format(format, count)); 208 209 // For clients who call SelectionManager.selectAll() directly, we need to ensure the 210 // menu status is consistent with selection manager. 211 mSelectionMenu.updateSelectAllMode(mSelectionManager.inSelectAllMode()); 212 } 213 214 private final OnShareTargetSelectedListener mShareTargetSelectedListener = 215 new OnShareTargetSelectedListener() { 216 @Override 217 public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) { 218 mSelectionManager.leaveSelectionMode(); 219 return false; 220 } 221 }; 222 223 @Override 224 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 225 return false; 226 } 227 228 @Override 229 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 230 mode.getMenuInflater().inflate(R.menu.operation, menu); 231 232 mMenu = menu; 233 mSharePanoramaMenuItem = menu.findItem(R.id.action_share_panorama); 234 if (mSharePanoramaMenuItem != null) { 235 mSharePanoramaActionProvider = (ShareActionProvider) mSharePanoramaMenuItem 236 .getActionProvider(); 237 mSharePanoramaActionProvider.setOnShareTargetSelectedListener( 238 mShareTargetSelectedListener); 239 mSharePanoramaActionProvider.setShareHistoryFileName("panorama_share_history.xml"); 240 } 241 mShareMenuItem = menu.findItem(R.id.action_share); 242 if (mShareMenuItem != null) { 243 mShareActionProvider = (ShareActionProvider) mShareMenuItem 244 .getActionProvider(); 245 mShareActionProvider.setOnShareTargetSelectedListener( 246 mShareTargetSelectedListener); 247 mShareActionProvider.setShareHistoryFileName("share_history.xml"); 248 } 249 return true; 250 } 251 252 @Override 253 public void onDestroyActionMode(ActionMode mode) { 254 mSelectionManager.leaveSelectionMode(); 255 } 256 257 private ArrayList<MediaObject> getSelectedMediaObjects(JobContext jc) { 258 ArrayList<Path> unexpandedPaths = mSelectionManager.getSelected(false); 259 if (unexpandedPaths.isEmpty()) { 260 // This happens when starting selection mode from overflow menu 261 // (instead of long press a media object) 262 return null; 263 } 264 ArrayList<MediaObject> selected = new ArrayList<MediaObject>(); 265 DataManager manager = mActivity.getDataManager(); 266 for (Path path : unexpandedPaths) { 267 if (jc.isCancelled()) { 268 return null; 269 } 270 selected.add(manager.getMediaObject(path)); 271 } 272 273 return selected; 274 } 275 // Menu options are determined by selection set itself. 276 // We cannot expand it because MenuExecuter executes it based on 277 // the selection set instead of the expanded result. 278 // e.g. LocalImage can be rotated but collections of them (LocalAlbum) can't. 279 private int computeMenuOptions(ArrayList<MediaObject> selected) { 280 int operation = MediaObject.SUPPORT_ALL; 281 int type = 0; 282 for (MediaObject mediaObject: selected) { 283 int support = mediaObject.getSupportedOperations(); 284 type |= mediaObject.getMediaType(); 285 operation &= support; 286 } 287 288 switch (selected.size()) { 289 case 1: 290 final String mimeType = MenuExecutor.getMimeType(type); 291 if (!GalleryUtils.isEditorAvailable(mActivity, mimeType)) { 292 operation &= ~MediaObject.SUPPORT_EDIT; 293 } 294 break; 295 default: 296 operation &= SUPPORT_MULTIPLE_MASK; 297 } 298 299 return operation; 300 } 301 302 @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) 303 private void setNfcBeamPushUris(Uri[] uris) { 304 if (mNfcAdapter != null && ApiHelper.HAS_SET_BEAM_PUSH_URIS) { 305 mNfcAdapter.setBeamPushUrisCallback(null, mActivity); 306 mNfcAdapter.setBeamPushUris(uris, mActivity); 307 } 308 } 309 310 // Share intent needs to expand the selection set so we can get URI of 311 // each media item 312 private Intent computePanoramaSharingIntent(JobContext jc) { 313 ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true); 314 if (expandedPaths.size() == 0) { 315 return null; 316 } 317 final ArrayList<Uri> uris = new ArrayList<Uri>(); 318 DataManager manager = mActivity.getDataManager(); 319 final Intent intent = new Intent(); 320 for (Path path : expandedPaths) { 321 if (jc.isCancelled()) return null; 322 uris.add(manager.getContentUri(path)); 323 } 324 325 final int size = uris.size(); 326 if (size > 0) { 327 if (size > 1) { 328 intent.setAction(Intent.ACTION_SEND_MULTIPLE); 329 intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360); 330 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); 331 } else { 332 intent.setAction(Intent.ACTION_SEND); 333 intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360); 334 intent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); 335 } 336 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 337 } 338 339 return intent; 340 } 341 342 private Intent computeSharingIntent(JobContext jc) { 343 ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true); 344 if (expandedPaths.size() == 0) { 345 setNfcBeamPushUris(null); 346 return null; 347 } 348 final ArrayList<Uri> uris = new ArrayList<Uri>(); 349 DataManager manager = mActivity.getDataManager(); 350 int type = 0; 351 final Intent intent = new Intent(); 352 for (Path path : expandedPaths) { 353 if (jc.isCancelled()) return null; 354 int support = manager.getSupportedOperations(path); 355 type |= manager.getMediaType(path); 356 357 if ((support & MediaObject.SUPPORT_SHARE) != 0) { 358 uris.add(manager.getContentUri(path)); 359 } 360 } 361 362 final int size = uris.size(); 363 if (size > 0) { 364 final String mimeType = MenuExecutor.getMimeType(type); 365 if (size > 1) { 366 intent.setAction(Intent.ACTION_SEND_MULTIPLE).setType(mimeType); 367 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); 368 } else { 369 intent.setAction(Intent.ACTION_SEND).setType(mimeType); 370 intent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); 371 } 372 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 373 setNfcBeamPushUris(uris.toArray(new Uri[uris.size()])); 374 } else { 375 setNfcBeamPushUris(null); 376 } 377 378 return intent; 379 } 380 381 public void updateSupportedOperation(Path path, boolean selected) { 382 // TODO: We need to improve the performance 383 updateSupportedOperation(); 384 } 385 386 public void updateSupportedOperation() { 387 // Interrupt previous unfinished task, mMenuTask is only accessed in main thread 388 if (mMenuTask != null) mMenuTask.cancel(); 389 390 updateSelectionMenu(); 391 392 // Disable share actions until share intent is in good shape 393 if (mSharePanoramaMenuItem != null) mSharePanoramaMenuItem.setEnabled(false); 394 if (mShareMenuItem != null) mShareMenuItem.setEnabled(false); 395 396 // Generate sharing intent and update supported operations in the background 397 // The task can take a long time and be canceled in the mean time. 398 mMenuTask = mActivity.getThreadPool().submit(new Job<Void>() { 399 @Override 400 public Void run(final JobContext jc) { 401 // Pass1: Deal with unexpanded media object list for menu operation. 402 ArrayList<MediaObject> selected = getSelectedMediaObjects(jc); 403 if (selected == null) { 404 return null; 405 } 406 final int operation = computeMenuOptions(selected); 407 if (jc.isCancelled()) { 408 return null; 409 } 410 final GetAllPanoramaSupports supportCallback = new GetAllPanoramaSupports(selected, 411 jc); 412 413 // Pass2: Deal with expanded media object list for sharing operation. 414 final Intent share_panorama_intent = computePanoramaSharingIntent(jc); 415 final Intent share_intent = computeSharingIntent(jc); 416 417 supportCallback.waitForPanoramaSupport(); 418 if (jc.isCancelled()) { 419 return null; 420 } 421 mMainHandler.post(new Runnable() { 422 @Override 423 public void run() { 424 mMenuTask = null; 425 if (jc.isCancelled()) return; 426 MenuExecutor.updateMenuOperation(mMenu, operation); 427 MenuExecutor.updateMenuForPanorama(mMenu, supportCallback.mAllPanorama360, 428 supportCallback.mHasPanorama360); 429 if (mSharePanoramaMenuItem != null) { 430 mSharePanoramaMenuItem.setEnabled(true); 431 if (supportCallback.mAllPanorama360) { 432 mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); 433 mShareMenuItem.setTitle( 434 mActivity.getResources().getString(R.string.share_as_photo)); 435 } else { 436 mSharePanoramaMenuItem.setVisible(false); 437 mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 438 mShareMenuItem.setTitle( 439 mActivity.getResources().getString(R.string.share)); 440 } 441 mSharePanoramaActionProvider.setShareIntent(share_panorama_intent); 442 } 443 if (mShareMenuItem != null) { 444 mShareMenuItem.setEnabled(true); 445 mShareActionProvider.setShareIntent(share_intent); 446 } 447 } 448 }); 449 return null; 450 } 451 }); 452 } 453 454 public void pause() { 455 if (mMenuTask != null) { 456 mMenuTask.cancel(); 457 mMenuTask = null; 458 } 459 mMenuExecutor.pause(); 460 } 461 462 public void resume() { 463 if (mSelectionManager.inSelectionMode()) updateSupportedOperation(); 464 } 465} 466