ActionModeHandler.java revision 4b4dbd225685502f4249c2bf25bf74f7ce526645
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.gallery3d.ui;
18
19import android.annotation.TargetApi;
20import android.app.Activity;
21import android.content.Intent;
22import android.net.Uri;
23import android.nfc.NfcAdapter;
24import android.os.Handler;
25import android.view.ActionMode;
26import android.view.ActionMode.Callback;
27import android.view.LayoutInflater;
28import android.view.Menu;
29import android.view.MenuItem;
30import android.view.View;
31import android.widget.Button;
32import android.widget.ShareActionProvider;
33import android.widget.ShareActionProvider.OnShareTargetSelectedListener;
34
35import com.android.gallery3d.R;
36import com.android.gallery3d.app.AbstractGalleryActivity;
37import com.android.gallery3d.common.ApiHelper;
38import com.android.gallery3d.common.Utils;
39import com.android.gallery3d.data.DataManager;
40import com.android.gallery3d.data.MediaObject;
41import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
42import com.android.gallery3d.data.Path;
43import com.android.gallery3d.ui.MenuExecutor.ProgressListener;
44import com.android.gallery3d.util.Future;
45import com.android.gallery3d.util.GalleryUtils;
46import com.android.gallery3d.util.ThreadPool.Job;
47import com.android.gallery3d.util.ThreadPool.JobContext;
48
49import java.util.ArrayList;
50
51public class ActionModeHandler implements Callback, PopupList.OnPopupItemClickListener {
52
53    @SuppressWarnings("unused")
54    private static final String TAG = "ActionModeHandler";
55
56    private static final int SUPPORT_MULTIPLE_MASK = MediaObject.SUPPORT_DELETE
57            | MediaObject.SUPPORT_ROTATE | MediaObject.SUPPORT_SHARE
58            | MediaObject.SUPPORT_CACHE | MediaObject.SUPPORT_IMPORT;
59
60    public interface ActionModeListener {
61        public boolean onActionItemClicked(MenuItem item);
62    }
63
64    private final AbstractGalleryActivity mActivity;
65    private final MenuExecutor mMenuExecutor;
66    private final SelectionManager mSelectionManager;
67    private final NfcAdapter mNfcAdapter;
68    private Menu mMenu;
69    private MenuItem mSharePanoramaMenuItem;
70    private MenuItem mShareMenuItem;
71    private ShareActionProvider mSharePanoramaActionProvider;
72    private ShareActionProvider mShareActionProvider;
73    private SelectionMenu mSelectionMenu;
74    private ActionModeListener mListener;
75    private Future<?> mMenuTask;
76    private final Handler mMainHandler;
77    private ActionMode mActionMode;
78
79    private static class GetAllPanoramaSupports implements PanoramaSupportCallback {
80        private int mNumInfoRequired;
81        private JobContext mJobContext;
82        public boolean mAllPanoramas = true;
83        public boolean mAllPanorama360 = true;
84        public boolean mHasPanorama360 = false;
85        private Object mLock = new Object();
86
87        public GetAllPanoramaSupports(ArrayList<MediaObject> mediaObjects, JobContext jc) {
88            mJobContext = jc;
89            mNumInfoRequired = mediaObjects.size();
90            for (MediaObject mediaObject : mediaObjects) {
91                mediaObject.getPanoramaSupport(this);
92            }
93        }
94
95        @Override
96        public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
97                boolean isPanorama360) {
98            synchronized (mLock) {
99                mNumInfoRequired--;
100                mAllPanoramas = isPanorama && mAllPanoramas;
101                mAllPanorama360 = isPanorama360 && mAllPanorama360;
102                mHasPanorama360 = mHasPanorama360 || isPanorama360;
103                if (mNumInfoRequired == 0 || mJobContext.isCancelled()) {
104                    mLock.notifyAll();
105                }
106            }
107        }
108
109        public void waitForPanoramaSupport() {
110            synchronized (mLock) {
111                while (mNumInfoRequired != 0 && !mJobContext.isCancelled()) {
112                    try {
113                        mLock.wait();
114                    } catch (InterruptedException e) {
115                        // May be a cancelled job context
116                    }
117                }
118            }
119        }
120    }
121
122    public ActionModeHandler(
123            AbstractGalleryActivity activity, SelectionManager selectionManager) {
124        mActivity = Utils.checkNotNull(activity);
125        mSelectionManager = Utils.checkNotNull(selectionManager);
126        mMenuExecutor = new MenuExecutor(activity, selectionManager);
127        mMainHandler = new Handler(activity.getMainLooper());
128        mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext());
129    }
130
131    public void startActionMode() {
132        Activity a = mActivity;
133        mActionMode = a.startActionMode(this);
134        View customView = LayoutInflater.from(a).inflate(
135                R.layout.action_mode, null);
136        mActionMode.setCustomView(customView);
137        mSelectionMenu = new SelectionMenu(a,
138                (Button) customView.findViewById(R.id.selection_menu), this);
139        updateSelectionMenu();
140    }
141
142    public void finishActionMode() {
143        mActionMode.finish();
144    }
145
146    public void setTitle(String title) {
147        mSelectionMenu.setTitle(title);
148    }
149
150    public void setActionModeListener(ActionModeListener listener) {
151        mListener = listener;
152    }
153
154    @Override
155    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
156        GLRoot root = mActivity.getGLRoot();
157        root.lockRenderThread();
158        try {
159            boolean result;
160            // Give listener a chance to process this command before it's routed to
161            // ActionModeHandler, which handles command only based on the action id.
162            // Sometimes the listener may have more background information to handle
163            // an action command.
164            if (mListener != null) {
165                result = mListener.onActionItemClicked(item);
166                if (result) {
167                    mSelectionManager.leaveSelectionMode();
168                    return result;
169                }
170            }
171            ProgressListener listener = null;
172            String confirmMsg = null;
173            int action = item.getItemId();
174            if (action == R.id.action_import) {
175                listener = new ImportCompleteListener(mActivity);
176            } else if (item.getItemId() == R.id.action_delete) {
177                confirmMsg = mActivity.getResources().getQuantityString(
178                        R.plurals.delete_selection, mSelectionManager.getSelectedCount());
179            }
180            mMenuExecutor.onMenuClicked(item, confirmMsg, listener);
181        } finally {
182            root.unlockRenderThread();
183        }
184        return true;
185    }
186
187    @Override
188    public boolean onPopupItemClick(int itemId) {
189        GLRoot root = mActivity.getGLRoot();
190        root.lockRenderThread();
191        try {
192            if (itemId == R.id.action_select_all) {
193                updateSupportedOperation();
194                mMenuExecutor.onMenuClicked(itemId, null, false, true);
195            }
196            return true;
197        } finally {
198            root.unlockRenderThread();
199        }
200    }
201
202    private void updateSelectionMenu() {
203        // update title
204        int count = mSelectionManager.getSelectedCount();
205        String format = mActivity.getResources().getQuantityString(
206                R.plurals.number_of_items_selected, count);
207        setTitle(String.format(format, count));
208
209        // For clients who call SelectionManager.selectAll() directly, we need to ensure the
210        // menu status is consistent with selection manager.
211        mSelectionMenu.updateSelectAllMode(mSelectionManager.inSelectAllMode());
212    }
213
214    private final OnShareTargetSelectedListener mShareTargetSelectedListener =
215            new OnShareTargetSelectedListener() {
216        @Override
217        public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) {
218            mSelectionManager.leaveSelectionMode();
219            return false;
220        }
221    };
222
223    @Override
224    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
225        return false;
226    }
227
228    @Override
229    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
230        mode.getMenuInflater().inflate(R.menu.operation, menu);
231
232        mMenu = menu;
233        mSharePanoramaMenuItem = menu.findItem(R.id.action_share_panorama);
234        if (mSharePanoramaMenuItem != null) {
235            mSharePanoramaActionProvider = (ShareActionProvider) mSharePanoramaMenuItem
236                .getActionProvider();
237            mSharePanoramaActionProvider.setOnShareTargetSelectedListener(
238                    mShareTargetSelectedListener);
239            mSharePanoramaActionProvider.setShareHistoryFileName("panorama_share_history.xml");
240        }
241        mShareMenuItem = menu.findItem(R.id.action_share);
242        if (mShareMenuItem != null) {
243            mShareActionProvider = (ShareActionProvider) mShareMenuItem
244                .getActionProvider();
245            mShareActionProvider.setOnShareTargetSelectedListener(
246                    mShareTargetSelectedListener);
247            mShareActionProvider.setShareHistoryFileName("share_history.xml");
248        }
249        return true;
250    }
251
252    @Override
253    public void onDestroyActionMode(ActionMode mode) {
254        mSelectionManager.leaveSelectionMode();
255    }
256
257    private ArrayList<MediaObject> getSelectedMediaObjects(JobContext jc) {
258        ArrayList<Path> unexpandedPaths = mSelectionManager.getSelected(false);
259        if (unexpandedPaths.isEmpty()) {
260            // This happens when starting selection mode from overflow menu
261            // (instead of long press a media object)
262            return null;
263        }
264        ArrayList<MediaObject> selected = new ArrayList<MediaObject>();
265        DataManager manager = mActivity.getDataManager();
266        for (Path path : unexpandedPaths) {
267            if (jc.isCancelled()) {
268                return null;
269            }
270            selected.add(manager.getMediaObject(path));
271        }
272
273        return selected;
274    }
275    // Menu options are determined by selection set itself.
276    // We cannot expand it because MenuExecuter executes it based on
277    // the selection set instead of the expanded result.
278    // e.g. LocalImage can be rotated but collections of them (LocalAlbum) can't.
279    private int computeMenuOptions(ArrayList<MediaObject> selected) {
280        int operation = MediaObject.SUPPORT_ALL;
281        int type = 0;
282        for (MediaObject mediaObject: selected) {
283            int support = mediaObject.getSupportedOperations();
284            type |= mediaObject.getMediaType();
285            operation &= support;
286        }
287
288        switch (selected.size()) {
289            case 1:
290                final String mimeType = MenuExecutor.getMimeType(type);
291                if (!GalleryUtils.isEditorAvailable(mActivity, mimeType)) {
292                    operation &= ~MediaObject.SUPPORT_EDIT;
293                }
294                break;
295            default:
296                operation &= SUPPORT_MULTIPLE_MASK;
297        }
298
299        return operation;
300    }
301
302    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
303    private void setNfcBeamPushUris(Uri[] uris) {
304        if (mNfcAdapter != null && ApiHelper.HAS_SET_BEAM_PUSH_URIS) {
305            mNfcAdapter.setBeamPushUrisCallback(null, mActivity);
306            mNfcAdapter.setBeamPushUris(uris, mActivity);
307        }
308    }
309
310    // Share intent needs to expand the selection set so we can get URI of
311    // each media item
312    private Intent computePanoramaSharingIntent(JobContext jc) {
313        ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true);
314        if (expandedPaths.size() == 0) {
315            return null;
316        }
317        final ArrayList<Uri> uris = new ArrayList<Uri>();
318        DataManager manager = mActivity.getDataManager();
319        final Intent intent = new Intent();
320        for (Path path : expandedPaths) {
321            if (jc.isCancelled()) return null;
322            uris.add(manager.getContentUri(path));
323        }
324
325        final int size = uris.size();
326        if (size > 0) {
327            if (size > 1) {
328                intent.setAction(Intent.ACTION_SEND_MULTIPLE);
329                intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360);
330                intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
331            } else {
332                intent.setAction(Intent.ACTION_SEND);
333                intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360);
334                intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
335            }
336            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
337        }
338
339        return intent;
340    }
341
342    private Intent computeSharingIntent(JobContext jc) {
343        ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true);
344        if (expandedPaths.size() == 0) {
345            setNfcBeamPushUris(null);
346            return null;
347        }
348        final ArrayList<Uri> uris = new ArrayList<Uri>();
349        DataManager manager = mActivity.getDataManager();
350        int type = 0;
351        final Intent intent = new Intent();
352        for (Path path : expandedPaths) {
353            if (jc.isCancelled()) return null;
354            int support = manager.getSupportedOperations(path);
355            type |= manager.getMediaType(path);
356
357            if ((support & MediaObject.SUPPORT_SHARE) != 0) {
358                uris.add(manager.getContentUri(path));
359            }
360        }
361
362        final int size = uris.size();
363        if (size > 0) {
364            final String mimeType = MenuExecutor.getMimeType(type);
365            if (size > 1) {
366                intent.setAction(Intent.ACTION_SEND_MULTIPLE).setType(mimeType);
367                intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
368            } else {
369                intent.setAction(Intent.ACTION_SEND).setType(mimeType);
370                intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
371            }
372            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
373            setNfcBeamPushUris(uris.toArray(new Uri[uris.size()]));
374        } else {
375            setNfcBeamPushUris(null);
376        }
377
378        return intent;
379    }
380
381    public void updateSupportedOperation(Path path, boolean selected) {
382        // TODO: We need to improve the performance
383        updateSupportedOperation();
384    }
385
386    public void updateSupportedOperation() {
387        // Interrupt previous unfinished task, mMenuTask is only accessed in main thread
388        if (mMenuTask != null) mMenuTask.cancel();
389
390        updateSelectionMenu();
391
392        // Disable share actions until share intent is in good shape
393        if (mSharePanoramaMenuItem != null) mSharePanoramaMenuItem.setEnabled(false);
394        if (mShareMenuItem != null) mShareMenuItem.setEnabled(false);
395
396        // Generate sharing intent and update supported operations in the background
397        // The task can take a long time and be canceled in the mean time.
398        mMenuTask = mActivity.getThreadPool().submit(new Job<Void>() {
399            @Override
400            public Void run(final JobContext jc) {
401                // Pass1: Deal with unexpanded media object list for menu operation.
402                ArrayList<MediaObject> selected = getSelectedMediaObjects(jc);
403                if (selected == null) {
404                    return null;
405                }
406                final int operation = computeMenuOptions(selected);
407                if (jc.isCancelled()) {
408                    return null;
409                }
410                final GetAllPanoramaSupports supportCallback = new GetAllPanoramaSupports(selected,
411                        jc);
412
413                // Pass2: Deal with expanded media object list for sharing operation.
414                final Intent share_panorama_intent = computePanoramaSharingIntent(jc);
415                final Intent share_intent = computeSharingIntent(jc);
416
417                supportCallback.waitForPanoramaSupport();
418                if (jc.isCancelled()) {
419                    return null;
420                }
421                mMainHandler.post(new Runnable() {
422                    @Override
423                    public void run() {
424                        mMenuTask = null;
425                        if (jc.isCancelled()) return;
426                        MenuExecutor.updateMenuOperation(mMenu, operation);
427                        MenuExecutor.updateMenuForPanorama(mMenu, supportCallback.mAllPanorama360,
428                                supportCallback.mHasPanorama360);
429                        if (mSharePanoramaMenuItem != null) {
430                            mSharePanoramaMenuItem.setEnabled(true);
431                            if (supportCallback.mAllPanorama360) {
432                                mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
433                                mShareMenuItem.setTitle(
434                                    mActivity.getResources().getString(R.string.share_as_photo));
435                            } else {
436                                mSharePanoramaMenuItem.setVisible(false);
437                                mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
438                                mShareMenuItem.setTitle(
439                                    mActivity.getResources().getString(R.string.share));
440                            }
441                            mSharePanoramaActionProvider.setShareIntent(share_panorama_intent);
442                        }
443                        if (mShareMenuItem != null) {
444                            mShareMenuItem.setEnabled(true);
445                            mShareActionProvider.setShareIntent(share_intent);
446                        }
447                    }
448                });
449                return null;
450            }
451        });
452    }
453
454    public void pause() {
455        if (mMenuTask != null) {
456            mMenuTask.cancel();
457            mMenuTask = null;
458        }
459        mMenuExecutor.pause();
460    }
461
462    public void resume() {
463        if (mSelectionManager.inSelectionMode()) updateSupportedOperation();
464    }
465}
466