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 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     private WakeLockHoldingProgressListener mDeleteProgressListener;
    155 
    156     @Override
    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
    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 
    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
    231     public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
    232         return false;
    233     }
    234 
    235     @Override
    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
    260     public void onDestroyActionMode(ActionMode mode) {
    261         mSelectionManager.leaveSelectionMode();
    262     }
    263 
    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.
    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)
    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
    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 
    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 
    388     public void updateSupportedOperation(Path path, boolean selected) {
    389         // TODO: We need to improve the performance
    390         updateSupportedOperation();
    391     }
    392 
    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 
    461     public void pause() {
    462         if (mMenuTask != null) {
    463             mMenuTask.cancel();
    464             mMenuTask = null;
    465         }
    466         mMenuExecutor.pause();
    467     }
    468 
    469     public void resume() {
    470         if (mSelectionManager.inSelectionMode()) updateSupportedOperation();
    471     }
    472 }
    473