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.app.Activity; 8 import android.content.ContentResolver; 9 import android.content.Intent; 10 import android.net.Uri; 11 import android.os.AsyncTask; 12 import android.os.Environment; 13 import android.provider.MediaStore; 14 import android.text.TextUtils; 15 16 import org.chromium.base.CalledByNative; 17 import org.chromium.base.ContentUriUtils; 18 import org.chromium.base.JNINamespace; 19 import org.chromium.ui.R; 20 21 import java.io.File; 22 import java.util.ArrayList; 23 import java.util.Arrays; 24 import java.util.List; 25 26 /** 27 * A dialog that is triggered from a file input field that allows a user to select a file based on 28 * a set of accepted file types. The path of the selected file is passed to the native dialog. 29 */ 30 @JNINamespace("ui") 31 class SelectFileDialog implements WindowAndroid.IntentCallback{ 32 private static final String IMAGE_TYPE = "image/"; 33 private static final String VIDEO_TYPE = "video/"; 34 private static final String AUDIO_TYPE = "audio/"; 35 private static final String ALL_IMAGE_TYPES = IMAGE_TYPE + "*"; 36 private static final String ALL_VIDEO_TYPES = VIDEO_TYPE + "*"; 37 private static final String ALL_AUDIO_TYPES = AUDIO_TYPE + "*"; 38 private static final String ANY_TYPES = "*/*"; 39 private static final String CAPTURE_IMAGE_DIRECTORY = "browser-photos"; 40 41 private final long mNativeSelectFileDialog; 42 private List<String> mFileTypes; 43 private boolean mCapture; 44 private Uri mCameraOutputUri; 45 SelectFileDialog(long nativeSelectFileDialog)46 private SelectFileDialog(long nativeSelectFileDialog) { 47 mNativeSelectFileDialog = nativeSelectFileDialog; 48 } 49 50 /** 51 * Creates and starts an intent based on the passed fileTypes and capture value. 52 * @param fileTypes MIME types requested (i.e. "image/*") 53 * @param capture The capture value as described in http://www.w3.org/TR/html-media-capture/ 54 * @param window The WindowAndroid that can show intents 55 */ 56 @CalledByNative selectFile(String[] fileTypes, boolean capture, WindowAndroid window)57 private void selectFile(String[] fileTypes, boolean capture, WindowAndroid window) { 58 mFileTypes = new ArrayList<String>(Arrays.asList(fileTypes)); 59 mCapture = capture; 60 61 Intent chooser = new Intent(Intent.ACTION_CHOOSER); 62 Intent camera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 63 mCameraOutputUri = Uri.fromFile(getFileForImageCapture()); 64 camera.putExtra(MediaStore.EXTRA_OUTPUT, mCameraOutputUri); 65 Intent camcorder = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 66 Intent soundRecorder = new Intent( 67 MediaStore.Audio.Media.RECORD_SOUND_ACTION); 68 69 // Quick check - if the |capture| parameter is set and |fileTypes| has the appropriate MIME 70 // type, we should just launch the appropriate intent. Otherwise build up a chooser based on 71 // the accept type and then display that to the user. 72 if (captureCamera()) { 73 if (window.showIntent(camera, this, R.string.low_memory_error)) return; 74 } else if (captureCamcorder()) { 75 if (window.showIntent(camcorder, this, R.string.low_memory_error)) return; 76 } else if (captureMicrophone()) { 77 if (window.showIntent(soundRecorder, this, R.string.low_memory_error)) return; 78 } 79 80 Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT); 81 getContentIntent.addCategory(Intent.CATEGORY_OPENABLE); 82 ArrayList<Intent> extraIntents = new ArrayList<Intent>(); 83 if (!noSpecificType()) { 84 // Create a chooser based on the accept type that was specified in the webpage. Note 85 // that if the web page specified multiple accept types, we will have built a generic 86 // chooser above. 87 if (shouldShowImageTypes()) { 88 extraIntents.add(camera); 89 getContentIntent.setType(ALL_IMAGE_TYPES); 90 } else if (shouldShowVideoTypes()) { 91 extraIntents.add(camcorder); 92 getContentIntent.setType(ALL_VIDEO_TYPES); 93 } else if (shouldShowAudioTypes()) { 94 extraIntents.add(soundRecorder); 95 getContentIntent.setType(ALL_AUDIO_TYPES); 96 } 97 } 98 99 if (extraIntents.isEmpty()) { 100 // We couldn't resolve an accept type, so fallback to a generic chooser. 101 getContentIntent.setType(ANY_TYPES); 102 extraIntents.add(camera); 103 extraIntents.add(camcorder); 104 extraIntents.add(soundRecorder); 105 } 106 107 chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, 108 extraIntents.toArray(new Intent[] { })); 109 110 chooser.putExtra(Intent.EXTRA_INTENT, getContentIntent); 111 112 if (!window.showIntent(chooser, this, R.string.low_memory_error)) { 113 onFileNotSelected(); 114 } 115 } 116 117 /** 118 * Get a file for the image capture in the CAPTURE_IMAGE_DIRECTORY directory. 119 */ getFileForImageCapture()120 private File getFileForImageCapture() { 121 File externalDataDir = Environment.getExternalStoragePublicDirectory( 122 Environment.DIRECTORY_DCIM); 123 File cameraDataDir = new File(externalDataDir.getAbsolutePath() + 124 File.separator + CAPTURE_IMAGE_DIRECTORY); 125 if (!cameraDataDir.exists() && !cameraDataDir.mkdirs()) { 126 cameraDataDir = externalDataDir; 127 } 128 File photoFile = new File(cameraDataDir.getAbsolutePath() + 129 File.separator + System.currentTimeMillis() + ".jpg"); 130 return photoFile; 131 } 132 133 /** 134 * Callback method to handle the intent results and pass on the path to the native 135 * SelectFileDialog. 136 * @param window The window that has access to the application activity. 137 * @param resultCode The result code whether the intent returned successfully. 138 * @param contentResolver The content resolver used to extract the path of the selected file. 139 * @param results The results of the requested intent. 140 */ 141 @Override onIntentCompleted(WindowAndroid window, int resultCode, ContentResolver contentResolver, Intent results)142 public void onIntentCompleted(WindowAndroid window, int resultCode, 143 ContentResolver contentResolver, Intent results) { 144 if (resultCode != Activity.RESULT_OK) { 145 onFileNotSelected(); 146 return; 147 } 148 149 if (results == null) { 150 // If we have a successful return but no data, then assume this is the camera returning 151 // the photo that we requested. 152 nativeOnFileSelected(mNativeSelectFileDialog, mCameraOutputUri.getPath(), ""); 153 154 // Broadcast to the media scanner that there's a new photo on the device so it will 155 // show up right away in the gallery (rather than waiting until the next time the media 156 // scanner runs). 157 window.sendBroadcast(new Intent( 158 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mCameraOutputUri)); 159 return; 160 } 161 162 if (ContentResolver.SCHEME_FILE.equals(results.getData().getScheme())) { 163 nativeOnFileSelected(mNativeSelectFileDialog, 164 results.getData().getSchemeSpecificPart(), ""); 165 return; 166 } 167 168 if (ContentResolver.SCHEME_CONTENT.equals(results.getScheme())) { 169 GetDisplayNameTask task = new GetDisplayNameTask(contentResolver, false); 170 task.execute(results.getData()); 171 return; 172 } 173 174 onFileNotSelected(); 175 window.showError(R.string.opening_file_error); 176 } 177 onFileNotSelected()178 private void onFileNotSelected() { 179 nativeOnFileNotSelected(mNativeSelectFileDialog); 180 } 181 noSpecificType()182 private boolean noSpecificType() { 183 // We use a single Intent to decide the type of the file chooser we display to the user, 184 // which means we can only give it a single type. If there are multiple accept types 185 // specified, we will fallback to a generic chooser (unless a capture parameter has been 186 // specified, in which case we'll try to satisfy that first. 187 return mFileTypes.size() != 1 || mFileTypes.contains(ANY_TYPES); 188 } 189 shouldShowTypes(String allTypes, String specificType)190 private boolean shouldShowTypes(String allTypes, String specificType) { 191 if (noSpecificType() || mFileTypes.contains(allTypes)) return true; 192 return acceptSpecificType(specificType); 193 } 194 shouldShowImageTypes()195 private boolean shouldShowImageTypes() { 196 return shouldShowTypes(ALL_IMAGE_TYPES, IMAGE_TYPE); 197 } 198 shouldShowVideoTypes()199 private boolean shouldShowVideoTypes() { 200 return shouldShowTypes(ALL_VIDEO_TYPES, VIDEO_TYPE); 201 } 202 shouldShowAudioTypes()203 private boolean shouldShowAudioTypes() { 204 return shouldShowTypes(ALL_AUDIO_TYPES, AUDIO_TYPE); 205 } 206 acceptsSpecificType(String type)207 private boolean acceptsSpecificType(String type) { 208 return mFileTypes.size() == 1 && TextUtils.equals(mFileTypes.get(0), type); 209 } 210 captureCamera()211 private boolean captureCamera() { 212 return mCapture && acceptsSpecificType(ALL_IMAGE_TYPES); 213 } 214 captureCamcorder()215 private boolean captureCamcorder() { 216 return mCapture && acceptsSpecificType(ALL_VIDEO_TYPES); 217 } 218 captureMicrophone()219 private boolean captureMicrophone() { 220 return mCapture && acceptsSpecificType(ALL_AUDIO_TYPES); 221 } 222 acceptSpecificType(String accept)223 private boolean acceptSpecificType(String accept) { 224 for (String type : mFileTypes) { 225 if (type.startsWith(accept)) { 226 return true; 227 } 228 } 229 return false; 230 } 231 232 private class GetDisplayNameTask extends AsyncTask<Uri, Void, String[]> { 233 String[] mFilePaths; 234 final ContentResolver mContentResolver; 235 final boolean mIsMultiple; 236 GetDisplayNameTask(ContentResolver contentResolver, boolean isMultiple)237 public GetDisplayNameTask(ContentResolver contentResolver, boolean isMultiple) { 238 mContentResolver = contentResolver; 239 mIsMultiple = isMultiple; 240 } 241 242 @Override doInBackground(Uri...uris)243 protected String[] doInBackground(Uri...uris) { 244 mFilePaths = new String[uris.length]; 245 String[] displayNames = new String[uris.length]; 246 for (int i = 0; i < uris.length; i++) { 247 mFilePaths[i] = uris[i].toString(); 248 displayNames[i] = ContentUriUtils.getDisplayName( 249 uris[i], mContentResolver, MediaStore.MediaColumns.DISPLAY_NAME); 250 } 251 return displayNames; 252 } 253 254 @Override onPostExecute(String[] result)255 protected void onPostExecute(String[] result) { 256 if (!mIsMultiple) { 257 nativeOnFileSelected(mNativeSelectFileDialog, mFilePaths[0], result[0]); 258 } 259 } 260 } 261 262 @CalledByNative create(long nativeSelectFileDialog)263 private static SelectFileDialog create(long nativeSelectFileDialog) { 264 return new SelectFileDialog(nativeSelectFileDialog); 265 } 266 nativeOnFileSelected(long nativeSelectFileDialogImpl, String filePath, String displayName)267 private native void nativeOnFileSelected(long nativeSelectFileDialogImpl, 268 String filePath, String displayName); nativeOnFileNotSelected(long nativeSelectFileDialogImpl)269 private native void nativeOnFileNotSelected(long nativeSelectFileDialogImpl); 270 } 271