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