Commit c8144b5e authored by Joel Klinghed's avatar Joel Klinghed Committed by Commit Bot

Android: Input type accept filter: handle overlap

Add support for handling an accept filter such as "image/*, image/png"
as if it was just "image/*".

The Android file selector dialog can only set one type filter so
the accept filter contains multiple filters we have to fallback to
a generic filter for the selector.

However, we can detect overlap, for example "image/*, image/png"
can still use "image/*" as a filter without any problems.

"image/png, image/jpeg" is handled as before, only when at least
one generic type is involved is overlap considered.

Bug: 1106651
Change-Id: I9bc74ed18e253b139f7fb7eb456dbc64fba7c3aa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2302751
Commit-Queue: Joel Klinghed <the_jk@opera.com>
Reviewed-by: default avatarFinnur Thorarinsson <finnur@chromium.org>
Cr-Commit-Position: refs/heads/master@{#811182}
parent 331278ef
...@@ -51,13 +51,10 @@ import java.util.concurrent.TimeUnit; ...@@ -51,13 +51,10 @@ import java.util.concurrent.TimeUnit;
@JNINamespace("ui") @JNINamespace("ui")
public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPickerListener { public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPickerListener {
private static final String TAG = "SelectFileDialog"; private static final String TAG = "SelectFileDialog";
private static final String IMAGE_TYPE = "image/"; private static final String IMAGE_TYPE = "image";
private static final String VIDEO_TYPE = "video/"; private static final String VIDEO_TYPE = "video";
private static final String AUDIO_TYPE = "audio/"; private static final String AUDIO_TYPE = "audio";
private static final String ALL_IMAGE_TYPES = IMAGE_TYPE + "*"; private static final String ALL_TYPES = "*/*";
private static final String ALL_VIDEO_TYPES = VIDEO_TYPE + "*";
private static final String ALL_AUDIO_TYPES = AUDIO_TYPE + "*";
private static final String ANY_TYPES = "*/*";
// Duration before temporary camera file is cleaned up, in milliseconds. // Duration before temporary camera file is cleaned up, in milliseconds.
private static final long DURATION_BEFORE_FILE_CLEAN_UP_IN_MILLIS = TimeUnit.HOURS.toMillis(1); private static final long DURATION_BEFORE_FILE_CLEAN_UP_IN_MILLIS = TimeUnit.HOURS.toMillis(1);
...@@ -288,19 +285,18 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick ...@@ -288,19 +285,18 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick
} }
ArrayList<Intent> extraIntents = new ArrayList<Intent>(); ArrayList<Intent> extraIntents = new ArrayList<Intent>();
if (!noSpecificType()) { if (acceptsSingleType()) {
// Create a chooser based on the accept type that was specified in the webpage. Note // If one and only one category of accept type was specified (image, video, etc..),
// that if the web page specified multiple accept types, we will have built a generic // then update the intent to specifically target that request.
// chooser above.
if (shouldShowImageTypes()) { if (shouldShowImageTypes()) {
if (camera != null) extraIntents.add(camera); if (camera != null) extraIntents.add(camera);
getContentIntent.setType(ALL_IMAGE_TYPES); getContentIntent.setType(IMAGE_TYPE + "/*");
} else if (shouldShowVideoTypes()) { } else if (shouldShowVideoTypes()) {
if (camcorder != null) extraIntents.add(camcorder); if (camcorder != null) extraIntents.add(camcorder);
getContentIntent.setType(ALL_VIDEO_TYPES); getContentIntent.setType(VIDEO_TYPE + "/*");
} else if (shouldShowAudioTypes()) { } else if (shouldShowAudioTypes()) {
if (soundRecorder != null) extraIntents.add(soundRecorder); if (soundRecorder != null) extraIntents.add(soundRecorder);
getContentIntent.setType(ALL_AUDIO_TYPES); getContentIntent.setType(AUDIO_TYPE + "/*");
} }
// If any types are specified, then only accept openable files, as coercing // If any types are specified, then only accept openable files, as coercing
...@@ -309,8 +305,8 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick ...@@ -309,8 +305,8 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick
} }
if (extraIntents.isEmpty()) { if (extraIntents.isEmpty()) {
// We couldn't resolve an accept type, so fallback to a generic chooser. // We couldn't resolve a single accept type, so fallback to a generic chooser.
getContentIntent.setType(ANY_TYPES); getContentIntent.setType(ALL_TYPES);
if (camera != null) extraIntents.add(camera); if (camera != null) extraIntents.add(camera);
if (camcorder != null) extraIntents.add(camcorder); if (camcorder != null) extraIntents.add(camcorder);
if (soundRecorder != null) extraIntents.add(soundRecorder); if (soundRecorder != null) extraIntents.add(soundRecorder);
...@@ -641,33 +637,77 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick ...@@ -641,33 +637,77 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick
return SELECT_FILE_DIALOG_SCOPE_IMAGES; return SELECT_FILE_DIALOG_SCOPE_IMAGES;
} }
private boolean noSpecificType() { /**
// We use a single Intent to decide the type of the file chooser we display to the user, * Whether any of the mime-types in mFileTypes accepts the given type.
// which means we can only give it a single type. If there are multiple accept types * If mFileTypes contains ALL_TYPES or is empty every type is accepted so always return true.
// specified, we will fallback to a generic chooser (unless a capture parameter has been * @param superType The superType to look for, such as 'image' or 'video'.
// specified, in which case we'll try to satisfy that first. * Note: This is string-matched on the prefix, so using generics as
return mFileTypes.size() != 1 || mFileTypes.contains(ANY_TYPES); * 'image/*' or '*' will not work.
*/
private boolean acceptsType(String superType) {
if (mFileTypes.isEmpty() || mFileTypes.contains(ALL_TYPES)) return true;
return countAcceptTypesFor(superType) > 0;
} }
private boolean shouldShowTypes(String allTypes, String specificType) { /**
if (noSpecificType() || mFileTypes.contains(allTypes)) return true; * Whether all mime-types in mFileTypes accepts the given type.
return countAcceptTypesFor(specificType) > 0; * @param superType The superType to look for, such as 'image' or 'video'.
* Note: This is string-matched on the prefix, so using generics as
* 'image/*' or '*' will not work.
*/
private boolean acceptsOnlyType(String superType) {
return countAcceptTypesFor(superType) == mFileTypes.size();
} }
private boolean shouldShowImageTypes() { /**
return shouldShowTypes(ALL_IMAGE_TYPES, IMAGE_TYPE); * Checks whether the list of accepted types effectively describes only a single
* type, which might be wildcard. For example:
*
* [image/jpeg] -> true: Only one type is specified.
* [image/jpeg, image/gif] -> false: Contains two distinct types.
* [image/*, image/gif] -> true: image/gif already part of image/*.
*/
@VisibleForTesting
boolean acceptsSingleType() {
// 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.
if (mFileTypes.size() == 1) return !mFileTypes.contains(ALL_TYPES);
// Also return true when a generic subtype "type/*" and one or more specific subtypes
// "type/subtype" are listed but all still have the same supertype.
// Ie. treat ["image/png", "image/*"] as if it said just ["image/*"].
String superTypeFound = null;
boolean foundGenericSubtype = false;
for (String fileType : mFileTypes) {
int slash = fileType.indexOf('/');
if (slash == -1) return false;
String superType = fileType.substring(0, slash);
boolean genericSubtype = fileType.substring(slash + 1).equals("*");
if (superTypeFound == null) {
superTypeFound = superType;
} else if (!superTypeFound.equals(superType)) {
// More than one type.
return false;
}
if (genericSubtype) foundGenericSubtype = true;
}
return foundGenericSubtype;
} }
private boolean shouldShowVideoTypes() { @VisibleForTesting
return shouldShowTypes(ALL_VIDEO_TYPES, VIDEO_TYPE); boolean shouldShowImageTypes() {
return acceptsType(IMAGE_TYPE);
} }
private boolean shouldShowAudioTypes() { @VisibleForTesting
return shouldShowTypes(ALL_AUDIO_TYPES, AUDIO_TYPE); boolean shouldShowVideoTypes() {
return acceptsType(VIDEO_TYPE);
} }
private boolean acceptsSpecificType(String type) { @VisibleForTesting
return mFileTypes.size() == 1 && TextUtils.equals(mFileTypes.get(0), type); boolean shouldShowAudioTypes() {
return acceptsType(AUDIO_TYPE);
} }
/** /**
...@@ -677,7 +717,7 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick ...@@ -677,7 +717,7 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick
* See https://www.w3.org/TR/html-media-capture/ for further description. * See https://www.w3.org/TR/html-media-capture/ for further description.
*/ */
private boolean captureImage() { private boolean captureImage() {
return mCapture && acceptsSpecificType(ALL_IMAGE_TYPES); return mCapture && acceptsOnlyType(IMAGE_TYPE);
} }
/** /**
...@@ -685,7 +725,7 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick ...@@ -685,7 +725,7 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick
* video capture. * video capture.
*/ */
private boolean captureVideo() { private boolean captureVideo() {
return mCapture && acceptsSpecificType(ALL_VIDEO_TYPES); return mCapture && acceptsOnlyType(VIDEO_TYPE);
} }
/** /**
...@@ -693,13 +733,14 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick ...@@ -693,13 +733,14 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick
* audio capture. * audio capture.
*/ */
private boolean captureAudio() { private boolean captureAudio() {
return mCapture && acceptsSpecificType(ALL_AUDIO_TYPES); return mCapture && acceptsOnlyType(AUDIO_TYPE);
} }
private int countAcceptTypesFor(String accept) { private int countAcceptTypesFor(String superType) {
assert superType.indexOf('/') == -1;
int count = 0; int count = 0;
for (String type : mFileTypes) { for (String type : mFileTypes) {
if (type.startsWith(accept)) { if (type.startsWith(superType)) {
count++; count++;
} }
} }
......
...@@ -26,6 +26,7 @@ import java.io.IOException; ...@@ -26,6 +26,7 @@ import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
/** /**
* Tests logic in the SelectFileDialog class. * Tests logic in the SelectFileDialog class.
...@@ -197,4 +198,96 @@ public class SelectFileDialogTest { ...@@ -197,4 +198,96 @@ public class SelectFileDialogTest {
dataDir + "/../" + lastComponent + "/xyz.jpg", null); dataDir + "/../" + lastComponent + "/xyz.jpg", null);
assertFalse(task.doInBackground()); assertFalse(task.doInBackground());
} }
@Test
public void testShowTypes() {
SelectFileDialog selectFileDialog = new SelectFileDialog(0);
selectFileDialog.setFileTypesForTests(Arrays.asList("image/jpeg"));
assertTrue(selectFileDialog.acceptsSingleType());
assertTrue(selectFileDialog.shouldShowImageTypes());
assertFalse(selectFileDialog.shouldShowVideoTypes());
assertFalse(selectFileDialog.shouldShowAudioTypes());
selectFileDialog.setFileTypesForTests(Arrays.asList("image/jpeg", "image/png"));
assertFalse(selectFileDialog.acceptsSingleType());
assertTrue(selectFileDialog.shouldShowImageTypes());
assertFalse(selectFileDialog.shouldShowVideoTypes());
assertFalse(selectFileDialog.shouldShowAudioTypes());
selectFileDialog.setFileTypesForTests(Arrays.asList("image/*", "image/jpeg"));
// Note: image/jpeg is part of image/* so this counts as a single type.
assertTrue(selectFileDialog.acceptsSingleType());
assertTrue(selectFileDialog.shouldShowImageTypes());
assertFalse(selectFileDialog.shouldShowVideoTypes());
assertFalse(selectFileDialog.shouldShowAudioTypes());
selectFileDialog.setFileTypesForTests(Arrays.asList("image/*", "video/mp4"));
assertFalse(selectFileDialog.acceptsSingleType());
assertTrue(selectFileDialog.shouldShowImageTypes());
assertTrue(selectFileDialog.shouldShowVideoTypes());
assertFalse(selectFileDialog.shouldShowAudioTypes());
selectFileDialog.setFileTypesForTests(Arrays.asList("image/jpeg", "video/mp4"));
assertFalse(selectFileDialog.acceptsSingleType());
assertTrue(selectFileDialog.shouldShowImageTypes());
assertTrue(selectFileDialog.shouldShowVideoTypes());
assertFalse(selectFileDialog.shouldShowAudioTypes());
selectFileDialog.setFileTypesForTests(Arrays.asList("video/mp4"));
assertTrue(selectFileDialog.acceptsSingleType());
assertFalse(selectFileDialog.shouldShowImageTypes());
assertTrue(selectFileDialog.shouldShowVideoTypes());
assertFalse(selectFileDialog.shouldShowAudioTypes());
selectFileDialog.setFileTypesForTests(Arrays.asList("video/mp4", "video/*"));
// Note: video/mp4 is part of video/* so this counts as a single type.
assertTrue(selectFileDialog.acceptsSingleType());
assertFalse(selectFileDialog.shouldShowImageTypes());
assertTrue(selectFileDialog.shouldShowVideoTypes());
assertFalse(selectFileDialog.shouldShowAudioTypes());
selectFileDialog.setFileTypesForTests(Arrays.asList("audio/wave", "audio/mpeg", "audio/*"));
// Note: both audio/wave and audio/mpeg are part of audio/* so this counts as a single type.
assertTrue(selectFileDialog.acceptsSingleType());
assertFalse(selectFileDialog.shouldShowImageTypes());
assertFalse(selectFileDialog.shouldShowVideoTypes());
assertTrue(selectFileDialog.shouldShowAudioTypes());
selectFileDialog.setFileTypesForTests(Arrays.asList("audio/wave", "audio/mpeg"));
assertFalse(selectFileDialog.acceptsSingleType());
assertFalse(selectFileDialog.shouldShowImageTypes());
assertFalse(selectFileDialog.shouldShowVideoTypes());
assertTrue(selectFileDialog.shouldShowAudioTypes());
selectFileDialog.setFileTypesForTests(Arrays.asList("*/*"));
assertFalse(selectFileDialog.acceptsSingleType());
assertTrue(selectFileDialog.shouldShowImageTypes());
assertTrue(selectFileDialog.shouldShowVideoTypes());
assertTrue(selectFileDialog.shouldShowAudioTypes());
selectFileDialog.setFileTypesForTests(Collections.emptyList());
assertFalse(selectFileDialog.acceptsSingleType());
assertTrue(selectFileDialog.shouldShowImageTypes());
assertTrue(selectFileDialog.shouldShowVideoTypes());
assertTrue(selectFileDialog.shouldShowAudioTypes());
selectFileDialog.setFileTypesForTests(Arrays.asList("image//png", "image/", "image"));
assertFalse(selectFileDialog.acceptsSingleType());
assertTrue(selectFileDialog.shouldShowImageTypes());
assertFalse(selectFileDialog.shouldShowVideoTypes());
assertFalse(selectFileDialog.shouldShowAudioTypes());
selectFileDialog.setFileTypesForTests(Arrays.asList("/image", "/"));
assertFalse(selectFileDialog.acceptsSingleType());
assertFalse(selectFileDialog.shouldShowImageTypes());
assertFalse(selectFileDialog.shouldShowVideoTypes());
assertFalse(selectFileDialog.shouldShowAudioTypes());
selectFileDialog.setFileTypesForTests(Arrays.asList("/", ""));
assertFalse(selectFileDialog.acceptsSingleType());
assertFalse(selectFileDialog.shouldShowImageTypes());
assertFalse(selectFileDialog.shouldShowVideoTypes());
assertFalse(selectFileDialog.shouldShowAudioTypes());
}
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment