| // Copyright 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.ui.base; |
| |
| import android.annotation.TargetApi; |
| import android.app.Activity; |
| import android.content.ClipData; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Build; |
| import android.os.Environment; |
| import android.provider.MediaStore; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import org.chromium.base.CalledByNative; |
| import org.chromium.base.ContentUriUtils; |
| import org.chromium.base.JNINamespace; |
| import org.chromium.ui.R; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * A dialog that is triggered from a file input field that allows a user to select a file based on |
| * a set of accepted file types. The path of the selected file is passed to the native dialog. |
| */ |
| @JNINamespace("ui") |
| class SelectFileDialog implements WindowAndroid.IntentCallback { |
| private static final String TAG = "SelectFileDialog"; |
| private static final String IMAGE_TYPE = "image/"; |
| private static final String VIDEO_TYPE = "video/"; |
| private static final String AUDIO_TYPE = "audio/"; |
| private static final String ALL_IMAGE_TYPES = IMAGE_TYPE + "*"; |
| private static final String ALL_VIDEO_TYPES = VIDEO_TYPE + "*"; |
| private static final String ALL_AUDIO_TYPES = AUDIO_TYPE + "*"; |
| private static final String ANY_TYPES = "*/*"; |
| private static final String CAPTURE_IMAGE_DIRECTORY = "browser-photos"; |
| // Keep this variable in sync with the value defined in file_paths.xml. |
| private static final String IMAGE_FILE_PATH = "images"; |
| |
| private final long mNativeSelectFileDialog; |
| private List<String> mFileTypes; |
| private boolean mCapture; |
| private Uri mCameraOutputUri; |
| |
| private SelectFileDialog(long nativeSelectFileDialog) { |
| mNativeSelectFileDialog = nativeSelectFileDialog; |
| } |
| |
| /** |
| * Creates and starts an intent based on the passed fileTypes and capture value. |
| * @param fileTypes MIME types requested (i.e. "image/*") |
| * @param capture The capture value as described in http://www.w3.org/TR/html-media-capture/ |
| * @param multiple Whether it should be possible to select multiple files. |
| * @param window The WindowAndroid that can show intents |
| */ |
| @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) |
| @CalledByNative |
| private void selectFile( |
| String[] fileTypes, boolean capture, boolean multiple, WindowAndroid window) { |
| mFileTypes = new ArrayList<String>(Arrays.asList(fileTypes)); |
| mCapture = capture; |
| |
| Intent chooser = new Intent(Intent.ACTION_CHOOSER); |
| Intent camera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); |
| Context context = window.getApplicationContext(); |
| camera.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | |
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION); |
| try { |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { |
| mCameraOutputUri = ContentUriUtils.getContentUriFromFile( |
| context, getFileForImageCapture(context)); |
| } else { |
| mCameraOutputUri = Uri.fromFile(getFileForImageCapture(context)); |
| } |
| } catch (IOException e) { |
| Log.e(TAG, "Cannot retrieve content uri from file", e); |
| } |
| |
| if (mCameraOutputUri == null) { |
| onFileNotSelected(); |
| return; |
| } |
| |
| camera.putExtra(MediaStore.EXTRA_OUTPUT, mCameraOutputUri); |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { |
| camera.setClipData( |
| ClipData.newUri(context.getContentResolver(), |
| IMAGE_FILE_PATH, mCameraOutputUri)); |
| } |
| Intent camcorder = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); |
| Intent soundRecorder = new Intent( |
| MediaStore.Audio.Media.RECORD_SOUND_ACTION); |
| |
| // Quick check - if the |capture| parameter is set and |fileTypes| has the appropriate MIME |
| // type, we should just launch the appropriate intent. Otherwise build up a chooser based on |
| // the accept type and then display that to the user. |
| if (captureCamera()) { |
| if (window.showIntent(camera, this, R.string.low_memory_error)) return; |
| } else if (captureCamcorder()) { |
| if (window.showIntent(camcorder, this, R.string.low_memory_error)) return; |
| } else if (captureMicrophone()) { |
| if (window.showIntent(soundRecorder, this, R.string.low_memory_error)) return; |
| } |
| |
| Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT); |
| getContentIntent.addCategory(Intent.CATEGORY_OPENABLE); |
| |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && multiple) |
| getContentIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); |
| |
| ArrayList<Intent> extraIntents = new ArrayList<Intent>(); |
| if (!noSpecificType()) { |
| // Create a chooser based on the accept type that was specified in the webpage. Note |
| // that if the web page specified multiple accept types, we will have built a generic |
| // chooser above. |
| if (shouldShowImageTypes()) { |
| extraIntents.add(camera); |
| getContentIntent.setType(ALL_IMAGE_TYPES); |
| } else if (shouldShowVideoTypes()) { |
| extraIntents.add(camcorder); |
| getContentIntent.setType(ALL_VIDEO_TYPES); |
| } else if (shouldShowAudioTypes()) { |
| extraIntents.add(soundRecorder); |
| getContentIntent.setType(ALL_AUDIO_TYPES); |
| } |
| } |
| |
| if (extraIntents.isEmpty()) { |
| // We couldn't resolve an accept type, so fallback to a generic chooser. |
| getContentIntent.setType(ANY_TYPES); |
| extraIntents.add(camera); |
| extraIntents.add(camcorder); |
| extraIntents.add(soundRecorder); |
| } |
| |
| chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, |
| extraIntents.toArray(new Intent[] { })); |
| |
| chooser.putExtra(Intent.EXTRA_INTENT, getContentIntent); |
| |
| if (!window.showIntent(chooser, this, R.string.low_memory_error)) { |
| onFileNotSelected(); |
| } |
| } |
| |
| /** |
| * Get a file for the image capture operation. For devices with JB MR2 or |
| * latter android versions, the file is put under IMAGE_FILE_PATH directory. |
| * For ICS devices, the file is put under CAPTURE_IMAGE_DIRECTORY. |
| * |
| * @param context The application context. |
| * @return file path for the captured image to be stored. |
| */ |
| private File getFileForImageCapture(Context context) throws IOException { |
| File path; |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { |
| path = new File(context.getFilesDir(), IMAGE_FILE_PATH); |
| if (!path.exists() && !path.mkdir()) { |
| throw new IOException("Folder cannot be created."); |
| } |
| } else { |
| File externalDataDir = Environment.getExternalStoragePublicDirectory( |
| Environment.DIRECTORY_DCIM); |
| path = new File(externalDataDir.getAbsolutePath() + |
| File.separator + CAPTURE_IMAGE_DIRECTORY); |
| if (!path.exists() && !path.mkdirs()) { |
| path = externalDataDir; |
| } |
| } |
| File photoFile = File.createTempFile( |
| String.valueOf(System.currentTimeMillis()), ".jpg", path); |
| return photoFile; |
| } |
| |
| /** |
| * Callback method to handle the intent results and pass on the path to the native |
| * SelectFileDialog. |
| * @param window The window that has access to the application activity. |
| * @param resultCode The result code whether the intent returned successfully. |
| * @param contentResolver The content resolver used to extract the path of the selected file. |
| * @param results The results of the requested intent. |
| */ |
| @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) |
| @Override |
| public void onIntentCompleted(WindowAndroid window, int resultCode, |
| ContentResolver contentResolver, Intent results) { |
| if (resultCode != Activity.RESULT_OK) { |
| onFileNotSelected(); |
| return; |
| } |
| |
| if (results == null) { |
| // If we have a successful return but no data, then assume this is the camera returning |
| // the photo that we requested. |
| // If the uri is a file, we need to convert it to the absolute path or otherwise |
| // android cannot handle it correctly on some earlier versions. |
| // http://crbug.com/423338. |
| String path = ContentResolver.SCHEME_FILE.equals(mCameraOutputUri.getScheme()) ? |
| mCameraOutputUri.getPath() : mCameraOutputUri.toString(); |
| nativeOnFileSelected(mNativeSelectFileDialog, path, |
| mCameraOutputUri.getLastPathSegment()); |
| // Broadcast to the media scanner that there's a new photo on the device so it will |
| // show up right away in the gallery (rather than waiting until the next time the media |
| // scanner runs). |
| window.sendBroadcast(new Intent( |
| Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mCameraOutputUri)); |
| return; |
| } |
| |
| // Path for when EXTRA_ALLOW_MULTIPLE Intent extra has been defined. Each of the selected |
| // files will be shared as an entry on the Intent's ClipData. This functionality is only |
| // available in Android JellyBean MR2 and higher. |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && |
| results.getData() == null && |
| results.getClipData() != null) { |
| ClipData clipData = results.getClipData(); |
| |
| int itemCount = clipData.getItemCount(); |
| if (itemCount == 0) { |
| onFileNotSelected(); |
| return; |
| } |
| |
| Uri[] filePathArray = new Uri[itemCount]; |
| for (int i = 0; i < itemCount; ++i) { |
| filePathArray[i] = clipData.getItemAt(i).getUri(); |
| } |
| GetDisplayNameTask task = new GetDisplayNameTask(contentResolver, true); |
| task.execute(filePathArray); |
| return; |
| } |
| |
| if (ContentResolver.SCHEME_FILE.equals(results.getData().getScheme())) { |
| nativeOnFileSelected(mNativeSelectFileDialog, |
| results.getData().getSchemeSpecificPart(), ""); |
| return; |
| } |
| |
| if (ContentResolver.SCHEME_CONTENT.equals(results.getScheme())) { |
| GetDisplayNameTask task = new GetDisplayNameTask(contentResolver, false); |
| task.execute(results.getData()); |
| return; |
| } |
| |
| onFileNotSelected(); |
| window.showError(R.string.opening_file_error); |
| } |
| |
| private void onFileNotSelected() { |
| nativeOnFileNotSelected(mNativeSelectFileDialog); |
| } |
| |
| private boolean noSpecificType() { |
| // We use a single Intent to decide the type of the file chooser we display to the user, |
| // which means we can only give it a single type. If there are multiple accept types |
| // specified, we will fallback to a generic chooser (unless a capture parameter has been |
| // specified, in which case we'll try to satisfy that first. |
| return mFileTypes.size() != 1 || mFileTypes.contains(ANY_TYPES); |
| } |
| |
| private boolean shouldShowTypes(String allTypes, String specificType) { |
| if (noSpecificType() || mFileTypes.contains(allTypes)) return true; |
| return acceptSpecificType(specificType); |
| } |
| |
| private boolean shouldShowImageTypes() { |
| return shouldShowTypes(ALL_IMAGE_TYPES, IMAGE_TYPE); |
| } |
| |
| private boolean shouldShowVideoTypes() { |
| return shouldShowTypes(ALL_VIDEO_TYPES, VIDEO_TYPE); |
| } |
| |
| private boolean shouldShowAudioTypes() { |
| return shouldShowTypes(ALL_AUDIO_TYPES, AUDIO_TYPE); |
| } |
| |
| private boolean acceptsSpecificType(String type) { |
| return mFileTypes.size() == 1 && TextUtils.equals(mFileTypes.get(0), type); |
| } |
| |
| private boolean captureCamera() { |
| return mCapture && acceptsSpecificType(ALL_IMAGE_TYPES); |
| } |
| |
| private boolean captureCamcorder() { |
| return mCapture && acceptsSpecificType(ALL_VIDEO_TYPES); |
| } |
| |
| private boolean captureMicrophone() { |
| return mCapture && acceptsSpecificType(ALL_AUDIO_TYPES); |
| } |
| |
| private boolean acceptSpecificType(String accept) { |
| for (String type : mFileTypes) { |
| if (type.startsWith(accept)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private class GetDisplayNameTask extends AsyncTask<Uri, Void, String[]> { |
| String[] mFilePaths; |
| final ContentResolver mContentResolver; |
| final boolean mIsMultiple; |
| |
| public GetDisplayNameTask(ContentResolver contentResolver, boolean isMultiple) { |
| mContentResolver = contentResolver; |
| mIsMultiple = isMultiple; |
| } |
| |
| @Override |
| protected String[] doInBackground(Uri...uris) { |
| mFilePaths = new String[uris.length]; |
| String[] displayNames = new String[uris.length]; |
| try { |
| for (int i = 0; i < uris.length; i++) { |
| mFilePaths[i] = uris[i].toString(); |
| displayNames[i] = ContentUriUtils.getDisplayName( |
| uris[i], mContentResolver, MediaStore.MediaColumns.DISPLAY_NAME); |
| } |
| } catch (SecurityException e) { |
| // Some third party apps will present themselves as being able |
| // to handle the ACTION_GET_CONTENT intent but then declare themselves |
| // as exported=false (or more often omit the exported keyword in |
| // the manifest which defaults to false after JB). |
| // In those cases trying to access the contents raises a security exception |
| // which we should not crash on. See crbug.com/382367 for details. |
| Log.w(TAG, "Unable to extract results from the content provider"); |
| return null; |
| } |
| |
| return displayNames; |
| } |
| |
| @Override |
| protected void onPostExecute(String[] result) { |
| if (result == null) { |
| onFileNotSelected(); |
| return; |
| } |
| if (mIsMultiple) { |
| nativeOnMultipleFilesSelected(mNativeSelectFileDialog, mFilePaths, result); |
| } else { |
| nativeOnFileSelected(mNativeSelectFileDialog, mFilePaths[0], result[0]); |
| } |
| } |
| } |
| |
| @CalledByNative |
| private static SelectFileDialog create(long nativeSelectFileDialog) { |
| return new SelectFileDialog(nativeSelectFileDialog); |
| } |
| |
| private native void nativeOnFileSelected(long nativeSelectFileDialogImpl, |
| String filePath, String displayName); |
| private native void nativeOnMultipleFilesSelected(long nativeSelectFileDialogImpl, |
| String[] filePathArray, String[] displayNameArray); |
| private native void nativeOnFileNotSelected(long nativeSelectFileDialogImpl); |
| } |