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 
17 package com.android.camera;
18 
19 import com.android.gallery.R;
20 
21 import android.app.Activity;
22 import android.app.AlertDialog;
23 import android.content.ActivityNotFoundException;
24 import android.content.Context;
25 import android.content.DialogInterface;
26 import android.content.Intent;
27 import android.content.DialogInterface.OnClickListener;
28 import android.location.Geocoder;
29 import android.media.ExifInterface;
30 import android.media.MediaMetadataRetriever;
31 import android.net.Uri;
32 import android.os.Environment;
33 import android.os.Handler;
34 import android.os.StatFs;
35 import android.preference.PreferenceManager;
36 import android.provider.MediaStore;
37 import android.provider.MediaStore.Images;
38 import android.text.format.Formatter;
39 import android.util.Log;
40 import android.view.Menu;
41 import android.view.MenuItem;
42 import android.view.SubMenu;
43 import android.view.View;
44 import android.widget.ImageView;
45 import android.widget.TextView;
46 import android.widget.Toast;
47 
48 import com.android.camera.gallery.IImage;
49 
50 import java.io.Closeable;
51 import java.io.IOException;
52 import java.lang.ref.WeakReference;
53 import java.text.SimpleDateFormat;
54 import java.util.ArrayList;
55 import java.util.Date;
56 import java.util.List;
57 
58 /**
59  * A utility class to handle various kinds of menu operations.
60  */
61 public 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 {
gettingReadyToOpen(Menu menu, IImage image)106         public void gettingReadyToOpen(Menu menu, IImage image);
aboutToCall(MenuItem item, IImage image)107         public void aboutToCall(MenuItem item, IImage image);
108     }
109 
110     public interface MenuInvoker {
run(MenuCallback r)111         public void run(MenuCallback r);
112     }
113 
114     public interface MenuCallback {
run(Uri uri, IImage image)115         public void run(Uri uri, IImage image);
116     }
117 
closeSilently(Closeable c)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 
getImageFileSize(IImage image)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)
isWhiteListUri(Uri uri)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 
enableShareMenuItem(Menu menu, boolean enabled)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 
hasLatLngData(IImage image)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 
enableShowOnMapMenuItem(Menu menu, boolean enabled)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 
setDetailsValue(View d, String text, int valueId)188     private static void setDetailsValue(View d, String text, int valueId) {
189         ((TextView) d.findViewById(valueId)).setText(text);
190     }
191 
hideDetailsRow(View d, int rowId)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 
UpdateLocationCallback(WeakReference<View> view)200         public UpdateLocationCallback(WeakReference<View> view) {
201             mView = view;
202         }
203 
onComplete(String location)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 
setLatLngDetails(final View d, Activity context, ExifInterface exif)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 
getExif(IImage image)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.
onShowMapClicked(MenuInvoker onInvoke, final Handler handler, final Activity activity)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 
hideExifInformation(View d)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 
showExifInformation(IImage image, View d, Activity activity)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      */
getWhiteBalanceString(ExifInterface exif)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.
onDetailsClicked(MenuInvoker onInvoke, final Handler handler, final Activity activity)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.
onRotateClicked(MenuInvoker onInvoke, final int degree)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.
onCropClicked(MenuInvoker onInvoke, final Activity activity)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.
onSetAsClicked(MenuInvoker onInvoke, final Activity activity)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.
onImageShareClicked(MenuInvoker onInvoke, final Activity activity)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.
onViewPlayClicked(MenuInvoker onInvoke, final Activity activity)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.
onDeleteClicked(MenuInvoker onInvoke, final Activity activity, final Runnable onDelete)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 
addImageMenuItems( Menu menu, int inclusions, final Activity activity, final Handler handler, final Runnable onDelete, final MenuInvoker onInvoke)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