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.getTitle());
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                d.findViewById(R.id.details_frame_rate_row)
398                            .setVisibility(View.GONE);
399                d.findViewById(R.id.details_bit_rate_row)
400                            .setVisibility(View.GONE);
401                d.findViewById(R.id.details_format_row)
402                            .setVisibility(View.GONE);
403                d.findViewById(R.id.details_codec_row)
404                            .setVisibility(View.GONE);
405
406                int dimensionWidth = 0;
407                int dimensionHeight = 0;
408                if (ImageManager.isImage(image)) {
409                    // getWidth is much slower than reading from EXIF
410                    dimensionWidth = image.getWidth();
411                    dimensionHeight = image.getHeight();
412                    d.findViewById(R.id.details_duration_row)
413                            .setVisibility(View.GONE);
414                }
415
416                String value = null;
417                if (dimensionWidth > 0 && dimensionHeight > 0) {
418                    value = String.format(
419                            activity.getString(R.string.details_dimension_x),
420                            dimensionWidth, dimensionHeight);
421                }
422
423                if (value != null) {
424                    setDetailsValue(d, value, R.id.details_resolution_value);
425                } else {
426                    hideDetailsRow(d, R.id.details_resolution_row);
427                }
428
429                value = EMPTY_STRING;
430                long dateTaken = image.getDateTaken();
431                if (dateTaken != 0) {
432                    Date date = new Date(image.getDateTaken());
433                    SimpleDateFormat dateFormat = new SimpleDateFormat();
434                    value = dateFormat.format(date);
435                }
436                if (value != EMPTY_STRING) {
437                    setDetailsValue(d, value, R.id.details_date_taken_value);
438                } else {
439                    hideDetailsRow(d, R.id.details_date_taken_row);
440                }
441
442                // Show more EXIF header details for JPEG images.
443                if (JPEG_MIME_TYPE.equals(image.getMimeType())) {
444                    showExifInformation(image, d, activity);
445                } else {
446                    hideExifInformation(d);
447                }
448
449                builder.setNeutralButton(R.string.details_ok,
450                        new DialogInterface.OnClickListener() {
451                            public void onClick(DialogInterface dialog,
452                                    int which) {
453                                dialog.dismiss();
454                            }
455                        });
456
457                handler.post(
458                        new Runnable() {
459                            public void run() {
460                                builder.setIcon(
461                                        android.R.drawable.ic_dialog_info)
462                                        .setTitle(R.string.details_panel_title)
463                                        .setView(d)
464                                        .show();
465                            }
466                        });
467            }
468        });
469        return true;
470    }
471
472    // Called when "Rotate left" or "Rotate right" is clicked.
473    private static boolean onRotateClicked(MenuInvoker onInvoke,
474            final int degree) {
475        onInvoke.run(new MenuCallback() {
476            public void run(Uri u, IImage image) {
477                if (image == null || image.isReadonly()) {
478                    return;
479                }
480                image.rotateImageBy(degree);
481            }
482        });
483        return true;
484    }
485
486    // Called when "Crop" is clicked.
487    private static boolean onCropClicked(MenuInvoker onInvoke,
488                                         final Activity activity) {
489        onInvoke.run(new MenuCallback() {
490            public void run(Uri u, IImage image) {
491                if (u == null) {
492                    return;
493                }
494
495                Intent cropIntent = new Intent(
496                        "com.android.camera.action.CROP");
497                cropIntent.setData(u);
498                activity.startActivityForResult(
499                        cropIntent, RESULT_COMMON_MENU_CROP);
500            }
501        });
502        return true;
503    }
504
505    // Called when "Set as" is clicked.
506    private static boolean onSetAsClicked(MenuInvoker onInvoke,
507                                          final Activity activity) {
508        onInvoke.run(new MenuCallback() {
509            public void run(Uri u, IImage image) {
510                if (u == null || image == null) {
511                    return;
512                }
513
514                Intent intent = Util.createSetAsIntent(image);
515                activity.startActivity(Intent.createChooser(intent,
516                        activity.getText(R.string.setImage)));
517            }
518        });
519        return true;
520    }
521
522    // Called when "Share" is clicked.
523    private static boolean onImageShareClicked(MenuInvoker onInvoke,
524            final Activity activity) {
525        onInvoke.run(new MenuCallback() {
526            public void run(Uri u, IImage image) {
527                if (image == null) return;
528
529                Intent intent = new Intent();
530                intent.setAction(Intent.ACTION_SEND);
531                String mimeType = image.getMimeType();
532                intent.setType(mimeType);
533                intent.putExtra(Intent.EXTRA_STREAM, u);
534                boolean isImage = ImageManager.isImage(image);
535                try {
536                    activity.startActivity(Intent.createChooser(intent,
537                            activity.getText(isImage
538                            ? R.string.sendImage
539                            : R.string.sendVideo)));
540                } catch (android.content.ActivityNotFoundException ex) {
541                    Toast.makeText(activity, isImage
542                            ? R.string.no_way_to_share_image
543                            : R.string.no_way_to_share_video,
544                            Toast.LENGTH_SHORT).show();
545                }
546            }
547        });
548        return true;
549    }
550
551    // Called when "Play" is clicked.
552    private static boolean onViewPlayClicked(MenuInvoker onInvoke,
553            final Activity activity) {
554        onInvoke.run(new MenuCallback() {
555            public void run(Uri uri, IImage image) {
556                if (image != null) {
557                    Intent intent = new Intent(Intent.ACTION_VIEW,
558                            image.fullSizeImageUri());
559                    activity.startActivity(intent);
560                }
561            }});
562        return true;
563    }
564
565    // Called when "Delete" is clicked.
566    private static boolean onDeleteClicked(MenuInvoker onInvoke,
567            final Activity activity, final Runnable onDelete) {
568        onInvoke.run(new MenuCallback() {
569            public void run(Uri uri, IImage image) {
570                if (image != null) {
571                    deleteImage(activity, onDelete, image);
572                }
573            }});
574        return true;
575    }
576
577    static MenuItemsResult addImageMenuItems(
578            Menu menu,
579            int inclusions,
580            final Activity activity,
581            final Handler handler,
582            final Runnable onDelete,
583            final MenuInvoker onInvoke) {
584        final ArrayList<MenuItem> requiresWriteAccessItems =
585                new ArrayList<MenuItem>();
586        final ArrayList<MenuItem> requiresNoDrmAccessItems =
587                new ArrayList<MenuItem>();
588        final ArrayList<MenuItem> requiresImageItems =
589                new ArrayList<MenuItem>();
590        final ArrayList<MenuItem> requiresVideoItems =
591                new ArrayList<MenuItem>();
592
593        if ((inclusions & INCLUDE_ROTATE_MENU) != 0) {
594            SubMenu rotateSubmenu = menu.addSubMenu(Menu.NONE, Menu.NONE,
595                    POSITION_IMAGE_ROTATE, R.string.rotate)
596                    .setIcon(android.R.drawable.ic_menu_rotate);
597            // Don't show the rotate submenu if the item at hand is read only
598            // since the items within the submenu won't be shown anyway. This
599            // is really a framework bug in that it shouldn't show the submenu
600            // if the submenu has no visible items.
601            MenuItem rotateLeft = rotateSubmenu.add(R.string.rotate_left)
602                    .setOnMenuItemClickListener(
603                    new MenuItem.OnMenuItemClickListener() {
604                        public boolean onMenuItemClick(MenuItem item) {
605                            return onRotateClicked(onInvoke, -90);
606                        }
607                    }).setAlphabeticShortcut('l');
608
609            MenuItem rotateRight = rotateSubmenu.add(R.string.rotate_right)
610                    .setOnMenuItemClickListener(
611                    new MenuItem.OnMenuItemClickListener() {
612                        public boolean onMenuItemClick(MenuItem item) {
613                            return onRotateClicked(onInvoke, 90);
614                        }
615                    }).setAlphabeticShortcut('r');
616
617            requiresWriteAccessItems.add(rotateSubmenu.getItem());
618            requiresWriteAccessItems.add(rotateLeft);
619            requiresWriteAccessItems.add(rotateRight);
620
621            requiresImageItems.add(rotateSubmenu.getItem());
622            requiresImageItems.add(rotateLeft);
623            requiresImageItems.add(rotateRight);
624        }
625
626        if ((inclusions & INCLUDE_CROP_MENU) != 0) {
627            MenuItem autoCrop = menu.add(Menu.NONE, Menu.NONE,
628                    POSITION_IMAGE_CROP, R.string.camera_crop);
629            autoCrop.setIcon(android.R.drawable.ic_menu_crop);
630            autoCrop.setOnMenuItemClickListener(
631                    new MenuItem.OnMenuItemClickListener() {
632                        public boolean onMenuItemClick(MenuItem item) {
633                            return onCropClicked(onInvoke, activity);
634                        }
635                    });
636            requiresWriteAccessItems.add(autoCrop);
637            requiresImageItems.add(autoCrop);
638        }
639
640        if ((inclusions & INCLUDE_SET_MENU) != 0) {
641            MenuItem setMenu = menu.add(Menu.NONE, Menu.NONE,
642                    POSITION_IMAGE_SET, R.string.camera_set);
643            setMenu.setIcon(android.R.drawable.ic_menu_set_as);
644            setMenu.setOnMenuItemClickListener(
645                    new MenuItem.OnMenuItemClickListener() {
646                        public boolean onMenuItemClick(MenuItem item) {
647                            return onSetAsClicked(onInvoke, activity);
648                        }
649                    });
650            requiresImageItems.add(setMenu);
651        }
652
653        if ((inclusions & INCLUDE_SHARE_MENU) != 0) {
654            MenuItem item1 = menu.add(Menu.NONE, MENU_IMAGE_SHARE,
655                    POSITION_IMAGE_SHARE, R.string.camera_share)
656                    .setOnMenuItemClickListener(
657                    new MenuItem.OnMenuItemClickListener() {
658                        public boolean onMenuItemClick(MenuItem item) {
659                            return onImageShareClicked(onInvoke, activity);
660                        }
661                    });
662            item1.setIcon(android.R.drawable.ic_menu_share);
663            MenuItem item = item1;
664            requiresNoDrmAccessItems.add(item);
665        }
666
667        if ((inclusions & INCLUDE_DELETE_MENU) != 0) {
668            MenuItem deleteItem = menu.add(Menu.NONE, Menu.NONE,
669                    POSITION_IMAGE_TOSS, R.string.camera_toss);
670            requiresWriteAccessItems.add(deleteItem);
671            deleteItem.setOnMenuItemClickListener(
672                    new MenuItem.OnMenuItemClickListener() {
673                        public boolean onMenuItemClick(MenuItem item) {
674                            return onDeleteClicked(onInvoke, activity,
675                                    onDelete);
676                        }
677                    })
678                    .setAlphabeticShortcut('d')
679                    .setIcon(android.R.drawable.ic_menu_delete);
680        }
681
682        if ((inclusions & INCLUDE_DETAILS_MENU) != 0) {
683            MenuItem detailsMenu = menu.add(Menu.NONE, Menu.NONE,
684                POSITION_DETAILS, R.string.details)
685            .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
686                public boolean onMenuItemClick(MenuItem item) {
687                    return onDetailsClicked(onInvoke, handler, activity);
688                }
689            });
690            detailsMenu.setIcon(R.drawable.ic_menu_view_details);
691        }
692
693        if ((inclusions & INCLUDE_SHOWMAP_MENU) != 0) {
694            MenuItem showOnMapItem = menu.add(Menu.NONE, MENU_IMAGE_SHOWMAP,
695                    POSITION_SHOWMAP, R.string.show_on_map);
696            showOnMapItem.setOnMenuItemClickListener(
697                        new MenuItem.OnMenuItemClickListener() {
698                            public boolean onMenuItemClick(MenuItem item) {
699                                return onShowMapClicked(onInvoke,
700                                        handler, activity);
701                            }
702                        }).setIcon(R.drawable.ic_menu_3d_globe);
703            requiresImageItems.add(showOnMapItem);
704        }
705
706        if ((inclusions & INCLUDE_VIEWPLAY_MENU) != 0) {
707            MenuItem videoPlayItem = menu.add(Menu.NONE, Menu.NONE,
708                POSITION_VIEWPLAY, R.string.video_play)
709                .setOnMenuItemClickListener(
710                new MenuItem.OnMenuItemClickListener() {
711                public boolean onMenuItemClick(MenuItem item) {
712                    return onViewPlayClicked(onInvoke, activity);
713                }
714            });
715            videoPlayItem.setIcon(
716                    com.android.internal.R.drawable.ic_menu_play_clip);
717            requiresVideoItems.add(videoPlayItem);
718        }
719
720        return new MenuItemsResult() {
721            public void gettingReadyToOpen(Menu menu, IImage image) {
722                // protect against null here.  this isn't strictly speaking
723                // required but if a client app isn't handling sdcard removal
724                // properly it could happen
725                if (image == null) {
726                    return;
727                }
728
729                ArrayList<MenuItem> enableList = new ArrayList<MenuItem>();
730                ArrayList<MenuItem> disableList = new ArrayList<MenuItem>();
731                ArrayList<MenuItem> list;
732
733                list = image.isReadonly() ? disableList : enableList;
734                list.addAll(requiresWriteAccessItems);
735
736                list = image.isDrm() ? disableList : enableList;
737                list.addAll(requiresNoDrmAccessItems);
738
739                list = ImageManager.isImage(image) ? enableList : disableList;
740                list.addAll(requiresImageItems);
741
742                list = ImageManager.isVideo(image) ? enableList : disableList;
743                list.addAll(requiresVideoItems);
744
745                for (MenuItem item : enableList) {
746                    item.setVisible(true);
747                    item.setEnabled(true);
748                }
749
750                for (MenuItem item : disableList) {
751                    item.setVisible(false);
752                    item.setEnabled(false);
753                }
754            }
755
756            // must override abstract method
757            public void aboutToCall(MenuItem menu, IImage image) {
758            }
759        };
760    }
761
762    static void deletePhoto(Activity activity, Runnable onDelete) {
763        deleteImpl(activity, onDelete, true);
764    }
765
766    static void deleteImage(
767            Activity activity, Runnable onDelete, IImage image) {
768        deleteImpl(activity, onDelete, ImageManager.isImage(image));
769    }
770
771    static void deleteImpl(
772            Activity activity, Runnable onDelete, boolean isImage) {
773        boolean needConfirm = PreferenceManager
774                 .getDefaultSharedPreferences(activity)
775                 .getBoolean("pref_gallery_confirm_delete_key", true);
776        if (!needConfirm) {
777            if (onDelete != null) onDelete.run();
778        } else {
779            String title = activity.getString(R.string.confirm_delete_title);
780            String message = activity.getString(isImage
781                    ? R.string.confirm_delete_message
782                    : R.string.confirm_delete_video_message);
783            confirmAction(activity, title, message, onDelete);
784        }
785    }
786
787    public static void deleteMultiple(Context context, Runnable action) {
788        boolean needConfirm = PreferenceManager
789            .getDefaultSharedPreferences(context)
790            .getBoolean("pref_gallery_confirm_delete_key", true);
791        if (!needConfirm) {
792            if (action != null) action.run();
793        } else {
794            String title = context.getString(R.string.confirm_delete_title);
795            String message = context.getString(
796                    R.string.confirm_delete_multiple_message);
797            confirmAction(context, title, message, action);
798        }
799    }
800
801    public static void confirmAction(Context context, String title,
802            String message, final Runnable action) {
803        OnClickListener listener = new OnClickListener() {
804            public void onClick(DialogInterface dialog, int which) {
805                switch (which) {
806                    case DialogInterface.BUTTON_POSITIVE:
807                        if (action != null) action.run();
808                }
809            }
810        };
811        new AlertDialog.Builder(context)
812            .setIcon(android.R.drawable.ic_dialog_alert)
813            .setTitle(title)
814            .setMessage(message)
815            .setPositiveButton(android.R.string.ok, listener)
816            .setNegativeButton(android.R.string.cancel, listener)
817            .create()
818            .show();
819    }
820
821    static void addCapturePictureMenuItems(Menu menu, final Activity activity) {
822        menu.add(Menu.NONE, Menu.NONE, POSITION_CAPTURE_PICTURE,
823                R.string.capture_picture)
824                .setOnMenuItemClickListener(
825                new MenuItem.OnMenuItemClickListener() {
826                    public boolean onMenuItemClick(MenuItem item) {
827                        return onCapturePictureClicked(activity);
828                    }
829                }).setIcon(android.R.drawable.ic_menu_camera);
830    }
831
832    private static boolean onCapturePictureClicked(Activity activity) {
833        Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
834        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
835        try {
836            activity.startActivity(intent);
837        } catch (android.content.ActivityNotFoundException e) {
838            // Ignore exception
839        }
840        return true;
841    }
842
843    static void addCaptureVideoMenuItems(Menu menu, final Activity activity) {
844        menu.add(Menu.NONE, Menu.NONE, POSITION_CAPTURE_VIDEO,
845                R.string.capture_video)
846                .setOnMenuItemClickListener(
847                new MenuItem.OnMenuItemClickListener() {
848                    public boolean onMenuItemClick(MenuItem item) {
849                        return onCaptureVideoClicked(activity);
850                    }
851                }).setIcon(R.drawable.ic_menu_camera_video_view);
852    }
853
854    private static boolean onCaptureVideoClicked(Activity activity) {
855        Intent intent = new Intent(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
856        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
857        try {
858            activity.startActivity(intent);
859        } catch (android.content.ActivityNotFoundException e) {
860            // Ignore exception
861        }
862        return true;
863    }
864
865    public static void addCaptureMenuItems(Menu menu, final Activity activity) {
866        addCapturePictureMenuItems(menu, activity);
867        addCaptureVideoMenuItems(menu, activity);
868    }
869
870    public static String formatDuration(final Context context,
871            int durationMs) {
872        int duration = durationMs / 1000;
873        int h = duration / 3600;
874        int m = (duration - h * 3600) / 60;
875        int s = duration - (h * 3600 + m * 60);
876        String durationValue;
877        if (h == 0) {
878            durationValue = String.format(
879                    context.getString(R.string.details_ms), m, s);
880        } else {
881            durationValue = String.format(
882                    context.getString(R.string.details_hms), h, m, s);
883        }
884        return durationValue;
885    }
886
887    public static void showStorageToast(Activity activity) {
888        showStorageToast(activity, calculatePicturesRemaining());
889    }
890
891    public static void showStorageToast(Activity activity, int remaining) {
892        String noStorageText = null;
893
894        if (remaining == MenuHelper.NO_STORAGE_ERROR) {
895            String state = Environment.getExternalStorageState();
896            if (state == Environment.MEDIA_CHECKING) {
897                noStorageText = activity.getString(R.string.preparing_sd);
898            } else {
899                noStorageText = activity.getString(R.string.no_storage);
900            }
901        } else if (remaining < 1) {
902            noStorageText = activity.getString(R.string.not_enough_space);
903        }
904
905        if (noStorageText != null) {
906            Toast.makeText(activity, noStorageText, 5000).show();
907        }
908    }
909
910    public static int calculatePicturesRemaining() {
911        try {
912            if (!ImageManager.hasStorage()) {
913                return NO_STORAGE_ERROR;
914            } else {
915                String storageDirectory =
916                        Environment.getExternalStorageDirectory().toString();
917                StatFs stat = new StatFs(storageDirectory);
918                float remaining = ((float) stat.getAvailableBlocks()
919                        * (float) stat.getBlockSize()) / 400000F;
920                return (int) remaining;
921            }
922        } catch (Exception ex) {
923            // if we can't stat the filesystem then we don't know how many
924            // pictures are remaining.  it might be zero but just leave it
925            // blank since we really don't know.
926            return CANNOT_STAT_ERROR;
927        }
928    }
929}
930