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