• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
SelectFileDialog(long nativeSelectFileDialog)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
selectFile( String[] fileTypes, boolean capture, boolean multiple, WindowAndroid window)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      */
getFileForImageCapture(Context context)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
onIntentCompleted(WindowAndroid window, int resultCode, ContentResolver contentResolver, Intent results)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 
onFileNotSelected()259     private void onFileNotSelected() {
260         nativeOnFileNotSelected(mNativeSelectFileDialog);
261     }
262 
noSpecificType()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 
shouldShowTypes(String allTypes, String specificType)271     private boolean shouldShowTypes(String allTypes, String specificType) {
272         if (noSpecificType() || mFileTypes.contains(allTypes)) return true;
273         return acceptSpecificType(specificType);
274     }
275 
shouldShowImageTypes()276     private boolean shouldShowImageTypes() {
277         return shouldShowTypes(ALL_IMAGE_TYPES, IMAGE_TYPE);
278     }
279 
shouldShowVideoTypes()280     private boolean shouldShowVideoTypes() {
281         return shouldShowTypes(ALL_VIDEO_TYPES, VIDEO_TYPE);
282     }
283 
shouldShowAudioTypes()284     private boolean shouldShowAudioTypes() {
285         return shouldShowTypes(ALL_AUDIO_TYPES, AUDIO_TYPE);
286     }
287 
acceptsSpecificType(String type)288     private boolean acceptsSpecificType(String type) {
289         return mFileTypes.size() == 1 && TextUtils.equals(mFileTypes.get(0), type);
290     }
291 
captureCamera()292     private boolean captureCamera() {
293         return mCapture && acceptsSpecificType(ALL_IMAGE_TYPES);
294     }
295 
captureCamcorder()296     private boolean captureCamcorder() {
297         return mCapture && acceptsSpecificType(ALL_VIDEO_TYPES);
298     }
299 
captureMicrophone()300     private boolean captureMicrophone() {
301         return mCapture && acceptsSpecificType(ALL_AUDIO_TYPES);
302     }
303 
acceptSpecificType(String accept)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 
GetDisplayNameTask(ContentResolver contentResolver, boolean isMultiple)318         public GetDisplayNameTask(ContentResolver contentResolver, boolean isMultiple) {
319             mContentResolver = contentResolver;
320             mIsMultiple = isMultiple;
321         }
322 
323         @Override
doInBackground(Uri...uris)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
onPostExecute(String[] result)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
create(long nativeSelectFileDialog)362     private static SelectFileDialog create(long nativeSelectFileDialog) {
363         return new SelectFileDialog(nativeSelectFileDialog);
364     }
365 
nativeOnFileSelected(long nativeSelectFileDialogImpl, String filePath, String displayName)366     private native void nativeOnFileSelected(long nativeSelectFileDialogImpl,
367             String filePath, String displayName);
nativeOnMultipleFilesSelected(long nativeSelectFileDialogImpl, String[] filePathArray, String[] displayNameArray)368     private native void nativeOnMultipleFilesSelected(long nativeSelectFileDialogImpl,
369             String[] filePathArray, String[] displayNameArray);
nativeOnFileNotSelected(long nativeSelectFileDialogImpl)370     private native void nativeOnFileNotSelected(long nativeSelectFileDialogImpl);
371 }
372