diff options
author | aurimas@chromium.org <aurimas@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-09-18 21:47:56 +0000 |
---|---|---|
committer | aurimas@chromium.org <aurimas@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-09-18 21:47:56 +0000 |
commit | 2d02a200c2f156c8e80b2396f80b87756cdc425b (patch) | |
tree | 0faeb5881c32ce8bae5be35e6c042b7eeaf8b48c /ui/android/java | |
parent | 41a8da1d1c970c0c7b7076beccb549592b4f60f8 (diff) | |
download | chromium_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.java | 257 | ||||
-rw-r--r-- | ui/android/java/src/org/chromium/ui/gfx/NativeWindow.java | 226 |
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); + +} |