summaryrefslogtreecommitdiffstats
path: root/ui/android/java
diff options
context:
space:
mode:
authoraurimas@chromium.org <aurimas@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-09-18 21:47:56 +0000
committeraurimas@chromium.org <aurimas@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-09-18 21:47:56 +0000
commit2d02a200c2f156c8e80b2396f80b87756cdc425b (patch)
tree0faeb5881c32ce8bae5be35e6c042b7eeaf8b48c /ui/android/java
parent41a8da1d1c970c0c7b7076beccb549592b4f60f8 (diff)
downloadchromium_src-2d02a200c2f156c8e80b2396f80b87756cdc425b.zip
chromium_src-2d02a200c2f156c8e80b2396f80b87756cdc425b.tar.gz
chromium_src-2d02a200c2f156c8e80b2396f80b87756cdc425b.tar.bz2
Upstreaming SelectFileDialog for Android
Upstreaming the Select File Dialog and its dependencies needed for Chrome on Android BUG=116131 Review URL: https://chromiumcodereview.appspot.com/10916160 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@157424 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui/android/java')
-rw-r--r--ui/android/java/src/org/chromium/ui/SelectFileDialog.java257
-rw-r--r--ui/android/java/src/org/chromium/ui/gfx/NativeWindow.java226
2 files changed, 483 insertions, 0 deletions
diff --git a/ui/android/java/src/org/chromium/ui/SelectFileDialog.java b/ui/android/java/src/org/chromium/ui/SelectFileDialog.java
new file mode 100644
index 0000000..a897300
--- /dev/null
+++ b/ui/android/java/src/org/chromium/ui/SelectFileDialog.java
@@ -0,0 +1,257 @@
+// Copyright (c) 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;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.chromium.base.CalledByNative;
+import org.chromium.base.JNINamespace;
+import org.chromium.ui.gfx.NativeWindow;
+
+/**
+ * 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 NativeWindow.IntentCallback{
+ // TODO (aurimas): Swap these constants with AppResources when it gets moved to base to support
+ // internationalization.
+ private static final String LOW_MEMORY_ERROR =
+ "Unable to complete previous operation due to low memory";
+ private static final String OPENING_FILE_ERROR = "Failed to open selected file";
+
+ 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_CAMERA = "camera";
+ private static final String CAPTURE_CAMCORDER = "camcorder";
+ private static final String CAPTURE_MICROPHONE = "microphone";
+ private static final String CAPTURE_FILESYSTEM = "filesystem";
+ private static final String CAPTURE_IMAGE_DIRECTORY = "browser-photos";
+
+ private final int mNativeSelectFileDialog;
+ private List<String> mFileTypes;
+ private String mCapture; // May be null if no capture parameter was set.
+ private Uri mCameraOutputUri;
+
+ private SelectFileDialog(int 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 window The NativeWindow that can show intents
+ */
+ @CalledByNative
+ private void selectFile(String[] fileTypes, String capture, NativeWindow 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);
+ mCameraOutputUri = Uri.fromFile(getFileForImageCapture());
+ camera.putExtra(MediaStore.EXTRA_OUTPUT, mCameraOutputUri);
+ Intent camcorder = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+ Intent soundRecorder = new Intent(
+ MediaStore.Audio.Media.RECORD_SOUND_ACTION);
+
+ // Quick check - if a capture parameter other than filesystem (the default) is specified 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, LOW_MEMORY_ERROR)) return;
+ } else if (captureCamcorder()) {
+ if (window.showIntent(camcorder, this, LOW_MEMORY_ERROR)) return;
+ } else if (captureMicrophone()) {
+ if (window.showIntent(soundRecorder, this, LOW_MEMORY_ERROR)) return;
+ }
+
+ Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT);
+ getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);
+ 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("image/*");
+ } else if (shouldShowVideoTypes()) {
+ extraIntents.add(camcorder);
+ getContentIntent.setType("video/*");
+ } else if (shouldShowAudioTypes()) {
+ extraIntents.add(soundRecorder);
+ getContentIntent.setType("audio/*");
+ }
+ }
+
+ if (extraIntents.isEmpty()) {
+ // We couldn't resolve an accept type, so fallback to a generic chooser.
+ getContentIntent.setType("*/*");
+ 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, LOW_MEMORY_ERROR)) onFileNotSelected();
+ }
+
+ /**
+ * Get a file for the image capture in the CAPTURE_IMAGE_DIRECTORY directory.
+ */
+ private File getFileForImageCapture() {
+ File externalDataDir = Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DCIM);
+ File cameraDataDir = new File(externalDataDir.getAbsolutePath() +
+ File.separator + CAPTURE_IMAGE_DIRECTORY);
+ if (!cameraDataDir.exists() && !cameraDataDir.mkdirs()) {
+ cameraDataDir = externalDataDir;
+ }
+ File photoFile = new File(cameraDataDir.getAbsolutePath() +
+ File.separator + System.currentTimeMillis() + ".jpg");
+ 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.
+ */
+ @Override
+ public void onIntentCompleted(NativeWindow window, int resultCode,
+ ContentResolver contentResolver, Intent results) {
+ if (resultCode != Activity.RESULT_OK) {
+ onFileNotSelected();
+ return;
+ }
+ boolean success = false;
+ 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.getPath());
+ success = true;
+
+ // 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.getActivity().sendBroadcast(new Intent(
+ Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mCameraOutputUri));
+ } else {
+ // We get back a content:// URI from the system if the user picked a file from the
+ // gallery. The ContentView has functionality that will convert that content:// URI to
+ // a file path on disk that Chromium understands.
+ Cursor c = contentResolver.query(results.getData(),
+ new String[] { MediaStore.MediaColumns.DATA }, null, null, null);
+ if (c != null) {
+ if (c.getCount() == 1) {
+ c.moveToFirst();
+ String path = c.getString(0);
+ if (path != null) {
+ // Not all providers support the MediaStore.DATA column. For example,
+ // Gallery3D (com.android.gallery3d.provider) does not support it for
+ // Picasa Web Album images.
+ nativeOnFileSelected(mNativeSelectFileDialog, path);
+ success = true;
+ }
+ }
+ c.close();
+ }
+ }
+ if (!success) {
+ onFileNotSelected();
+ window.showError(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 captureCamera() {
+ return shouldShowImageTypes() && mCapture != null && mCapture.startsWith(CAPTURE_CAMERA);
+ }
+
+ private boolean captureCamcorder() {
+ return shouldShowVideoTypes() && mCapture != null &&
+ mCapture.startsWith(CAPTURE_CAMCORDER);
+ }
+
+ private boolean captureMicrophone() {
+ return shouldShowAudioTypes() && mCapture != null &&
+ mCapture.startsWith(CAPTURE_MICROPHONE);
+ }
+
+ private boolean captureFilesystem() {
+ return mCapture != null && mCapture.startsWith(CAPTURE_FILESYSTEM);
+ }
+
+ private boolean acceptSpecificType(String accept) {
+ for (String type : mFileTypes) {
+ if (type.startsWith(accept)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @CalledByNative
+ private static SelectFileDialog create(int nativeSelectFileDialog) {
+ return new SelectFileDialog(nativeSelectFileDialog);
+ }
+
+ private native void nativeOnFileSelected(int nativeSelectFileDialogImpl,
+ String filePath);
+ private native void nativeOnFileNotSelected(int nativeSelectFileDialogImpl);
+}
diff --git a/ui/android/java/src/org/chromium/ui/gfx/NativeWindow.java b/ui/android/java/src/org/chromium/ui/gfx/NativeWindow.java
new file mode 100644
index 0000000..cc0198c
--- /dev/null
+++ b/ui/android/java/src/org/chromium/ui/gfx/NativeWindow.java
@@ -0,0 +1,226 @@
+// Copyright (c) 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.gfx;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.util.SparseArray;
+import android.widget.Toast;
+
+import org.chromium.base.JNINamespace;
+
+import java.util.HashMap;
+
+/**
+ * The window that has access to the main activity and is able to create and receive intents,
+ * and show error messages.
+ */
+@JNINamespace("ui")
+public class NativeWindow {
+
+ // Constants used for intent request code bounding.
+ private static final int REQUEST_CODE_PREFIX = 1000;
+ private static final int REQUEST_CODE_RANGE_SIZE = 100;
+ // A string used as a key to store intent errors in a bundle
+ static final String WINDOW_CALLBACK_ERRORS = "window_callback_errors";
+
+ // Native pointer to the c++ WindowAndroid object.
+ private int mNativeWindowAndroid = 0;
+ private int mNextRequestCode = 0;
+ protected Activity mActivity;
+ private SparseArray<IntentCallback> mOutstandingIntents;
+ private HashMap<Integer, String> mIntentErrors;
+
+ /**
+ * An interface that intent callback objects have to implement.
+ */
+ public interface IntentCallback {
+ /**
+ * Handles the data returned by the requested intent.
+ * @param window A window reference.
+ * @param resultCode Result code of the requested intent.
+ * @param contentResolver An instance of ContentResolver class for accessing returned data.
+ * @param data The data returned by the intent.
+ */
+ public void onIntentCompleted(NativeWindow window, int resultCode,
+ ContentResolver contentResolver, Intent data);
+ }
+
+ /**
+ * Constructs a Window object, saves a reference to the main activity, and initializes the
+ * outstanding intent map. NativeWindowAndroid gets lazily loaded on getNativePointer().
+ * @param activity The main application activity.
+ */
+ public NativeWindow(Activity activity) {
+ mActivity = activity;
+ mOutstandingIntents = new SparseArray<IntentCallback>();
+ mIntentErrors = new HashMap<Integer, String>();
+ mNativeWindowAndroid = 0;
+ }
+
+ /**
+ * Destroys the c++ WindowAndroid object if one has been created.
+ */
+ public void destroy() {
+ if (mNativeWindowAndroid != 0) {
+ nativeDestroy(mNativeWindowAndroid);
+ mNativeWindowAndroid = 0;
+ }
+ }
+
+ /**
+ * Shows an intent and returns the results to the callback object.
+ * @param intent The intent that needs to be showed.
+ * @param callback The object that will receive the results for the intent.
+ * @return Whether the intent was shown.
+ */
+ public boolean showIntent(Intent intent, IntentCallback callback) {
+ return showIntent(intent, callback, null);
+ }
+
+ /**
+ * Shows an intent and returns the results to the callback object.
+ * @param intent The intent that needs to be showed.
+ * @param callback The object that will receive the results for the intent.
+ * @param errorId The id of the error string to be show if activity is paused before intent
+ * results.
+ * @return Whether the intent was shown.
+ */
+ public boolean showIntent(Intent intent, IntentCallback callback, int errorId) {
+ String error = null;
+ try {
+ error = mActivity.getString(errorId);
+ } catch (Resources.NotFoundException e) { }
+ return showIntent(intent, callback, error);
+ }
+
+ /**
+ * Shows an intent and returns the results to the callback object.
+ * @param intent The intent that needs to be showed.
+ * @param callback The object that will receive the results for the intent.
+ * @param error The error string to be show if activity is paused before intent results.
+ * @return Whether the intent was shown.
+ */
+ public boolean showIntent(Intent intent, IntentCallback callback, String error) {
+ int requestCode = REQUEST_CODE_PREFIX + mNextRequestCode;
+ mNextRequestCode = (mNextRequestCode + 1) % REQUEST_CODE_RANGE_SIZE;
+
+ try {
+ mActivity.startActivityForResult(intent, requestCode);
+ } catch (ActivityNotFoundException e) {
+ return false;
+ }
+
+ mOutstandingIntents.put(requestCode, callback);
+ if (error != null) mIntentErrors.put(requestCode, error);
+
+ return true;
+ }
+
+ /**
+ * Saves the error messages that should be shown if any pending intents would return
+ * after the application has been put onPause.
+ * @param bundle The bundle to save the information in onPause
+ */
+ public void saveInstanceState(Bundle bundle) {
+ bundle.putSerializable(WINDOW_CALLBACK_ERRORS, mIntentErrors);
+ }
+
+ /**
+ * Restores the error messages that should be shown if any pending intents would return
+ * after the application has been put onPause.
+ * @param bundle The bundle to restore the information from onResume
+ */
+ public void restoreInstanceState(Bundle bundle) {
+ if (bundle == null) return;
+
+ Object errors = bundle.getSerializable(WINDOW_CALLBACK_ERRORS);
+ if (errors instanceof HashMap) {
+ @SuppressWarnings("unchecked")
+ HashMap<Integer, String> intentErrors = (HashMap<Integer, String>) errors;
+ mIntentErrors = intentErrors;
+ }
+ }
+
+ /**
+ * Displays an error message with a provided error message string.
+ * @param error The error message string to be displayed.
+ */
+ public void showError(String error) {
+ if (error != null) Toast.makeText(mActivity, error, Toast.LENGTH_SHORT).show();
+ }
+
+ /**
+ * Displays an error message with a provided error message string id.
+ * @param errorId The string id of the error message string to be displayed.
+ */
+ public void showError(int errorId) {
+ String error = null;
+ try {
+ error = mActivity.getString(errorId);
+ } catch (Resources.NotFoundException e) { }
+ showError(error);
+ }
+
+ /**
+ * Displays an error message for a nonexistent callback.
+ * @param error The error message string to be displayed.
+ */
+ protected void showCallbackNonExistentError(String error) {
+ showError(error);
+ }
+
+ /**
+ * @return The main application activity.
+ */
+ public Activity getActivity() {
+ return mActivity;
+ }
+
+ /**
+ * Responds to the intent result if the intent was created by the native window.
+ * @param requestCode Request code of the requested intent.
+ * @param resultCode Result code of the requested intent.
+ * @param data The data returned by the intent.
+ * @return Boolean value of whether the intent was started by the native window.
+ */
+ public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
+ IntentCallback callback = mOutstandingIntents.get(requestCode);
+ mOutstandingIntents.delete(requestCode);
+ String errorMessage = mIntentErrors.remove(requestCode);
+
+ if (callback != null) {
+ callback.onIntentCompleted(this, resultCode,
+ mActivity.getContentResolver(), data);
+ return true;
+ } else {
+ if (errorMessage != null) {
+ showCallbackNonExistentError(errorMessage);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a pointer to the c++ AndroidWindow object and calls the initializer if
+ * the object has not been previously initialized.
+ * @return A pointer to the c++ AndroidWindow.
+ */
+ public int getNativePointer() {
+ if (mNativeWindowAndroid == 0) {
+ mNativeWindowAndroid = nativeInit();
+ }
+ return mNativeWindowAndroid;
+ }
+
+ private native int nativeInit();
+ private native void nativeDestroy(int nativeWindowAndroid);
+
+}