Home | History | Annotate | Download | only in camera
      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 {
    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