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 {
onActionItemClicked(MenuItem item)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 
GetAllPanoramaSupports(ArrayList<MediaObject> mediaObjects, JobContext jc)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
panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama, boolean isPanorama360)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 
waitForPanoramaSupport()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 
ActionModeHandler( AbstractGalleryActivity activity, SelectionManager selectionManager)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 
startActionMode()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 
finishActionMode()145     public void finishActionMode() {
146         mActionMode.finish();
147     }
148 
setTitle(String title)149     public void setTitle(String title) {
150         mSelectionMenu.setTitle(title);
151     }
152 
setActionModeListener(ActionModeListener listener)153     public void setActionModeListener(ActionModeListener listener) {
154         mListener = listener;
155     }
156 
157     private WakeLockHoldingProgressListener mDeleteProgressListener;
158 
159     @Override
onActionItemClicked(ActionMode mode, MenuItem item)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
onPopupItemClick(int itemId)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 
updateSelectionMenu()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
onPrepareActionMode(ActionMode mode, Menu menu)232     public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
233         return false;
234     }
235 
236     @Override
onCreateActionMode(ActionMode mode, Menu menu)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
onDestroyActionMode(ActionMode mode)261     public void onDestroyActionMode(ActionMode mode) {
262         mSelectionManager.leaveSelectionMode();
263     }
264 
getSelectedMediaObjects(JobContext jc)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.
computeMenuOptions(ArrayList<MediaObject> selected)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)
setNfcBeamPushUris(Uri[] uris)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
computePanoramaSharingIntent(JobContext jc, int maxItems)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 
computeSharingIntent(JobContext jc, int maxItems)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 
updateSupportedOperation(Path path, boolean selected)389     public void updateSupportedOperation(Path path, boolean selected) {
390         // TODO: We need to improve the performance
391         updateSupportedOperation();
392     }
393 
updateSupportedOperation()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 
pause()485     public void pause() {
486         if (mMenuTask != null) {
487             mMenuTask.cancel();
488             mMenuTask = null;
489         }
490         mMenuExecutor.pause();
491     }
492 
destroy()493     public void destroy() {
494         mMenuExecutor.destroy();
495     }
496 
resume()497     public void resume() {
498         if (mSelectionManager.inSelectionMode()) updateSupportedOperation();
499         mMenuExecutor.resume();
500     }
501 }
502