MenuHelper.java revision 7d2d5ec1e9d6c19278e41c103eacd8090be24406
1/*
2 * Copyright (C) 2008 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.camera;
18
19import com.android.gallery.R;
20
21import android.app.Activity;
22import android.app.AlertDialog;
23import android.content.ActivityNotFoundException;
24import android.content.Context;
25import android.content.DialogInterface;
26import android.content.Intent;
27import android.content.DialogInterface.OnClickListener;
28import android.location.Geocoder;
29import android.media.ExifInterface;
30import android.media.MediaMetadataRetriever;
31import android.net.Uri;
32import android.os.Environment;
33import android.os.Handler;
34import android.os.StatFs;
35import android.preference.PreferenceManager;
36import android.provider.MediaStore;
37import android.provider.MediaStore.Images;
38import android.text.format.Formatter;
39import android.util.Log;
40import android.view.Menu;
41import android.view.MenuItem;
42import android.view.SubMenu;
43import android.view.View;
44import android.widget.ImageView;
45import android.widget.TextView;
46import android.widget.Toast;
47
48import com.android.camera.gallery.IImage;
49
50import java.io.Closeable;
51import java.io.IOException;
52import java.lang.ref.WeakReference;
53import java.text.SimpleDateFormat;
54import java.util.ArrayList;
55import java.util.Date;
56import java.util.List;
57
58/**
59 * A utility class to handle various kinds of menu operations.
60 */
61public class MenuHelper {
62    private static final String TAG = "MenuHelper";
63
64    public static final int INCLUDE_ALL           = 0xFFFFFFFF;
65    public static final int INCLUDE_VIEWPLAY_MENU = (1 << 0);
66    public static final int INCLUDE_SHARE_MENU    = (1 << 1);
67    public static final int INCLUDE_SET_MENU      = (1 << 2);
68    public static final int INCLUDE_CROP_MENU     = (1 << 3);
69    public static final int INCLUDE_DELETE_MENU   = (1 << 4);
70    public static final int INCLUDE_ROTATE_MENU   = (1 << 5);
71    public static final int INCLUDE_DETAILS_MENU  = (1 << 6);
72    public static final int INCLUDE_SHOWMAP_MENU  = (1 << 7);
73
74    public static final int MENU_IMAGE_SHARE = 1;
75    public static final int MENU_IMAGE_SHOWMAP = 2;
76
77    public static final int POSITION_SWITCH_CAMERA_MODE = 1;
78    public static final int POSITION_GOTO_GALLERY = 2;
79    public static final int POSITION_VIEWPLAY = 3;
80    public static final int POSITION_CAPTURE_PICTURE = 4;
81    public static final int POSITION_CAPTURE_VIDEO = 5;
82    public static final int POSITION_IMAGE_SHARE = 6;
83    public static final int POSITION_IMAGE_ROTATE = 7;
84    public static final int POSITION_IMAGE_TOSS = 8;
85    public static final int POSITION_IMAGE_CROP = 9;
86    public static final int POSITION_IMAGE_SET = 10;
87    public static final int POSITION_DETAILS = 11;
88    public static final int POSITION_SHOWMAP = 12;
89    public static final int POSITION_SLIDESHOW = 13;
90    public static final int POSITION_MULTISELECT = 14;
91    public static final int POSITION_CAMERA_SETTING = 15;
92    public static final int POSITION_GALLERY_SETTING = 16;
93
94    public static final int NO_STORAGE_ERROR = -1;
95    public static final int CANNOT_STAT_ERROR = -2;
96    public static final String EMPTY_STRING = "";
97    public static final String JPEG_MIME_TYPE = "image/jpeg";
98    // valid range is -180f to +180f
99    public static final float INVALID_LATLNG = 255f;
100
101    /** Activity result code used to report crop results.
102     */
103    public static final int RESULT_COMMON_MENU_CROP = 490;
104
105    public interface MenuItemsResult {
106        public void gettingReadyToOpen(Menu menu, IImage image);
107        public void aboutToCall(MenuItem item, IImage image);
108    }
109
110    public interface MenuInvoker {
111        public void run(MenuCallback r);
112    }
113
114    public interface MenuCallback {
115        public void run(Uri uri, IImage image);
116    }
117
118    public static void closeSilently(Closeable c) {
119        if (c != null) {
120            try {
121                c.close();
122            } catch (Throwable e) {
123                // ignore
124            }
125        }
126    }
127
128    public static long getImageFileSize(IImage image) {
129        java.io.InputStream data = image.fullSizeImageData();
130        if (data == null) return -1;
131        try {
132            return data.available();
133        } catch (java.io.IOException ex) {
134            return -1;
135        } finally {
136            closeSilently(data);
137        }
138    }
139
140    // This is a hack before we find a solution to pass a permission to other
141    // applications. See bug #1735149, #1836138.
142    // Checks if the URI is on our whitelist:
143    // content://media/... (MediaProvider)
144    // file:///sdcard/... (Browser download)
145    public static boolean isWhiteListUri(Uri uri) {
146        if (uri == null) return false;
147
148        String scheme = uri.getScheme();
149        String authority = uri.getAuthority();
150
151        if (scheme.equals("content") && authority.equals("media")) {
152            return true;
153        }
154
155        if (scheme.equals("file")) {
156            List<String> p = uri.getPathSegments();
157
158            if (p.size() >= 1 && p.get(0).equals("sdcard")) {
159                return true;
160            }
161        }
162
163        return false;
164    }
165
166    public static void enableShareMenuItem(Menu menu, boolean enabled) {
167        MenuItem item = menu.findItem(MENU_IMAGE_SHARE);
168        if (item != null) {
169            item.setVisible(enabled);
170            item.setEnabled(enabled);
171        }
172    }
173
174    public static boolean hasLatLngData(IImage image) {
175        ExifInterface exif = getExif(image);
176        if (exif == null) return false;
177        float latlng[] = new float[2];
178        return exif.getLatLong(latlng);
179    }
180
181    public static void enableShowOnMapMenuItem(Menu menu, boolean enabled) {
182        MenuItem item = menu.findItem(MENU_IMAGE_SHOWMAP);
183        if (item != null) {
184            item.setEnabled(enabled);
185        }
186    }
187
188    private static void setDetailsValue(View d, String text, int valueId) {
189        ((TextView) d.findViewById(valueId)).setText(text);
190    }
191
192    private static void hideDetailsRow(View d, int rowId) {
193        d.findViewById(rowId).setVisibility(View.GONE);
194    }
195
196    private static class UpdateLocationCallback implements
197            ReverseGeocoderTask.Callback {
198        WeakReference<View> mView;
199
200        public UpdateLocationCallback(WeakReference<View> view) {
201            mView = view;
202        }
203
204        public void onComplete(String location) {
205            // View d is per-thread data, so when setDetailsValue is
206            // executed by UI thread, it doesn't matter whether the
207            // details dialog is dismissed or not.
208            View view = mView.get();
209            if (view == null) return;
210            if (!location.equals(MenuHelper.EMPTY_STRING)) {
211                MenuHelper.setDetailsValue(view, location,
212                        R.id.details_location_value);
213            } else {
214                MenuHelper.hideDetailsRow(view, R.id.details_location_row);
215            }
216        }
217    }
218
219    private static void setLatLngDetails(final View d, Activity context,
220            ExifInterface exif) {
221        float[] latlng = new float[2];
222        if (exif.getLatLong(latlng)) {
223            setDetailsValue(d, String.valueOf(latlng[0]),
224                    R.id.details_latitude_value);
225            setDetailsValue(d, String.valueOf(latlng[1]),
226                    R.id.details_longitude_value);
227
228            if (latlng[0] == INVALID_LATLNG || latlng[1] == INVALID_LATLNG) {
229                hideDetailsRow(d, R.id.details_latitude_row);
230                hideDetailsRow(d, R.id.details_longitude_row);
231                hideDetailsRow(d, R.id.details_location_row);
232                return;
233            }
234
235            UpdateLocationCallback cb = new UpdateLocationCallback(
236                    new WeakReference<View>(d));
237            Geocoder geocoder = new Geocoder(context);
238            new ReverseGeocoderTask(geocoder, latlng, cb).execute();
239        } else {
240            hideDetailsRow(d, R.id.details_latitude_row);
241            hideDetailsRow(d, R.id.details_longitude_row);
242            hideDetailsRow(d, R.id.details_location_row);
243        }
244    }
245
246    private static ExifInterface getExif(IImage image) {
247        if (!JPEG_MIME_TYPE.equals(image.getMimeType())) {
248            return null;
249        }
250
251        try {
252            return new ExifInterface(image.getDataPath());
253        } catch (IOException ex) {
254            Log.e(TAG, "cannot read exif", ex);
255            return null;
256        }
257    }
258    // Called when "Show on Maps" is clicked.
259    // Displays image location on Google Maps for further operations.
260    private static boolean onShowMapClicked(MenuInvoker onInvoke,
261                                            final Handler handler,
262                                            final Activity activity) {
263        onInvoke.run(new MenuCallback() {
264            public void run(Uri u, IImage image) {
265                if (image == null) {
266                    return;
267                }
268
269                boolean ok = false;
270                ExifInterface exif = getExif(image);
271                float latlng[] = null;
272                if (exif != null) {
273                    latlng = new float[2];
274                    if (exif.getLatLong(latlng)) {
275                        ok = true;
276                    }
277                }
278
279                if (!ok) {
280                    handler.post(new Runnable() {
281                        public void run() {
282                            Toast.makeText(activity,
283                                    R.string.no_location_image,
284                                    Toast.LENGTH_SHORT).show();
285                        }
286                    });
287                    return;
288                }
289
290                // Can't use geo:latitude,longitude because it only centers
291                // the MapView to specified location, but we need a bubble
292                // for further operations (routing to/from).
293                // The q=(lat, lng) syntax is suggested by geo-team.
294                String uri = "http://maps.google.com/maps?f=q&" +
295                        "q=(" + latlng[0] + "," + latlng[1] + ")";
296                activity.startActivity(new Intent(
297                        android.content.Intent.ACTION_VIEW,
298                        Uri.parse(uri)));
299            }
300        });
301        return true;
302    }
303
304    private static void hideExifInformation(View d) {
305        hideDetailsRow(d, R.id.details_resolution_row);
306        hideDetailsRow(d, R.id.details_make_row);
307        hideDetailsRow(d, R.id.details_model_row);
308        hideDetailsRow(d, R.id.details_whitebalance_row);
309        hideDetailsRow(d, R.id.details_latitude_row);
310        hideDetailsRow(d, R.id.details_longitude_row);
311        hideDetailsRow(d, R.id.details_location_row);
312    }
313
314    private static void showExifInformation(IImage image, View d,
315            Activity activity) {
316        ExifInterface exif = getExif(image);
317        if (exif == null) {
318            hideExifInformation(d);
319            return;
320        }
321
322        String value = exif.getAttribute(ExifInterface.TAG_MAKE);
323        if (value != null) {
324            setDetailsValue(d, value, R.id.details_make_value);
325        } else {
326            hideDetailsRow(d, R.id.details_make_row);
327        }
328
329        value = exif.getAttribute(ExifInterface.TAG_MODEL);
330        if (value != null) {
331            setDetailsValue(d, value, R.id.details_model_value);
332        } else {
333            hideDetailsRow(d, R.id.details_model_row);
334        }
335
336        value = getWhiteBalanceString(exif);
337        if (value != null && !value.equals(EMPTY_STRING)) {
338            setDetailsValue(d, value, R.id.details_whitebalance_value);
339        } else {
340            hideDetailsRow(d, R.id.details_whitebalance_row);
341        }
342
343        setLatLngDetails(d, activity, exif);
344    }
345
346    /**
347     * Returns a human-readable string describing the white balance value. Returns empty
348     * string if there is no white balance value or it is not recognized.
349     */
350    private static String getWhiteBalanceString(ExifInterface exif) {
351        int whitebalance = exif.getAttributeInt(ExifInterface.TAG_WHITE_BALANCE, -1);
352        if (whitebalance == -1) return "";
353
354        switch (whitebalance) {
355            case ExifInterface.WHITEBALANCE_AUTO:
356                return "Auto";
357            case ExifInterface.WHITEBALANCE_MANUAL:
358                return "Manual";
359            default:
360                return "";
361        }
362    }
363
364    // Called when "Details" is clicked.
365    // Displays detailed information about the image/video.
366    private static boolean onDetailsClicked(MenuInvoker onInvoke,
367                                            final Handler handler,
368                                            final Activity activity) {
369        onInvoke.run(new MenuCallback() {
370            public void run(Uri u, IImage image) {
371                if (image == null) {
372                    return;
373                }
374
375                final AlertDialog.Builder builder =
376                        new AlertDialog.Builder(activity);
377
378                final View d = View.inflate(activity, R.layout.detailsview,
379                        null);
380
381                ImageView imageView = (ImageView) d.findViewById(
382                        R.id.details_thumbnail_image);
383                imageView.setImageBitmap(image.miniThumbBitmap());
384
385                TextView textView = (TextView) d.findViewById(
386                        R.id.details_image_title);
387                textView.setText(image.getDisplayName());
388
389                long length = getImageFileSize(image);
390                String lengthString = length < 0
391                        ? EMPTY_STRING
392                        : Formatter.formatFileSize(activity, length);
393                ((TextView) d
394                    .findViewById(R.id.details_file_size_value))
395                    .setText(lengthString);
396
397                int dimensionWidth = 0;
398                int dimensionHeight = 0;
399                if (ImageManager.isImage(image)) {
400                    // getWidth is much slower than reading from EXIF
401                    dimensionWidth = image.getWidth();
402                    dimensionHeight = image.getHeight();
403                    d.findViewById(R.id.details_duration_row)
404                            .setVisibility(View.GONE);
405                    d.findViewById(R.id.details_frame_rate_row)
406                            .setVisibility(View.GONE);
407                    d.findViewById(R.id.details_bit_rate_row)
408                            .setVisibility(View.GONE);
409                    d.findViewById(R.id.details_format_row)
410                            .setVisibility(View.GONE);
411                    d.findViewById(R.id.details_codec_row)
412                            .setVisibility(View.GONE);
413                } else {
414                    MediaMetadataRetriever retriever
415                            = new MediaMetadataRetriever();
416                    try {
417                        retriever.setMode(MediaMetadataRetriever
418                                .MODE_GET_METADATA_ONLY);
419                        retriever.setDataSource(image.getDataPath());
420                        try {
421                            dimensionWidth = Integer.parseInt(
422                                    retriever.extractMetadata(
423                                    MediaMetadataRetriever
424                                    .METADATA_KEY_VIDEO_WIDTH));
425                            dimensionHeight = Integer.parseInt(
426                                    retriever.extractMetadata(
427                                    MediaMetadataRetriever
428                                    .METADATA_KEY_VIDEO_HEIGHT));
429                        } catch (NumberFormatException e) {
430                            dimensionWidth = 0;
431                            dimensionHeight = 0;
432                        }
433
434                        try {
435                            int durationMs = Integer.parseInt(
436                                    retriever.extractMetadata(
437                                    MediaMetadataRetriever
438                                    .METADATA_KEY_DURATION));
439                            String durationValue = formatDuration(
440                                    activity, durationMs);
441                            ((TextView) d.findViewById(
442                                R.id.details_duration_value))
443                                .setText(durationValue);
444                        } catch (NumberFormatException e) {
445                            d.findViewById(
446                                    R.id.details_frame_rate_row)
447                                    .setVisibility(View.GONE);
448                        }
449
450                        try {
451                            String frameRate = String.format(
452                                    activity.getString(R.string.details_fps),
453                                    Integer.parseInt(
454                                            retriever.extractMetadata(
455                                            MediaMetadataRetriever
456                                            .METADATA_KEY_FRAME_RATE)));
457                            ((TextView) d.findViewById(
458                                R.id.details_frame_rate_value))
459                                .setText(frameRate);
460                        } catch (NumberFormatException e) {
461                            d.findViewById(
462                                    R.id.details_frame_rate_row)
463                                    .setVisibility(View.GONE);
464                        }
465
466                        try {
467                            long bitRate = Long.parseLong(
468                                    retriever.extractMetadata(
469                                    MediaMetadataRetriever
470                                    .METADATA_KEY_BIT_RATE));
471                            String bps;
472                            if (bitRate < 1000000) {
473                                bps = String.format(
474                                        activity.getString(
475                                        R.string.details_kbps),
476                                        bitRate / 1000);
477                            } else {
478                                bps = String.format(
479                                        activity.getString(
480                                        R.string.details_mbps),
481                                        (bitRate) / 1000000.0);
482                            }
483                            ((TextView) d.findViewById(
484                                    R.id.details_bit_rate_value))
485                                    .setText(bps);
486                        } catch (NumberFormatException e) {
487                            d.findViewById(R.id.details_bit_rate_row)
488                                    .setVisibility(View.GONE);
489                        }
490
491                        String format = retriever.extractMetadata(
492                                MediaMetadataRetriever
493                                .METADATA_KEY_VIDEO_FORMAT);
494                        ((TextView) d.findViewById(
495                                R.id.details_format_value))
496                                .setText(format);
497
498                        String codec = retriever.extractMetadata(
499                                MediaMetadataRetriever.METADATA_KEY_CODEC);
500                        if (codec != null) {
501                            setDetailsValue(d, codec, R.id.details_codec_value);
502                        } else {
503                            hideDetailsRow(d, R.id.details_codec_row);
504                        }
505
506                    } catch (RuntimeException ex) {
507                        // Assume this is a corrupt video file.
508                    } finally {
509                        try {
510                            retriever.release();
511                        } catch (RuntimeException ex) {
512                            // Ignore failures while cleaning up.
513                        }
514                    }
515                }
516
517                String value = null;
518                if (dimensionWidth > 0 && dimensionHeight > 0) {
519                    value = String.format(
520                            activity.getString(R.string.details_dimension_x),
521                            dimensionWidth, dimensionHeight);
522                }
523
524                if (value != null) {
525                    setDetailsValue(d, value, R.id.details_resolution_value);
526                } else {
527                    hideDetailsRow(d, R.id.details_resolution_row);
528                }
529
530                value = EMPTY_STRING;
531                long dateTaken = image.getDateTaken();
532                if (dateTaken != 0) {
533                    Date date = new Date(image.getDateTaken());
534                    SimpleDateFormat dateFormat = new SimpleDateFormat();
535                    value = dateFormat.format(date);
536                }
537                if (value != EMPTY_STRING) {
538                    setDetailsValue(d, value, R.id.details_date_taken_value);
539                } else {
540                    hideDetailsRow(d, R.id.details_date_taken_row);
541                }
542
543                // Show more EXIF header details for JPEG images.
544                if (JPEG_MIME_TYPE.equals(image.getMimeType())) {
545                    showExifInformation(image, d, activity);
546                } else {
547                    hideExifInformation(d);
548                }
549
550                builder.setNeutralButton(R.string.details_ok,
551                        new DialogInterface.OnClickListener() {
552                            public void onClick(DialogInterface dialog,
553                                    int which) {
554                                dialog.dismiss();
555                            }
556                        });
557
558                handler.post(
559                        new Runnable() {
560                            public void run() {
561                                builder.setIcon(
562                                        android.R.drawable.ic_dialog_info)
563                                        .setTitle(R.string.details_panel_title)
564                                        .setView(d)
565                                        .show();
566                            }
567                        });
568            }
569        });
570        return true;
571    }
572
573    // Called when "Rotate left" or "Rotate right" is clicked.
574    private static boolean onRotateClicked(MenuInvoker onInvoke,
575            final int degree) {
576        onInvoke.run(new MenuCallback() {
577            public void run(Uri u, IImage image) {
578                if (image == null || image.isReadonly()) {
579                    return;
580                }
581                image.rotateImageBy(degree);
582            }
583        });
584        return true;
585    }
586
587    // Called when "Crop" is clicked.
588    private static boolean onCropClicked(MenuInvoker onInvoke,
589                                         final Activity activity) {
590        onInvoke.run(new MenuCallback() {
591            public void run(Uri u, IImage image) {
592                if (u == null) {
593                    return;
594                }
595
596                Intent cropIntent = new Intent(
597                        "com.android.camera.action.CROP");
598                cropIntent.setData(u);
599                activity.startActivityForResult(
600                        cropIntent, RESULT_COMMON_MENU_CROP);
601            }
602        });
603        return true;
604    }
605
606    // Called when "Set as" is clicked.
607    private static boolean onSetAsClicked(MenuInvoker onInvoke,
608                                          final Activity activity) {
609        onInvoke.run(new MenuCallback() {
610            public void run(Uri u, IImage image) {
611                if (u == null || image == null) {
612                    return;
613                }
614
615                Intent intent = Util.createSetAsIntent(image);
616                activity.startActivity(Intent.createChooser(intent,
617                        activity.getText(R.string.setImage)));
618            }
619        });
620        return true;
621    }
622
623    // Called when "Share" is clicked.
624    private static boolean onImageShareClicked(MenuInvoker onInvoke,
625            final Activity activity) {
626        onInvoke.run(new MenuCallback() {
627            public void run(Uri u, IImage image) {
628                if (image == null) return;
629
630                Intent intent = new Intent();
631                intent.setAction(Intent.ACTION_SEND);
632                String mimeType = image.getMimeType();
633                intent.setType(mimeType);
634                intent.putExtra(Intent.EXTRA_STREAM, u);
635                boolean isImage = ImageManager.isImage(image);
636                try {
637                    activity.startActivity(Intent.createChooser(intent,
638                            activity.getText(isImage
639                            ? R.string.sendImage
640                            : R.string.sendVideo)));
641                } catch (android.content.ActivityNotFoundException ex) {
642                    Toast.makeText(activity, isImage
643                            ? R.string.no_way_to_share_image
644                            : R.string.no_way_to_share_video,
645                            Toast.LENGTH_SHORT).show();
646                }
647            }
648        });
649        return true;
650    }
651
652    // Called when "Play" is clicked.
653    private static boolean onViewPlayClicked(MenuInvoker onInvoke,
654            final Activity activity) {
655        onInvoke.run(new MenuCallback() {
656            public void run(Uri uri, IImage image) {
657                if (image != null) {
658                    Intent intent = new Intent(Intent.ACTION_VIEW,
659                            image.fullSizeImageUri());
660                    activity.startActivity(intent);
661                }
662            }});
663        return true;
664    }
665
666    // Called when "Delete" is clicked.
667    private static boolean onDeleteClicked(MenuInvoker onInvoke,
668            final Activity activity, final Runnable onDelete) {
669        onInvoke.run(new MenuCallback() {
670            public void run(Uri uri, IImage image) {
671                if (image != null) {
672                    deleteImage(activity, onDelete, image);
673                }
674            }});
675        return true;
676    }
677
678    static MenuItemsResult addImageMenuItems(
679            Menu menu,
680            int inclusions,
681            final Activity activity,
682            final Handler handler,
683            final Runnable onDelete,
684            final MenuInvoker onInvoke) {
685        final ArrayList<MenuItem> requiresWriteAccessItems =
686                new ArrayList<MenuItem>();
687        final ArrayList<MenuItem> requiresNoDrmAccessItems =
688                new ArrayList<MenuItem>();
689        final ArrayList<MenuItem> requiresImageItems =
690                new ArrayList<MenuItem>();
691        final ArrayList<MenuItem> requiresVideoItems =
692                new ArrayList<MenuItem>();
693
694        if ((inclusions & INCLUDE_ROTATE_MENU) != 0) {
695            SubMenu rotateSubmenu = menu.addSubMenu(Menu.NONE, Menu.NONE,
696                    POSITION_IMAGE_ROTATE, R.string.rotate)
697                    .setIcon(android.R.drawable.ic_menu_rotate);
698            // Don't show the rotate submenu if the item at hand is read only
699            // since the items within the submenu won't be shown anyway. This
700            // is really a framework bug in that it shouldn't show the submenu
701            // if the submenu has no visible items.
702            MenuItem rotateLeft = rotateSubmenu.add(R.string.rotate_left)
703                    .setOnMenuItemClickListener(
704                    new MenuItem.OnMenuItemClickListener() {
705                        public boolean onMenuItemClick(MenuItem item) {
706                            return onRotateClicked(onInvoke, -90);
707                        }
708                    }).setAlphabeticShortcut('l');
709
710            MenuItem rotateRight = rotateSubmenu.add(R.string.rotate_right)
711                    .setOnMenuItemClickListener(
712                    new MenuItem.OnMenuItemClickListener() {
713                        public boolean onMenuItemClick(MenuItem item) {
714                            return onRotateClicked(onInvoke, 90);
715                        }
716                    }).setAlphabeticShortcut('r');
717
718            requiresWriteAccessItems.add(rotateSubmenu.getItem());
719            requiresWriteAccessItems.add(rotateLeft);
720            requiresWriteAccessItems.add(rotateRight);
721
722            requiresImageItems.add(rotateSubmenu.getItem());
723            requiresImageItems.add(rotateLeft);
724            requiresImageItems.add(rotateRight);
725        }
726
727        if ((inclusions & INCLUDE_CROP_MENU) != 0) {
728            MenuItem autoCrop = menu.add(Menu.NONE, Menu.NONE,
729                    POSITION_IMAGE_CROP, R.string.camera_crop);
730            autoCrop.setIcon(android.R.drawable.ic_menu_crop);
731            autoCrop.setOnMenuItemClickListener(
732                    new MenuItem.OnMenuItemClickListener() {
733                        public boolean onMenuItemClick(MenuItem item) {
734                            return onCropClicked(onInvoke, activity);
735                        }
736                    });
737            requiresWriteAccessItems.add(autoCrop);
738            requiresImageItems.add(autoCrop);
739        }
740
741        if ((inclusions & INCLUDE_SET_MENU) != 0) {
742            MenuItem setMenu = menu.add(Menu.NONE, Menu.NONE,
743                    POSITION_IMAGE_SET, R.string.camera_set);
744            setMenu.setIcon(android.R.drawable.ic_menu_set_as);
745            setMenu.setOnMenuItemClickListener(
746                    new MenuItem.OnMenuItemClickListener() {
747                        public boolean onMenuItemClick(MenuItem item) {
748                            return onSetAsClicked(onInvoke, activity);
749                        }
750                    });
751            requiresImageItems.add(setMenu);
752        }
753
754        if ((inclusions & INCLUDE_SHARE_MENU) != 0) {
755            MenuItem item1 = menu.add(Menu.NONE, MENU_IMAGE_SHARE,
756                    POSITION_IMAGE_SHARE, R.string.camera_share)
757                    .setOnMenuItemClickListener(
758                    new MenuItem.OnMenuItemClickListener() {
759                        public boolean onMenuItemClick(MenuItem item) {
760                            return onImageShareClicked(onInvoke, activity);
761                        }
762                    });
763            item1.setIcon(android.R.drawable.ic_menu_share);
764            MenuItem item = item1;
765            requiresNoDrmAccessItems.add(item);
766        }
767
768        if ((inclusions & INCLUDE_DELETE_MENU) != 0) {
769            MenuItem deleteItem = menu.add(Menu.NONE, Menu.NONE,
770                    POSITION_IMAGE_TOSS, R.string.camera_toss);
771            requiresWriteAccessItems.add(deleteItem);
772            deleteItem.setOnMenuItemClickListener(
773                    new MenuItem.OnMenuItemClickListener() {
774                        public boolean onMenuItemClick(MenuItem item) {
775                            return onDeleteClicked(onInvoke, activity,
776                                    onDelete);
777                        }
778                    })
779                    .setAlphabeticShortcut('d')
780                    .setIcon(android.R.drawable.ic_menu_delete);
781        }
782
783        if ((inclusions & INCLUDE_DETAILS_MENU) != 0) {
784            MenuItem detailsMenu = menu.add(Menu.NONE, Menu.NONE,
785                POSITION_DETAILS, R.string.details)
786            .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
787                public boolean onMenuItemClick(MenuItem item) {
788                    return onDetailsClicked(onInvoke, handler, activity);
789                }
790            });
791            detailsMenu.setIcon(R.drawable.ic_menu_view_details);
792        }
793
794        if ((inclusions & INCLUDE_SHOWMAP_MENU) != 0) {
795            MenuItem showOnMapItem = menu.add(Menu.NONE, MENU_IMAGE_SHOWMAP,
796                    POSITION_SHOWMAP, R.string.show_on_map);
797            showOnMapItem.setOnMenuItemClickListener(
798                        new MenuItem.OnMenuItemClickListener() {
799                            public boolean onMenuItemClick(MenuItem item) {
800                                return onShowMapClicked(onInvoke,
801                                        handler, activity);
802                            }
803                        }).setIcon(R.drawable.ic_menu_3d_globe);
804            requiresImageItems.add(showOnMapItem);
805        }
806
807        if ((inclusions & INCLUDE_VIEWPLAY_MENU) != 0) {
808            MenuItem videoPlayItem = menu.add(Menu.NONE, Menu.NONE,
809                POSITION_VIEWPLAY, R.string.video_play)
810                .setOnMenuItemClickListener(
811                new MenuItem.OnMenuItemClickListener() {
812                public boolean onMenuItemClick(MenuItem item) {
813                    return onViewPlayClicked(onInvoke, activity);
814                }
815            });
816            videoPlayItem.setIcon(
817                    com.android.internal.R.drawable.ic_menu_play_clip);
818            requiresVideoItems.add(videoPlayItem);
819        }
820
821        return new MenuItemsResult() {
822            public void gettingReadyToOpen(Menu menu, IImage image) {
823                // protect against null here.  this isn't strictly speaking
824                // required but if a client app isn't handling sdcard removal
825                // properly it could happen
826                if (image == null) {
827                    return;
828                }
829
830                ArrayList<MenuItem> enableList = new ArrayList<MenuItem>();
831                ArrayList<MenuItem> disableList = new ArrayList<MenuItem>();
832                ArrayList<MenuItem> list;
833
834                list = image.isReadonly() ? disableList : enableList;
835                list.addAll(requiresWriteAccessItems);
836
837                list = image.isDrm() ? disableList : enableList;
838                list.addAll(requiresNoDrmAccessItems);
839
840                list = ImageManager.isImage(image) ? enableList : disableList;
841                list.addAll(requiresImageItems);
842
843                list = ImageManager.isVideo(image) ? enableList : disableList;
844                list.addAll(requiresVideoItems);
845
846                for (MenuItem item : enableList) {
847                    item.setVisible(true);
848                    item.setEnabled(true);
849                }
850
851                for (MenuItem item : disableList) {
852                    item.setVisible(false);
853                    item.setEnabled(false);
854                }
855            }
856
857            // must override abstract method
858            public void aboutToCall(MenuItem menu, IImage image) {
859            }
860        };
861    }
862
863    static void deletePhoto(Activity activity, Runnable onDelete) {
864        deleteImpl(activity, onDelete, true);
865    }
866
867    static void deleteImage(
868            Activity activity, Runnable onDelete, IImage image) {
869        deleteImpl(activity, onDelete, ImageManager.isImage(image));
870    }
871
872    static void deleteImpl(
873            Activity activity, Runnable onDelete, boolean isImage) {
874        boolean needConfirm = PreferenceManager
875                 .getDefaultSharedPreferences(activity)
876                 .getBoolean("pref_gallery_confirm_delete_key", true);
877        if (!needConfirm) {
878            if (onDelete != null) onDelete.run();
879        } else {
880            String title = activity.getString(R.string.confirm_delete_title);
881            String message = activity.getString(isImage
882                    ? R.string.confirm_delete_message
883                    : R.string.confirm_delete_video_message);
884            confirmAction(activity, title, message, onDelete);
885        }
886    }
887
888    public static void deleteMultiple(Context context, Runnable action) {
889        boolean needConfirm = PreferenceManager
890            .getDefaultSharedPreferences(context)
891            .getBoolean("pref_gallery_confirm_delete_key", true);
892        if (!needConfirm) {
893            if (action != null) action.run();
894        } else {
895            String title = context.getString(R.string.confirm_delete_title);
896            String message = context.getString(
897                    R.string.confirm_delete_multiple_message);
898            confirmAction(context, title, message, action);
899        }
900    }
901
902    public static void confirmAction(Context context, String title,
903            String message, final Runnable action) {
904        OnClickListener listener = new OnClickListener() {
905            public void onClick(DialogInterface dialog, int which) {
906                switch (which) {
907                    case DialogInterface.BUTTON_POSITIVE:
908                        if (action != null) action.run();
909                }
910            }
911        };
912        new AlertDialog.Builder(context)
913            .setIcon(android.R.drawable.ic_dialog_alert)
914            .setTitle(title)
915            .setMessage(message)
916            .setPositiveButton(android.R.string.ok, listener)
917            .setNegativeButton(android.R.string.cancel, listener)
918            .create()
919            .show();
920    }
921
922    static void addCapturePictureMenuItems(Menu menu, final Activity activity) {
923        menu.add(Menu.NONE, Menu.NONE, POSITION_CAPTURE_PICTURE,
924                R.string.capture_picture)
925                .setOnMenuItemClickListener(
926                new MenuItem.OnMenuItemClickListener() {
927                    public boolean onMenuItemClick(MenuItem item) {
928                        return onCapturePictureClicked(activity);
929                    }
930                }).setIcon(android.R.drawable.ic_menu_camera);
931    }
932
933    private static boolean onCapturePictureClicked(Activity activity) {
934        Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
935        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
936        try {
937            activity.startActivity(intent);
938        } catch (android.content.ActivityNotFoundException e) {
939            // Ignore exception
940        }
941        return true;
942    }
943
944    static void addCaptureVideoMenuItems(Menu menu, final Activity activity) {
945        menu.add(Menu.NONE, Menu.NONE, POSITION_CAPTURE_VIDEO,
946                R.string.capture_video)
947                .setOnMenuItemClickListener(
948                new MenuItem.OnMenuItemClickListener() {
949                    public boolean onMenuItemClick(MenuItem item) {
950                        return onCaptureVideoClicked(activity);
951                    }
952                }).setIcon(R.drawable.ic_menu_camera_video_view);
953    }
954
955    private static boolean onCaptureVideoClicked(Activity activity) {
956        Intent intent = new Intent(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
957        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
958        try {
959            activity.startActivity(intent);
960        } catch (android.content.ActivityNotFoundException e) {
961            // Ignore exception
962        }
963        return true;
964    }
965
966    public static void addCaptureMenuItems(Menu menu, final Activity activity) {
967        addCapturePictureMenuItems(menu, activity);
968        addCaptureVideoMenuItems(menu, activity);
969    }
970
971    public static String formatDuration(final Context context,
972            int durationMs) {
973        int duration = durationMs / 1000;
974        int h = duration / 3600;
975        int m = (duration - h * 3600) / 60;
976        int s = duration - (h * 3600 + m * 60);
977        String durationValue;
978        if (h == 0) {
979            durationValue = String.format(
980                    context.getString(R.string.details_ms), m, s);
981        } else {
982            durationValue = String.format(
983                    context.getString(R.string.details_hms), h, m, s);
984        }
985        return durationValue;
986    }
987
988    public static void showStorageToast(Activity activity) {
989        showStorageToast(activity, calculatePicturesRemaining());
990    }
991
992    public static void showStorageToast(Activity activity, int remaining) {
993        String noStorageText = null;
994
995        if (remaining == MenuHelper.NO_STORAGE_ERROR) {
996            String state = Environment.getExternalStorageState();
997            if (state == Environment.MEDIA_CHECKING) {
998                noStorageText = activity.getString(R.string.preparing_sd);
999            } else {
1000                noStorageText = activity.getString(R.string.no_storage);
1001            }
1002        } else if (remaining < 1) {
1003            noStorageText = activity.getString(R.string.not_enough_space);
1004        }
1005
1006        if (noStorageText != null) {
1007            Toast.makeText(activity, noStorageText, 5000).show();
1008        }
1009    }
1010
1011    public static int calculatePicturesRemaining() {
1012        try {
1013            if (!ImageManager.hasStorage()) {
1014                return NO_STORAGE_ERROR;
1015            } else {
1016                String storageDirectory =
1017                        Environment.getExternalStorageDirectory().toString();
1018                StatFs stat = new StatFs(storageDirectory);
1019                float remaining = ((float) stat.getAvailableBlocks()
1020                        * (float) stat.getBlockSize()) / 400000F;
1021                return (int) remaining;
1022            }
1023        } catch (Exception ex) {
1024            // if we can't stat the filesystem then we don't know how many
1025            // pictures are remaining.  it might be zero but just leave it
1026            // blank since we really don't know.
1027            return CANNOT_STAT_ERROR;
1028        }
1029    }
1030}
1031