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.
5 package org.chromium.ui.base;
7 import android.annotation.TargetApi;
8 import android.app.Activity;
9 import android.content.ClipData;
10 import android.content.ContentResolver;
11 import android.content.Intent;
12 import android.net.Uri;
13 import android.os.AsyncTask;
14 import android.os.Build;
15 import android.os.Environment;
16 import android.provider.MediaStore;
17 import android.text.TextUtils;
19 import org.chromium.base.CalledByNative;
20 import org.chromium.base.ContentUriUtils;
21 import org.chromium.base.JNINamespace;
22 import org.chromium.ui.R;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.List;
30 * A dialog that is triggered from a file input field that allows a user to select a file based on
31 * a set of accepted file types. The path of the selected file is passed to the native dialog.
34 class SelectFileDialog implements WindowAndroid.IntentCallback{
35 private static final String IMAGE_TYPE = "image/";
36 private static final String VIDEO_TYPE = "video/";
37 private static final String AUDIO_TYPE = "audio/";
38 private static final String ALL_IMAGE_TYPES = IMAGE_TYPE + "*";
39 private static final String ALL_VIDEO_TYPES = VIDEO_TYPE + "*";
40 private static final String ALL_AUDIO_TYPES = AUDIO_TYPE + "*";
41 private static final String ANY_TYPES = "*/*";
42 private static final String CAPTURE_IMAGE_DIRECTORY = "browser-photos";
44 private final long mNativeSelectFileDialog;
45 private List<String> mFileTypes;
46 private boolean mCapture;
47 private Uri mCameraOutputUri;
49 private SelectFileDialog(long nativeSelectFileDialog) {
50 mNativeSelectFileDialog = nativeSelectFileDialog;
54 * Creates and starts an intent based on the passed fileTypes and capture value.
55 * @param fileTypes MIME types requested (i.e. "image/*")
56 * @param capture The capture value as described in http://www.w3.org/TR/html-media-capture/
57 * @param multiple Whether it should be possible to select multiple files.
58 * @param window The WindowAndroid that can show intents
60 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
62 private void selectFile(
63 String[] fileTypes, boolean capture, boolean multiple, WindowAndroid window) {
64 mFileTypes = new ArrayList<String>(Arrays.asList(fileTypes));
67 Intent chooser = new Intent(Intent.ACTION_CHOOSER);
68 Intent camera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
69 mCameraOutputUri = Uri.fromFile(getFileForImageCapture());
70 camera.putExtra(MediaStore.EXTRA_OUTPUT, mCameraOutputUri);
71 Intent camcorder = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
72 Intent soundRecorder = new Intent(
73 MediaStore.Audio.Media.RECORD_SOUND_ACTION);
75 // Quick check - if the |capture| parameter is set and |fileTypes| has the appropriate MIME
76 // type, we should just launch the appropriate intent. Otherwise build up a chooser based on
77 // the accept type and then display that to the user.
78 if (captureCamera()) {
79 if (window.showIntent(camera, this, R.string.low_memory_error)) return;
80 } else if (captureCamcorder()) {
81 if (window.showIntent(camcorder, this, R.string.low_memory_error)) return;
82 } else if (captureMicrophone()) {
83 if (window.showIntent(soundRecorder, this, R.string.low_memory_error)) return;
86 Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT);
87 getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);
89 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && multiple)
90 getContentIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
92 ArrayList<Intent> extraIntents = new ArrayList<Intent>();
93 if (!noSpecificType()) {
94 // Create a chooser based on the accept type that was specified in the webpage. Note
95 // that if the web page specified multiple accept types, we will have built a generic
97 if (shouldShowImageTypes()) {
98 extraIntents.add(camera);
99 getContentIntent.setType(ALL_IMAGE_TYPES);
100 } else if (shouldShowVideoTypes()) {
101 extraIntents.add(camcorder);
102 getContentIntent.setType(ALL_VIDEO_TYPES);
103 } else if (shouldShowAudioTypes()) {
104 extraIntents.add(soundRecorder);
105 getContentIntent.setType(ALL_AUDIO_TYPES);
109 if (extraIntents.isEmpty()) {
110 // We couldn't resolve an accept type, so fallback to a generic chooser.
111 getContentIntent.setType(ANY_TYPES);
112 extraIntents.add(camera);
113 extraIntents.add(camcorder);
114 extraIntents.add(soundRecorder);
117 chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS,
118 extraIntents.toArray(new Intent[] { }));
120 chooser.putExtra(Intent.EXTRA_INTENT, getContentIntent);
122 if (!window.showIntent(chooser, this, R.string.low_memory_error)) {
128 * Get a file for the image capture in the CAPTURE_IMAGE_DIRECTORY directory.
130 private File getFileForImageCapture() {
131 File externalDataDir = Environment.getExternalStoragePublicDirectory(
132 Environment.DIRECTORY_DCIM);
133 File cameraDataDir = new File(externalDataDir.getAbsolutePath() +
134 File.separator + CAPTURE_IMAGE_DIRECTORY);
135 if (!cameraDataDir.exists() && !cameraDataDir.mkdirs()) {
136 cameraDataDir = externalDataDir;
138 File photoFile = new File(cameraDataDir.getAbsolutePath() +
139 File.separator + System.currentTimeMillis() + ".jpg");
144 * Callback method to handle the intent results and pass on the path to the native
146 * @param window The window that has access to the application activity.
147 * @param resultCode The result code whether the intent returned successfully.
148 * @param contentResolver The content resolver used to extract the path of the selected file.
149 * @param results The results of the requested intent.
151 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
153 public void onIntentCompleted(WindowAndroid window, int resultCode,
154 ContentResolver contentResolver, Intent results) {
155 if (resultCode != Activity.RESULT_OK) {
160 if (results == null) {
161 // If we have a successful return but no data, then assume this is the camera returning
162 // the photo that we requested.
163 nativeOnFileSelected(mNativeSelectFileDialog, mCameraOutputUri.getPath(), "");
165 // Broadcast to the media scanner that there's a new photo on the device so it will
166 // show up right away in the gallery (rather than waiting until the next time the media
168 window.sendBroadcast(new Intent(
169 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mCameraOutputUri));
173 // Path for when EXTRA_ALLOW_MULTIPLE Intent extra has been defined. Each of the selected
174 // files will be shared as an entry on the Intent's ClipData. This functionality is only
175 // available in Android JellyBean MR2 and higher.
176 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 &&
177 results.getData() == null &&
178 results.getClipData() != null) {
179 ClipData clipData = results.getClipData();
181 int itemCount = clipData.getItemCount();
182 if (itemCount == 0) {
187 Uri[] filePathArray = new Uri[itemCount];
188 for (int i = 0; i < itemCount; ++i) {
189 filePathArray[i] = clipData.getItemAt(i).getUri();
191 GetDisplayNameTask task = new GetDisplayNameTask(contentResolver, true);
192 task.execute(filePathArray);
196 if (ContentResolver.SCHEME_FILE.equals(results.getData().getScheme())) {
197 nativeOnFileSelected(mNativeSelectFileDialog,
198 results.getData().getSchemeSpecificPart(), "");
202 if (ContentResolver.SCHEME_CONTENT.equals(results.getScheme())) {
203 GetDisplayNameTask task = new GetDisplayNameTask(contentResolver, false);
204 task.execute(results.getData());
209 window.showError(R.string.opening_file_error);
212 private void onFileNotSelected() {
213 nativeOnFileNotSelected(mNativeSelectFileDialog);
216 private boolean noSpecificType() {
217 // We use a single Intent to decide the type of the file chooser we display to the user,
218 // which means we can only give it a single type. If there are multiple accept types
219 // specified, we will fallback to a generic chooser (unless a capture parameter has been
220 // specified, in which case we'll try to satisfy that first.
221 return mFileTypes.size() != 1 || mFileTypes.contains(ANY_TYPES);
224 private boolean shouldShowTypes(String allTypes, String specificType) {
225 if (noSpecificType() || mFileTypes.contains(allTypes)) return true;
226 return acceptSpecificType(specificType);
229 private boolean shouldShowImageTypes() {
230 return shouldShowTypes(ALL_IMAGE_TYPES, IMAGE_TYPE);
233 private boolean shouldShowVideoTypes() {
234 return shouldShowTypes(ALL_VIDEO_TYPES, VIDEO_TYPE);
237 private boolean shouldShowAudioTypes() {
238 return shouldShowTypes(ALL_AUDIO_TYPES, AUDIO_TYPE);
241 private boolean acceptsSpecificType(String type) {
242 return mFileTypes.size() == 1 && TextUtils.equals(mFileTypes.get(0), type);
245 private boolean captureCamera() {
246 return mCapture && acceptsSpecificType(ALL_IMAGE_TYPES);
249 private boolean captureCamcorder() {
250 return mCapture && acceptsSpecificType(ALL_VIDEO_TYPES);
253 private boolean captureMicrophone() {
254 return mCapture && acceptsSpecificType(ALL_AUDIO_TYPES);
257 private boolean acceptSpecificType(String accept) {
258 for (String type : mFileTypes) {
259 if (type.startsWith(accept)) {
266 private class GetDisplayNameTask extends AsyncTask<Uri, Void, String[]> {
268 final ContentResolver mContentResolver;
269 final boolean mIsMultiple;
271 public GetDisplayNameTask(ContentResolver contentResolver, boolean isMultiple) {
272 mContentResolver = contentResolver;
273 mIsMultiple = isMultiple;
277 protected String[] doInBackground(Uri...uris) {
278 mFilePaths = new String[uris.length];
279 String[] displayNames = new String[uris.length];
280 for (int i = 0; i < uris.length; i++) {
281 mFilePaths[i] = uris[i].toString();
282 displayNames[i] = ContentUriUtils.getDisplayName(
283 uris[i], mContentResolver, MediaStore.MediaColumns.DISPLAY_NAME);
289 protected void onPostExecute(String[] result) {
291 nativeOnMultipleFilesSelected(mNativeSelectFileDialog, mFilePaths, result);
293 nativeOnFileSelected(mNativeSelectFileDialog, mFilePaths[0], result[0]);
299 private static SelectFileDialog create(long nativeSelectFileDialog) {
300 return new SelectFileDialog(nativeSelectFileDialog);
303 private native void nativeOnFileSelected(long nativeSelectFileDialogImpl,
304 String filePath, String displayName);
305 private native void nativeOnMultipleFilesSelected(long nativeSelectFileDialogImpl,
306 String[] filePathArray, String[] displayNameArray);
307 private native void nativeOnFileNotSelected(long nativeSelectFileDialogImpl);