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 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