blob: 9f532647ae6a51cbe8cd4a2d8d2dc6c76f76ef66 [file] [log] [blame]
// 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.
nativeOnFileSelected(mNativeSelectFileDialog, mCameraOutputUri.toString(),
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];
for (int i = 0; i < uris.length; i++) {
mFilePaths[i] = uris[i].toString();
displayNames[i] = ContentUriUtils.getDisplayName(
uris[i], mContentResolver, MediaStore.MediaColumns.DISPLAY_NAME);
}
return displayNames;
}
@Override
protected void onPostExecute(String[] result) {
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);
}