diff --git a/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/FileEnumWorkerTask.java b/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/FileEnumWorkerTask.java new file mode 100644 index 0000000000000..9dd811c423231 --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/FileEnumWorkerTask.java @@ -0,0 +1,127 @@ +// Copyright 2017 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.chrome.browser.photo_picker; + +import android.os.AsyncTask; +import android.os.Environment; + +import org.chromium.base.ThreadUtils; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A worker task to enumerate image files on disk. + */ +class FileEnumWorkerTask extends AsyncTask> { + /** + * An interface to use to communicate back the results to the client. + */ + public interface FilesEnumeratedCallback { + /** + * A callback to define to receive the list of all images on disk. + * @param files The list of images. + */ + void filesEnumeratedCallback(List files); + } + + // The callback to use to communicate the results. + private FilesEnumeratedCallback mCallback; + + // The filter to apply to the list. + private MimeTypeFileFilter mFilter; + + // The camera directory undir DCIM. + private static final String SAMPLE_DCIM_SOURCE_SUB_DIRECTORY = "Camera"; + + /** + * A FileEnumWorkerTask constructor. + * @param callback The callback to use to communicate back the results. + * @param filter The file filter to apply to the list. + */ + public FileEnumWorkerTask(FilesEnumeratedCallback callback, MimeTypeFileFilter filter) { + mCallback = callback; + mFilter = filter; + } + + /** + * Retrieves the DCIM/camera directory. + */ + private File getCameraDirectory() { + return new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), + SAMPLE_DCIM_SOURCE_SUB_DIRECTORY); + } + + /** + * Recursively enumerate files in a directory (and subdirectories) and add them to a list. + * @param directory The parent directory to recursively traverse. + * @param pickerBitmaps The list to add the results to. + * @return True if traversing can continue, false if traversing was aborted and should stop. + */ + private boolean traverseDir(File directory, List pickerBitmaps) { + File[] files = directory.listFiles(mFilter); + if (files == null) return true; + + for (File file : files) { + if (isCancelled()) return false; + + if (file.isDirectory()) { + if (!traverseDir(file, pickerBitmaps)) return false; + } else { + pickerBitmaps.add(new PickerBitmap( + file.getPath(), file.lastModified(), PickerBitmap.PICTURE)); + } + } + + return true; + } + + /** + * Enumerates (in the background) the image files on disk. Called on a non-UI thread + * @param params Ignored, do not use. + * @return A sorted list of images (by last-modified first). + */ + @Override + protected List doInBackground(Void... params) { + assert !ThreadUtils.runningOnUiThread(); + + if (isCancelled()) return null; + + List pickerBitmaps = new ArrayList<>(); + + // TODO(finnur): Figure out which directories to scan and stop hard coding "Camera" above. + File[] sourceDirs = new File[] { + getCameraDirectory(), + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + }; + + for (File directory : sourceDirs) { + if (!traverseDir(directory, pickerBitmaps)) return null; + } + + Collections.sort(pickerBitmaps); + + pickerBitmaps.add(0, new PickerBitmap("", 0, PickerBitmap.GALLERY)); + pickerBitmaps.add(0, new PickerBitmap("", 0, PickerBitmap.CAMERA)); + + return pickerBitmaps; + } + + /** + * Communicates the results back to the client. Called on the UI thread. + * @param files The resulting list of files on disk. + */ + @Override + protected void onPostExecute(List files) { + if (isCancelled()) { + return; + } + + mCallback.filesEnumeratedCallback(files); + } +} diff --git a/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/MimeTypeFileFilter.java b/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/MimeTypeFileFilter.java new file mode 100644 index 0000000000000..2b64157e43671 --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/MimeTypeFileFilter.java @@ -0,0 +1,94 @@ +// Copyright 2017 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.chrome.browser.photo_picker; + +import android.support.annotation.NonNull; +import android.webkit.MimeTypeMap; + +import java.io.File; +import java.io.FileFilter; +import java.util.HashSet; +import java.util.Locale; + +/** + * A file filter for handling extensions mapping to MIME types (such as images/jpeg and images/*). + */ +class MimeTypeFileFilter implements FileFilter { + private HashSet mExtensions = new HashSet<>(); + private HashSet mMimeTypes = new HashSet<>(); + private HashSet mMimeSupertypes = new HashSet<>(); + private MimeTypeMap mMimeTypeMap; + + /** + * Contructs a MimeTypeFileFilter object. + * @param acceptAttr A comma seperated list of MIME types this filter accepts. + * For example: images/gif, video/*. + */ + // TODO(finnur): Convert param to List. + public MimeTypeFileFilter(@NonNull String acceptAttr) { + for (String field : acceptAttr.toLowerCase(Locale.US).split(",")) { + field = field.trim(); + if (field.startsWith(".")) { + mExtensions.add(field.substring(1)); + } else if (field.endsWith("/*")) { + mMimeSupertypes.add(field.substring(0, field.length() - 2)); + } else if (field.contains("/")) { + mMimeTypes.add(field); + } else { + // Throw exception? + } + } + + mMimeTypeMap = MimeTypeMap.getSingleton(); + } + + @Override + public boolean accept(@NonNull File file) { + if (file.isDirectory()) { + return true; + } + + String uri = file.toURI().toString(); + String ext = MimeTypeMap.getFileExtensionFromUrl(uri).toLowerCase(Locale.US); + if (mExtensions.contains(ext)) { + return true; + } + + String mimeType = getMimeTypeFromExtension(ext); + if (mimeType != null) { + if (mMimeTypes.contains(mimeType) + || mMimeSupertypes.contains(getMimeSupertype(mimeType))) { + return true; + } + } + + return false; + } + + private HashSet getAcceptedSupertypes() { + HashSet supertypes = new HashSet<>(); + supertypes.addAll(mMimeSupertypes); + for (String mimeType : mMimeTypes) { + supertypes.add(getMimeSupertype(mimeType)); + } + for (String ext : mExtensions) { + String mimeType = getMimeTypeFromExtension(ext); + if (mimeType != null) { + supertypes.add(getMimeSupertype(mimeType)); + } + } + return supertypes; + } + + private String getMimeTypeFromExtension(@NonNull String ext) { + String mimeType = mMimeTypeMap.getMimeTypeFromExtension(ext); + return (mimeType != null) ? mimeType.toLowerCase(Locale.US) : null; + } + + @NonNull + private String getMimeSupertype(@NonNull String mimeType) { + return mimeType.split("/", 2)[0]; + } +} diff --git a/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/PhotoPickerDialog.java b/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/PhotoPickerDialog.java index ded6cff7a740a..d42df1f38b2f9 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/PhotoPickerDialog.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/PhotoPickerDialog.java @@ -46,4 +46,10 @@ protected void onCreate(Bundle savedInstanceState) { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } + + @Override + public void dismiss() { + super.dismiss(); + mCategoryView.onDialogDismissed(); + } } diff --git a/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/PickerCategoryView.java b/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/PickerCategoryView.java index 17dde3f751d18..6c73972eadb3c 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/PickerCategoryView.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/PickerCategoryView.java @@ -10,7 +10,6 @@ import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar.OnMenuItemClickListener; -import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; @@ -21,14 +20,14 @@ import org.chromium.chrome.browser.widget.selection.SelectionDelegate; import org.chromium.ui.PhotoPickerListener; -import java.util.ArrayList; import java.util.List; /** * A class for keeping track of common data associated with showing photos in * the photo picker, for example the RecyclerView and the bitmap caches. */ -public class PickerCategoryView extends RelativeLayout implements OnMenuItemClickListener { +public class PickerCategoryView extends RelativeLayout + implements FileEnumWorkerTask.FilesEnumeratedCallback, OnMenuItemClickListener { // The dialog that owns us. private PhotoPickerDialog mDialog; @@ -69,21 +68,14 @@ public class PickerCategoryView extends RelativeLayout implements OnMenuItemClic // The size of the bitmaps (equal length for width and height). private int mImageSize; + // A worker task for asynchronously enumerating files off the main thread. + private FileEnumWorkerTask mWorkerTask; + public PickerCategoryView(Context context) { super(context); postConstruction(context); } - public PickerCategoryView(Context context, AttributeSet attrs) { - super(context, attrs); - postConstruction(context); - } - - public PickerCategoryView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - postConstruction(context); - } - /** * A helper function for initializing the PickerCategoryView. * @param context The context to use. @@ -119,6 +111,16 @@ private void postConstruction(Context context) { prepareBitmaps(); } + /** + * Cancels any outstanding requests. + */ + public void onDialogDismissed() { + if (mWorkerTask != null) { + mWorkerTask.cancel(true); + mWorkerTask = null; + } + } + /** * Initializes the PickerCategoryView object. * @param dialog The dialog showing us. @@ -134,6 +136,16 @@ public void initialize( mListener = listener; } + // FileEnumWorkerTask.FilesEnumeratedCallback: + + @Override + public void filesEnumeratedCallback(List files) { + mPickerBitmaps = files; + if (files != null && files.size() > 0) { + mPickerAdapter.notifyDataSetChanged(); + } + } + // OnMenuItemClickListener: @Override @@ -203,15 +215,12 @@ private void calculateGridMetrics(int width) { * Prepares bitmaps for loading. */ private void prepareBitmaps() { - // TODO(finnur): Use worker thread to fetch bitmaps instead of hard-coding. - mPickerBitmaps = new ArrayList<>(); - mPickerBitmaps.add(0, new PickerBitmap("", 0, PickerBitmap.GALLERY)); - mPickerBitmaps.add(0, new PickerBitmap("", 0, PickerBitmap.CAMERA)); - mPickerBitmaps.add(new PickerBitmap("foo/bar1.jpg", 1, PickerBitmap.PICTURE)); - mPickerBitmaps.add(new PickerBitmap("foo/bar2.jpg", 2, PickerBitmap.PICTURE)); - mPickerBitmaps.add(new PickerBitmap("foo/bar3.jpg", 3, PickerBitmap.PICTURE)); - mPickerBitmaps.add(new PickerBitmap("foo/bar4.jpg", 4, PickerBitmap.PICTURE)); - mPickerAdapter.notifyDataSetChanged(); + if (mWorkerTask != null) { + mWorkerTask.cancel(true); + } + + mWorkerTask = new FileEnumWorkerTask(this, new MimeTypeFileFilter("image/*")); + mWorkerTask.execute(); } /** diff --git a/chrome/android/java_sources.gni b/chrome/android/java_sources.gni index f113f6b0ea576..b74c28bcec841 100644 --- a/chrome/android/java_sources.gni +++ b/chrome/android/java_sources.gni @@ -771,6 +771,8 @@ chrome_java_sources = [ "java/src/org/chromium/chrome/browser/permissions/PermissionDialogController.java", "java/src/org/chromium/chrome/browser/permissions/PermissionDialogDelegate.java", "java/src/org/chromium/chrome/browser/physicalweb/BitmapHttpRequest.java", + "java/src/org/chromium/chrome/browser/photo_picker/FileEnumWorkerTask.java", + "java/src/org/chromium/chrome/browser/photo_picker/MimeTypeFileFilter.java", "java/src/org/chromium/chrome/browser/photo_picker/PhotoPickerDialog.java", "java/src/org/chromium/chrome/browser/photo_picker/PhotoPickerToolbar.java", "java/src/org/chromium/chrome/browser/photo_picker/PickerAdapter.java",