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