Home | History | Annotate | Download | only in base
      1 // Copyright 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 package org.chromium.ui.base;
      6 
      7 import android.annotation.TargetApi;
      8 import android.app.Activity;
      9 import android.content.ClipData;
     10 import android.content.ContentResolver;
     11 import android.content.Context;
     12 import android.content.Intent;
     13 import android.net.Uri;
     14 import android.os.AsyncTask;
     15 import android.os.Build;
     16 import android.os.Environment;
     17 import android.provider.MediaStore;
     18 import android.text.TextUtils;
     19 import android.util.Log;
     20 
     21 import org.chromium.base.CalledByNative;
     22 import org.chromium.base.ContentUriUtils;
     23 import org.chromium.base.JNINamespace;
     24 import org.chromium.ui.R;
     25 
     26 import java.io.File;
     27 import java.io.IOException;
     28 import java.util.ArrayList;
     29 import java.util.Arrays;
     30 import java.util.List;
     31 
     32 /**
     33  * A dialog that is triggered from a file input field that allows a user to select a file based on
     34  * a set of accepted file types. The path of the selected file is passed to the native dialog.
     35  */
     36 @JNINamespace("ui")
     37 class SelectFileDialog implements WindowAndroid.IntentCallback {
     38     private static final String TAG = "SelectFileDialog";
     39     private static final String IMAGE_TYPE = "image/";
     40     private static final String VIDEO_TYPE = "video/";
     41     private static final String AUDIO_TYPE = "audio/";
     42     private static final String ALL_IMAGE_TYPES = IMAGE_TYPE + "*";
     43     private static final String ALL_VIDEO_TYPES = VIDEO_TYPE + "*";
     44     private static final String ALL_AUDIO_TYPES = AUDIO_TYPE + "*";
     45     private static final String ANY_TYPES = "*/*";
     46     private static final String CAPTURE_IMAGE_DIRECTORY = "browser-photos";
     47     // Keep this variable in sync with the value defined in file_paths.xml.
     48     private static final String IMAGE_FILE_PATH = "images";
     49 
     50     private final long mNativeSelectFileDialog;
     51     private List<String> mFileTypes;
     52     private boolean mCapture;
     53     private Uri mCameraOutputUri;
     54 
     55     private SelectFileDialog(long nativeSelectFileDialog) {
     56         mNativeSelectFileDialog = nativeSelectFileDialog;
     57     }
     58 
     59     /**
     60      * Creates and starts an intent based on the passed fileTypes and capture value.
     61      * @param fileTypes MIME types requested (i.e. "image/*")
     62      * @param capture The capture value as described in http://www.w3.org/TR/html-media-capture/
     63      * @param multiple Whether it should be possible to select multiple files.
     64      * @param window The WindowAndroid that can show intents
     65      */
     66     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
     67     @CalledByNative
     68     private void selectFile(
     69             String[] fileTypes, boolean capture, boolean multiple, WindowAndroid window) {
     70         mFileTypes = new ArrayList<String>(Arrays.asList(fileTypes));
     71         mCapture = capture;
     72 
     73         Intent chooser = new Intent(Intent.ACTION_CHOOSER);
     74         Intent camera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
     75         Context context = window.getApplicationContext();
     76         camera.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
     77                 Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
     78         try {
     79             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
     80                 mCameraOutputUri = ContentUriUtils.getContentUriFromFile(
     81                         context, getFileForImageCapture(context));
     82             } else {
     83                 mCameraOutputUri = Uri.fromFile(getFileForImageCapture(context));
     84             }
     85         } catch (IOException e) {
     86             Log.e(TAG, "Cannot retrieve content uri from file", e);
     87         }
     88 
     89         if (mCameraOutputUri == null) {
     90             onFileNotSelected();
     91             return;
     92         }
     93 
     94         camera.putExtra(MediaStore.EXTRA_OUTPUT, mCameraOutputUri);
     95         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
     96             camera.setClipData(
     97                     ClipData.newUri(context.getContentResolver(),
     98                     IMAGE_FILE_PATH, mCameraOutputUri));
     99         }
    100         Intent camcorder = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
    101         Intent soundRecorder = new Intent(
    102                 MediaStore.Audio.Media.RECORD_SOUND_ACTION);
    103 
    104         // Quick check - if the |capture| parameter is set and |fileTypes| has the appropriate MIME
    105         // type, we should just launch the appropriate intent. Otherwise build up a chooser based on
    106         // the accept type and then display that to the user.
    107         if (captureCamera()) {
    108             if (window.showIntent(camera, this, R.string.low_memory_error)) return;
    109         } else if (captureCamcorder()) {
    110             if (window.showIntent(camcorder, this, R.string.low_memory_error)) return;
    111         } else if (captureMicrophone()) {
    112             if (window.showIntent(soundRecorder, this, R.string.low_memory_error)) return;
    113         }
    114 
    115         Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT);
    116         getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);
    117 
    118         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && multiple)
    119             getContentIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
    120 
    121         ArrayList<Intent> extraIntents = new ArrayList<Intent>();
    122         if (!noSpecificType()) {
    123             // Create a chooser based on the accept type that was specified in the webpage. Note
    124             // that if the web page specified multiple accept types, we will have built a generic
    125             // chooser above.
    126             if (shouldShowImageTypes()) {
    127                 extraIntents.add(camera);
    128                 getContentIntent.setType(ALL_IMAGE_TYPES);
    129             } else if (shouldShowVideoTypes()) {
    130                 extraIntents.add(camcorder);
    131                 getContentIntent.setType(ALL_VIDEO_TYPES);
    132             } else if (shouldShowAudioTypes()) {
    133                 extraIntents.add(soundRecorder);
    134                 getContentIntent.setType(ALL_AUDIO_TYPES);
    135             }
    136         }
    137 
    138         if (extraIntents.isEmpty()) {
    139             // We couldn't resolve an accept type, so fallback to a generic chooser.
    140             getContentIntent.setType(ANY_TYPES);
    141             extraIntents.add(camera);
    142             extraIntents.add(camcorder);
    143             extraIntents.add(soundRecorder);
    144         }
    145 
    146         chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS,
    147                 extraIntents.toArray(new Intent[] { }));
    148 
    149         chooser.putExtra(Intent.EXTRA_INTENT, getContentIntent);
    150 
    151         if (!window.showIntent(chooser, this, R.string.low_memory_error)) {
    152             onFileNotSelected();
    153         }
    154     }
    155 
    156     /**
    157      * Get a file for the image capture operation. For devices with JB MR2 or
    158      * latter android versions, the file is put under IMAGE_FILE_PATH directory.
    159      * For ICS devices, the file is put under CAPTURE_IMAGE_DIRECTORY.
    160      *
    161      * @param context The application context.
    162      * @return file path for the captured image to be stored.
    163      */
    164     private File getFileForImageCapture(Context context) throws IOException {
    165         File path;
    166         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
    167             path = new File(context.getFilesDir(), IMAGE_FILE_PATH);
    168             if (!path.exists() && !path.mkdir()) {
    169                 throw new IOException("Folder cannot be created.");
    170             }
    171         } else {
    172             File externalDataDir = Environment.getExternalStoragePublicDirectory(
    173                     Environment.DIRECTORY_DCIM);
    174             path = new File(externalDataDir.getAbsolutePath() +
    175                     File.separator + CAPTURE_IMAGE_DIRECTORY);
    176             if (!path.exists() && !path.mkdirs()) {
    177                 path = externalDataDir;
    178             }
    179         }
    180         File photoFile = File.createTempFile(
    181                 String.valueOf(System.currentTimeMillis()), ".jpg", path);
    182         return photoFile;
    183     }
    184 
    185     /**
    186      * Callback method to handle the intent results and pass on the path to the native
    187      * SelectFileDialog.
    188      * @param window The window that has access to the application activity.
    189      * @param resultCode The result code whether the intent returned successfully.
    190      * @param contentResolver The content resolver used to extract the path of the selected file.
    191      * @param results The results of the requested intent.
    192      */
    193     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    194     @Override
    195     public void onIntentCompleted(WindowAndroid window, int resultCode,
    196             ContentResolver contentResolver, Intent results) {
    197         if (resultCode != Activity.RESULT_OK) {
    198             onFileNotSelected();
    199             return;
    200         }
    201 
    202         if (results == null) {
    203             // If we have a successful return but no data, then assume this is the camera returning
    204             // the photo that we requested.
    205             // If the uri is a file, we need to convert it to the absolute path or otherwise
    206             // android cannot handle it correctly on some earlier versions.
    207             // http://crbug.com/423338.
    208             String path = ContentResolver.SCHEME_FILE.equals(mCameraOutputUri.getScheme()) ?
    209                     mCameraOutputUri.getPath() : mCameraOutputUri.toString();
    210             nativeOnFileSelected(mNativeSelectFileDialog, path,
    211                     mCameraOutputUri.getLastPathSegment());
    212             // Broadcast to the media scanner that there's a new photo on the device so it will
    213             // show up right away in the gallery (rather than waiting until the next time the media
    214             // scanner runs).
    215             window.sendBroadcast(new Intent(
    216                     Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mCameraOutputUri));
    217             return;
    218         }
    219 
    220         // Path for when EXTRA_ALLOW_MULTIPLE Intent extra has been defined. Each of the selected
    221         // files will be shared as an entry on the Intent's ClipData. This functionality is only
    222         // available in Android JellyBean MR2 and higher.
    223         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 &&
    224                 results.getData() == null &&
    225                 results.getClipData() != null) {
    226             ClipData clipData = results.getClipData();
    227 
    228             int itemCount = clipData.getItemCount();
    229             if (itemCount == 0) {
    230                 onFileNotSelected();
    231                 return;
    232             }
    233 
    234             Uri[] filePathArray = new Uri[itemCount];
    235             for (int i = 0; i < itemCount; ++i) {
    236                 filePathArray[i] = clipData.getItemAt(i).getUri();
    237             }
    238             GetDisplayNameTask task = new GetDisplayNameTask(contentResolver, true);
    239             task.execute(filePathArray);
    240             return;
    241         }
    242 
    243         if (ContentResolver.SCHEME_FILE.equals(results.getData().getScheme())) {
    244             nativeOnFileSelected(mNativeSelectFileDialog,
    245                     results.getData().getSchemeSpecificPart(), "");
    246             return;
    247         }
    248 
    249         if (ContentResolver.SCHEME_CONTENT.equals(results.getScheme())) {
    250             GetDisplayNameTask task = new GetDisplayNameTask(contentResolver, false);
    251             task.execute(results.getData());
    252             return;
    253         }
    254 
    255         onFileNotSelected();
    256         window.showError(R.string.opening_file_error);
    257     }
    258 
    259     private void onFileNotSelected() {
    260         nativeOnFileNotSelected(mNativeSelectFileDialog);
    261     }
    262 
    263     private boolean noSpecificType() {
    264         // We use a single Intent to decide the type of the file chooser we display to the user,
    265         // which means we can only give it a single type. If there are multiple accept types
    266         // specified, we will fallback to a generic chooser (unless a capture parameter has been
    267         // specified, in which case we'll try to satisfy that first.
    268         return mFileTypes.size() != 1 || mFileTypes.contains(ANY_TYPES);
    269     }
    270 
    271     private boolean shouldShowTypes(String allTypes, String specificType) {
    272         if (noSpecificType() || mFileTypes.contains(allTypes)) return true;
    273         return acceptSpecificType(specificType);
    274     }
    275 
    276     private boolean shouldShowImageTypes() {
    277         return shouldShowTypes(ALL_IMAGE_TYPES, IMAGE_TYPE);
    278     }
    279 
    280     private boolean shouldShowVideoTypes() {
    281         return shouldShowTypes(ALL_VIDEO_TYPES, VIDEO_TYPE);
    282     }
    283 
    284     private boolean shouldShowAudioTypes() {
    285         return shouldShowTypes(ALL_AUDIO_TYPES, AUDIO_TYPE);
    286     }
    287 
    288     private boolean acceptsSpecificType(String type) {
    289         return mFileTypes.size() == 1 && TextUtils.equals(mFileTypes.get(0), type);
    290     }
    291 
    292     private boolean captureCamera() {
    293         return mCapture && acceptsSpecificType(ALL_IMAGE_TYPES);
    294     }
    295 
    296     private boolean captureCamcorder() {
    297         return mCapture && acceptsSpecificType(ALL_VIDEO_TYPES);
    298     }
    299 
    300     private boolean captureMicrophone() {
    301         return mCapture && acceptsSpecificType(ALL_AUDIO_TYPES);
    302     }
    303 
    304     private boolean acceptSpecificType(String accept) {
    305         for (String type : mFileTypes) {
    306             if (type.startsWith(accept)) {
    307                 return true;
    308             }
    309         }
    310         return false;
    311     }
    312 
    313     private class GetDisplayNameTask extends AsyncTask<Uri, Void, String[]> {
    314         String[] mFilePaths;
    315         final ContentResolver mContentResolver;
    316         final boolean mIsMultiple;
    317 
    318         public GetDisplayNameTask(ContentResolver contentResolver, boolean isMultiple) {
    319             mContentResolver = contentResolver;
    320             mIsMultiple = isMultiple;
    321         }
    322 
    323         @Override
    324         protected String[] doInBackground(Uri...uris) {
    325             mFilePaths = new String[uris.length];
    326             String[] displayNames = new String[uris.length];
    327             try {
    328                 for (int i = 0; i < uris.length; i++) {
    329                     mFilePaths[i] = uris[i].toString();
    330                     displayNames[i] = ContentUriUtils.getDisplayName(
    331                             uris[i], mContentResolver, MediaStore.MediaColumns.DISPLAY_NAME);
    332                 }
    333             }  catch (SecurityException e) {
    334                 // Some third party apps will present themselves as being able
    335                 // to handle the ACTION_GET_CONTENT intent but then declare themselves
    336                 // as exported=false (or more often omit the exported keyword in
    337                 // the manifest which defaults to false after JB).
    338                 // In those cases trying to access the contents raises a security exception
    339                 // which we should not crash on. See crbug.com/382367 for details.
    340                 Log.w(TAG, "Unable to extract results from the content provider");
    341                 return null;
    342             }
    343 
    344             return displayNames;
    345         }
    346 
    347         @Override
    348         protected void onPostExecute(String[] result) {
    349             if (result == null) {
    350                 onFileNotSelected();
    351                 return;
    352             }
    353             if (mIsMultiple) {
    354                 nativeOnMultipleFilesSelected(mNativeSelectFileDialog, mFilePaths, result);
    355             } else {
    356                 nativeOnFileSelected(mNativeSelectFileDialog, mFilePaths[0], result[0]);
    357             }
    358         }
    359     }
    360 
    361     @CalledByNative
    362     private static SelectFileDialog create(long nativeSelectFileDialog) {
    363         return new SelectFileDialog(nativeSelectFileDialog);
    364     }
    365 
    366     private native void nativeOnFileSelected(long nativeSelectFileDialogImpl,
    367             String filePath, String displayName);
    368     private native void nativeOnMultipleFilesSelected(long nativeSelectFileDialogImpl,
    369             String[] filePathArray, String[] displayNameArray);
    370     private native void nativeOnFileNotSelected(long nativeSelectFileDialogImpl);
    371 }
    372