Home | History | Annotate | Download | only in ui
      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