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