Bug 1674428 - Part 1. Implement folder upload picker API. r=geckoview-reviewers,owlish,ohall
Differential Revision: https://phabricator.services.mozilla.com/D227912
This commit is contained in:
@@ -1464,6 +1464,7 @@ package org.mozilla.geckoview {
|
|||||||
method @Nullable @UiThread default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onCreditCardSelect(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.CreditCardSelectOption>);
|
method @Nullable @UiThread default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onCreditCardSelect(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.CreditCardSelectOption>);
|
||||||
method @Nullable @UiThread default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onDateTimePrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.DateTimePrompt);
|
method @Nullable @UiThread default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onDateTimePrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.DateTimePrompt);
|
||||||
method @Nullable @UiThread default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onFilePrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.FilePrompt);
|
method @Nullable @UiThread default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onFilePrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.FilePrompt);
|
||||||
|
method @Nullable @UiThread default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onFolderUploadPrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.FolderUploadPrompt);
|
||||||
method @Nullable @UiThread default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onLoginSave(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.LoginSaveOption>);
|
method @Nullable @UiThread default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onLoginSave(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.LoginSaveOption>);
|
||||||
method @Nullable @UiThread default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onLoginSelect(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.LoginSelectOption>);
|
method @Nullable @UiThread default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onLoginSelect(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.LoginSelectOption>);
|
||||||
method @Nullable @UiThread default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onPopupPrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.PopupPrompt);
|
method @Nullable @UiThread default public GeckoResult<GeckoSession.PromptDelegate.PromptResponse> onPopupPrompt(@NonNull GeckoSession, @NonNull GeckoSession.PromptDelegate.PopupPrompt);
|
||||||
@@ -1643,10 +1644,17 @@ package org.mozilla.geckoview {
|
|||||||
|
|
||||||
public static class GeckoSession.PromptDelegate.FilePrompt.Type {
|
public static class GeckoSession.PromptDelegate.FilePrompt.Type {
|
||||||
ctor protected Type();
|
ctor protected Type();
|
||||||
|
field public static final int FOLDER = 3;
|
||||||
field public static final int MULTIPLE = 2;
|
field public static final int MULTIPLE = 2;
|
||||||
field public static final int SINGLE = 1;
|
field public static final int SINGLE = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class GeckoSession.PromptDelegate.FolderUploadPrompt extends GeckoSession.PromptDelegate.BasePrompt {
|
||||||
|
ctor protected FolderUploadPrompt(@NonNull String, @Nullable String, @NonNull GeckoSession.PromptDelegate.BasePrompt.Observer);
|
||||||
|
method @NonNull @UiThread public GeckoSession.PromptDelegate.PromptResponse confirm(@Nullable AllowOrDeny);
|
||||||
|
field @Nullable public final String directoryName;
|
||||||
|
}
|
||||||
|
|
||||||
public static final class GeckoSession.PromptDelegate.IdentityCredential {
|
public static final class GeckoSession.PromptDelegate.IdentityCredential {
|
||||||
ctor public IdentityCredential();
|
ctor public IdentityCredential();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
accept="image/*,.pdf"
|
accept="image/*,.pdf"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<input type="file" id="direxample" webkitdirectory />
|
||||||
|
|
||||||
<datalist id="colorlist">
|
<datalist id="colorlist">
|
||||||
<option>#000000</option>
|
<option>#000000</option>
|
||||||
<option>#808080</option>
|
<option>#808080</option>
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
|
|
||||||
package org.mozilla.geckoview.test
|
package org.mozilla.geckoview.test
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import org.hamcrest.Matchers.* // ktlint-disable no-wildcard-imports
|
import org.hamcrest.Matchers.* // ktlint-disable no-wildcard-imports
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -1102,6 +1104,7 @@ class PromptDelegateTest : BaseSessionTest(
|
|||||||
assertThat("First accept attribute should match", "image/*", equalTo(prompt.mimeTypes?.get(0)))
|
assertThat("First accept attribute should match", "image/*", equalTo(prompt.mimeTypes?.get(0)))
|
||||||
assertThat("Second accept attribute should match", ".pdf", equalTo(prompt.mimeTypes?.get(1)))
|
assertThat("Second accept attribute should match", ".pdf", equalTo(prompt.mimeTypes?.get(1)))
|
||||||
assertThat("Capture attribute should match", PromptDelegate.FilePrompt.Capture.USER, equalTo(prompt.capture))
|
assertThat("Capture attribute should match", PromptDelegate.FilePrompt.Capture.USER, equalTo(prompt.capture))
|
||||||
|
assertThat("Type should match", prompt.type, equalTo(PromptDelegate.FilePrompt.Type.SINGLE))
|
||||||
return GeckoResult.fromValue(prompt.dismiss())
|
return GeckoResult.fromValue(prompt.dismiss())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1131,6 +1134,44 @@ class PromptDelegateTest : BaseSessionTest(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WithDisplay(width = 100, height = 100)
|
||||||
|
@Test
|
||||||
|
fun directoryTest() {
|
||||||
|
sessionRule.setPrefsUntilTestEnd(
|
||||||
|
mapOf(
|
||||||
|
"dom.disable_open_during_load" to false,
|
||||||
|
"dom.webkitBlink.dirPicker.enabled" to true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
mainSession.loadTestPath(PROMPT_HTML_PATH)
|
||||||
|
mainSession.waitForPageStop()
|
||||||
|
|
||||||
|
sessionRule.delegateUntilTestEnd(object : PromptDelegate {
|
||||||
|
@AssertCalled(count = 1)
|
||||||
|
override fun onFilePrompt(session: GeckoSession, prompt: PromptDelegate.FilePrompt): GeckoResult<PromptDelegate.PromptResponse> {
|
||||||
|
assertThat("Type should match", prompt.type, equalTo(PromptDelegate.FilePrompt.Type.FOLDER))
|
||||||
|
return GeckoResult.fromValue(
|
||||||
|
prompt.confirm(
|
||||||
|
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
|
Uri.parse("file:///storage/emulated/0/Download"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mainSession.evaluateJS("document.addEventListener('click', () => document.getElementById('direxample').click(), { once: true });")
|
||||||
|
mainSession.synthesizeTap(1, 1)
|
||||||
|
|
||||||
|
sessionRule.waitUntilCalled(object : PromptDelegate {
|
||||||
|
@AssertCalled(count = 1)
|
||||||
|
override fun onFolderUploadPrompt(session: GeckoSession, prompt: PromptDelegate.FolderUploadPrompt): GeckoResult<PromptDelegate.PromptResponse>? {
|
||||||
|
assertThat("directoryName should match", prompt.directoryName, equalTo("Download"))
|
||||||
|
return GeckoResult.fromValue(prompt.confirm(AllowOrDeny.ALLOW))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@Test fun shareTextSucceeds() {
|
@Test fun shareTextSucceeds() {
|
||||||
sessionRule.setPrefsUntilTestEnd(mapOf("dom.webshare.requireinteraction" to false))
|
sessionRule.setPrefsUntilTestEnd(mapOf("dom.webshare.requireinteraction" to false))
|
||||||
mainSession.loadTestPath(HELLO_HTML_PATH)
|
mainSession.loadTestPath(HELLO_HTML_PATH)
|
||||||
|
|||||||
@@ -6,13 +6,26 @@
|
|||||||
|
|
||||||
package org.mozilla.gecko.util;
|
package org.mozilla.gecko.util;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
import android.util.Log;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
/** Utilities for Intents. */
|
/** Utilities for Intents. */
|
||||||
public class IntentUtils {
|
public class IntentUtils {
|
||||||
|
private static final String LOGTAG = "IntentUtils";
|
||||||
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
|
private static final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY =
|
||||||
|
"com.android.externalstorage.documents";
|
||||||
|
|
||||||
private IntentUtils() {}
|
private IntentUtils() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,4 +130,90 @@ public class IntentUtils {
|
|||||||
private static void nullIntentSelector(final Intent intent) {
|
private static void nullIntentSelector(final Intent intent) {
|
||||||
intent.setSelector(null);
|
intent.setSelector(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a local path from the Uri that is content schema.
|
||||||
|
*
|
||||||
|
* @param context The context.
|
||||||
|
* @param uri The URI.
|
||||||
|
* @return A local path if resolved. If this cannot resolve URI, return null.
|
||||||
|
*/
|
||||||
|
public static String resolveContentUri(final Context context, final Uri uri) {
|
||||||
|
final ContentResolver cr = context.getContentResolver();
|
||||||
|
try (final Cursor cur =
|
||||||
|
cr.query(
|
||||||
|
uri, new String[] {"_data"}, /* selection */ null, /* args */ null, /* sort */ null)) {
|
||||||
|
final int idx = cur.getColumnIndex("_data");
|
||||||
|
if (idx < 0 || !cur.moveToFirst()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
final String path = cur.getString(idx);
|
||||||
|
if (path != null && !path.isEmpty()) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
} catch (final Exception e) {
|
||||||
|
}
|
||||||
|
} while (cur.moveToNext());
|
||||||
|
} catch (final UnsupportedOperationException e) {
|
||||||
|
Log.e(LOGTAG, "Failed to query child documents", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.e(LOGTAG, "Failed to resolve uri. uri=" + uri.toString());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a local path from tree Uri.
|
||||||
|
*
|
||||||
|
* @param context The context.
|
||||||
|
* @param uri The uri that @{link DoumentContract#isTreeUri} returns true.
|
||||||
|
* @return A local path if resolved. If this cannot resolve URI, return null.
|
||||||
|
*/
|
||||||
|
public static String resolveTreeUri(final Context context, final Uri uri) {
|
||||||
|
final Uri docDirUri =
|
||||||
|
DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri));
|
||||||
|
return resolveDocumentUri(context, docDirUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a local path from document Uri.
|
||||||
|
*
|
||||||
|
* @param context The context.
|
||||||
|
* @param uri The uri that @{link DoumentContract#isDocumentUri} returns true.
|
||||||
|
* @return A local path if resolved. If this cannot resolve URI, return null.
|
||||||
|
*/
|
||||||
|
public static String resolveDocumentUri(final Context context, final Uri uri) {
|
||||||
|
if (EXTERNAL_STORAGE_PROVIDER_AUTHORITY.equals(uri.getAuthority())) {
|
||||||
|
final String docId = DocumentsContract.getDocumentId(uri);
|
||||||
|
final String[] split = docId.split(":");
|
||||||
|
|
||||||
|
if (split[0].equals("primary")) {
|
||||||
|
// This is the internal storage.
|
||||||
|
final StringBuilder sb =
|
||||||
|
new StringBuilder(Environment.getExternalStorageDirectory().toString());
|
||||||
|
if (split.length > 1) {
|
||||||
|
sb.append("/").append(split[1]);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
// This might be sd card. /storage/xxxx-xxxx/...
|
||||||
|
final StringBuilder sb = new StringBuilder(Environment.getStorageDirectory().toString());
|
||||||
|
sb.append("/").append(split[0]);
|
||||||
|
if (split.length > 1) {
|
||||||
|
sb.append("/").append(split[1]);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.e(LOGTAG, "Failed to resolve uri. uri=" + uri.toString());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ import static org.mozilla.geckoview.GeckoSession.GeckoPrintException.ERROR_NO_PR
|
|||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Matrix;
|
import android.graphics.Matrix;
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
@@ -27,6 +25,7 @@ import android.os.IInterface;
|
|||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -5502,6 +5501,42 @@ public class GeckoSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FolderUploadPrompt represents a prompt shown whenever the browser needs to upload folder data
|
||||||
|
*/
|
||||||
|
class FolderUploadPrompt extends BasePrompt {
|
||||||
|
/** The directory name to confirm folder tries to uploading. */
|
||||||
|
public final @Nullable String directoryName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A constructor for FolderUploadPrompt
|
||||||
|
*
|
||||||
|
* @param id The identification for this prompt.
|
||||||
|
* @param directoryName The directory that is confirmed.
|
||||||
|
* @param observer A callback to notify when the prompt has been completed.
|
||||||
|
*/
|
||||||
|
protected FolderUploadPrompt(
|
||||||
|
@NonNull final String id,
|
||||||
|
@Nullable final String directoryName,
|
||||||
|
@NonNull final Observer observer) {
|
||||||
|
super(id, null, observer);
|
||||||
|
this.directoryName = directoryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirms the prompt.
|
||||||
|
*
|
||||||
|
* @param allowOrDeny whether the browser should allow resubmitting data.
|
||||||
|
* @return A {@link PromptResponse} which can be used to complete the {@link GeckoResult}
|
||||||
|
* associated with this prompt.
|
||||||
|
*/
|
||||||
|
@UiThread
|
||||||
|
public @NonNull PromptResponse confirm(final @Nullable AllowOrDeny allowOrDeny) {
|
||||||
|
ensureResult().putBoolean("allow", allowOrDeny != AllowOrDeny.DENY);
|
||||||
|
return super.confirm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RepostConfirmPrompt represents a prompt shown whenever the browser needs to resubmit POST
|
* RepostConfirmPrompt represents a prompt shown whenever the browser needs to resubmit POST
|
||||||
* data (e.g. due to page refresh).
|
* data (e.g. due to page refresh).
|
||||||
@@ -6372,7 +6407,7 @@ public class GeckoSession {
|
|||||||
*/
|
*/
|
||||||
class FilePrompt extends BasePrompt {
|
class FilePrompt extends BasePrompt {
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef({Type.SINGLE, Type.MULTIPLE})
|
@IntDef({Type.SINGLE, Type.MULTIPLE, Type.FOLDER})
|
||||||
public @interface FileType {}
|
public @interface FileType {}
|
||||||
|
|
||||||
/** Types of file prompts. */
|
/** Types of file prompts. */
|
||||||
@@ -6383,6 +6418,9 @@ public class GeckoSession {
|
|||||||
/** Prompt for multiple files. */
|
/** Prompt for multiple files. */
|
||||||
public static final int MULTIPLE = 2;
|
public static final int MULTIPLE = 2;
|
||||||
|
|
||||||
|
/** Prompt for directory. */
|
||||||
|
public static final int FOLDER = 3;
|
||||||
|
|
||||||
protected Type() {}
|
protected Type() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6458,7 +6496,7 @@ public class GeckoSession {
|
|||||||
@UiThread
|
@UiThread
|
||||||
public @NonNull PromptResponse confirm(
|
public @NonNull PromptResponse confirm(
|
||||||
@NonNull final Context context, @NonNull final Uri[] uris) {
|
@NonNull final Context context, @NonNull final Uri[] uris) {
|
||||||
if (Type.SINGLE == type && (uris == null || uris.length != 1)) {
|
if ((Type.SINGLE == type || Type.FOLDER == type) && (uris == null || uris.length != 1)) {
|
||||||
throw new IllegalArgumentException();
|
throw new IllegalArgumentException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6483,33 +6521,14 @@ public class GeckoSession {
|
|||||||
if ("file".equals(uri.getScheme())) {
|
if ("file".equals(uri.getScheme())) {
|
||||||
return uri.getPath();
|
return uri.getPath();
|
||||||
}
|
}
|
||||||
final ContentResolver cr = context.getContentResolver();
|
if ("content".equals(uri.getScheme())) {
|
||||||
final Cursor cur =
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && DocumentsContract.isTreeUri(uri)) {
|
||||||
cr.query(
|
return IntentUtils.resolveTreeUri(context, uri);
|
||||||
uri,
|
|
||||||
new String[] {"_data"}, /* selection */
|
|
||||||
null,
|
|
||||||
/* args */ null, /* sort */
|
|
||||||
null);
|
|
||||||
if (cur == null) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
try {
|
if (DocumentsContract.isDocumentUri(context, uri)) {
|
||||||
final int idx = cur.getColumnIndex("_data");
|
return IntentUtils.resolveDocumentUri(context, uri);
|
||||||
if (idx < 0 || !cur.moveToFirst()) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
do {
|
return IntentUtils.resolveContentUri(context, uri);
|
||||||
try {
|
|
||||||
final String path = cur.getString(idx);
|
|
||||||
if (path != null && !path.isEmpty()) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
} catch (final Exception e) {
|
|
||||||
}
|
|
||||||
} while (cur.moveToNext());
|
|
||||||
} finally {
|
|
||||||
cur.close();
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -6709,6 +6728,20 @@ public class GeckoSession {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a folder upload prompt.
|
||||||
|
*
|
||||||
|
* @param session GeckoSession that triggered the prompt.
|
||||||
|
* @param prompt The {@link FolderUploadPrompt} that describes the prompt.
|
||||||
|
* @return A {@link GeckoResult} resolving to a {@link PromptResponse} which includes all
|
||||||
|
* necessary information to resolve the prompt.
|
||||||
|
*/
|
||||||
|
@UiThread
|
||||||
|
default @Nullable GeckoResult<PromptResponse> onFolderUploadPrompt(
|
||||||
|
@NonNull final GeckoSession session, @NonNull final FolderUploadPrompt prompt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a text prompt.
|
* Display a text prompt.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import org.mozilla.geckoview.GeckoSession.PromptDelegate.ChoicePrompt;
|
|||||||
import org.mozilla.geckoview.GeckoSession.PromptDelegate.ColorPrompt;
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.ColorPrompt;
|
||||||
import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt;
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt;
|
||||||
import org.mozilla.geckoview.GeckoSession.PromptDelegate.FilePrompt;
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.FilePrompt;
|
||||||
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.FolderUploadPrompt;
|
||||||
import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.AccountSelectorPrompt;
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.AccountSelectorPrompt;
|
||||||
import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.PrivacyPolicyPrompt;
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.PrivacyPolicyPrompt;
|
||||||
import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.ProviderSelectorPrompt;
|
import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.ProviderSelectorPrompt;
|
||||||
@@ -362,6 +363,8 @@ import org.mozilla.geckoview.GeckoSession.PromptDelegate.TextPrompt;
|
|||||||
intMode = FilePrompt.Type.SINGLE;
|
intMode = FilePrompt.Type.SINGLE;
|
||||||
} else if ("multiple".equals(mode)) {
|
} else if ("multiple".equals(mode)) {
|
||||||
intMode = FilePrompt.Type.MULTIPLE;
|
intMode = FilePrompt.Type.MULTIPLE;
|
||||||
|
} else if ("folder".equals(mode)) {
|
||||||
|
intMode = FilePrompt.Type.FOLDER;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -379,6 +382,22 @@ import org.mozilla.geckoview.GeckoSession.PromptDelegate.TextPrompt;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class FolderUploadHandler implements PromptHandler<FolderUploadPrompt> {
|
||||||
|
@Override
|
||||||
|
public FolderUploadPrompt newPrompt(final GeckoBundle info, final Observer observer) {
|
||||||
|
return new FolderUploadPrompt(
|
||||||
|
info.getString("id"), info.getString("directoryName"), observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GeckoResult<PromptResponse> callDelegate(
|
||||||
|
final FolderUploadPrompt prompt,
|
||||||
|
final GeckoSession session,
|
||||||
|
final PromptDelegate delegate) {
|
||||||
|
return delegate.onFolderUploadPrompt(session, prompt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class PopupHandler implements PromptHandler<PopupPrompt> {
|
private static final class PopupHandler implements PromptHandler<PopupPrompt> {
|
||||||
@Override
|
@Override
|
||||||
public PopupPrompt newPrompt(final GeckoBundle info, final Observer observer) {
|
public PopupPrompt newPrompt(final GeckoBundle info, final Observer observer) {
|
||||||
@@ -727,6 +746,7 @@ import org.mozilla.geckoview.GeckoSession.PromptDelegate.TextPrompt;
|
|||||||
sPromptHandlers.register(new ColorHandler(), "color");
|
sPromptHandlers.register(new ColorHandler(), "color");
|
||||||
sPromptHandlers.register(new DateTimeHandler(), "datetime");
|
sPromptHandlers.register(new DateTimeHandler(), "datetime");
|
||||||
sPromptHandlers.register(new FileHandler(), "file");
|
sPromptHandlers.register(new FileHandler(), "file");
|
||||||
|
sPromptHandlers.register(new FolderUploadHandler(), "folderUpload");
|
||||||
sPromptHandlers.register(new PopupHandler(), "popup");
|
sPromptHandlers.register(new PopupHandler(), "popup");
|
||||||
sPromptHandlers.register(new RepostHandler(), "repost");
|
sPromptHandlers.register(new RepostHandler(), "repost");
|
||||||
sPromptHandlers.register(new ShareHandler(), "share");
|
sPromptHandlers.register(new ShareHandler(), "share");
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ exclude: true
|
|||||||
|
|
||||||
⚠️ breaking change and deprecation notices
|
⚠️ breaking change and deprecation notices
|
||||||
|
|
||||||
|
## v138
|
||||||
|
- Added [`GeckoSession.Loader.originalInput`][138.1] option, which allows passing through the original user address bar input
|
||||||
|
- Added [`PromptDelegate.FilePrompt.Type.FOLDER`][138.2] to show directory picker.
|
||||||
|
([bug 1674428]({{bugzilla}}1674428))
|
||||||
|
- Added [`onFolderUploadPrompt`][138.3] and [`PromptDelegate.FolderUploadPrompt`][138.4] to confirm folder upload.
|
||||||
|
([bug 1674428]({{bugzilla}}1674428))
|
||||||
|
|
||||||
|
[138.1]: {{javadoc_uri}}/GeckoSession.Loader.html#originalInput(java.lang.String)
|
||||||
|
[138.2]: {{javadoc_uri}}/GeckoSession.PromptDelegate.FilePrompt.Type.html#FOLDER
|
||||||
|
[138.3]: {{javadoc_uri}}/GeckoSession.PromptDelegate.html#onFolderUploadPrompt(org.mozilla.geckoview.GeckoSession,org.mozilla.geckoview.GeckoSession.PromptDelegate.FolderUploadPrompt)
|
||||||
|
[138.4]: {{javadoc_uri}}/GeckoSession.PromptDelegate.FolderUploadPrompt.html
|
||||||
|
|
||||||
## v137
|
## v137
|
||||||
- ⚠️ [`GeckoSession.requestAnalysis`][118.4], [`GeckoSession.requestCreateAnalysis`][122.2], [`GeckoSession.requestAnalysisStatus`][137.1], [`GeckoSession.sendPlacementAttributionEvent`][123.3], [`GeckoSession.pollForAnalysisCompleted`][137.2], [`GeckoSession.sendClickAttributionEvent`][121.4], [`GeckoSession.sendImpressionAttributionEvent`][121.5], [`GeckoSession.sendPlacementAttributionEvent`][123.3], [`GeckoSession.requestRecommendations`][118.5], [`GeckoSession.reportBackInStock`][122.1], `AnalysisStatusResponse`, [`ReviewAnalysis`][120.2] and [`Recommendation`][120.3] are deprecated, and it will be deleted in version 139 see https://bugzilla.mozilla.org/show_bug.cgi?id=1941470.
|
- ⚠️ [`GeckoSession.requestAnalysis`][118.4], [`GeckoSession.requestCreateAnalysis`][122.2], [`GeckoSession.requestAnalysisStatus`][137.1], [`GeckoSession.sendPlacementAttributionEvent`][123.3], [`GeckoSession.pollForAnalysisCompleted`][137.2], [`GeckoSession.sendClickAttributionEvent`][121.4], [`GeckoSession.sendImpressionAttributionEvent`][121.5], [`GeckoSession.sendPlacementAttributionEvent`][123.3], [`GeckoSession.requestRecommendations`][118.5], [`GeckoSession.reportBackInStock`][122.1], `AnalysisStatusResponse`, [`ReviewAnalysis`][120.2] and [`Recommendation`][120.3] are deprecated, and it will be deleted in version 139 see https://bugzilla.mozilla.org/show_bug.cgi?id=1941470.
|
||||||
- Added support for controlling `network.trr.default_provider_uri` via [`GeckoRuntimeSettings.setDefaultRecursiveResolverUri`][137.3] and [`GeckoRuntimeSettings.getDefaultRecursiveResolverUri`][137.4]
|
- Added support for controlling `network.trr.default_provider_uri` via [`GeckoRuntimeSettings.setDefaultRecursiveResolverUri`][137.3] and [`GeckoRuntimeSettings.getDefaultRecursiveResolverUri`][137.4]
|
||||||
@@ -1677,4 +1689,4 @@ to allow adding gecko profiler markers.
|
|||||||
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport(android.content.Context,android.os.Bundle,java.lang.String)
|
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport(android.content.Context,android.os.Bundle,java.lang.String)
|
||||||
[65.25]: {{javadoc_uri}}/GeckoResult.html
|
[65.25]: {{javadoc_uri}}/GeckoResult.html
|
||||||
|
|
||||||
[api-version]: 5457ccf9a76bbdba450eede7c0f8084747965685
|
[api-version]: c4088b9c37d508e0a5346aa173795fc7d82ff6f2
|
||||||
|
|||||||
@@ -205,6 +205,39 @@ final class BasicGeckoViewPrompt implements GeckoSession.PromptDelegate {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public GeckoResult<PromptResponse> onFolderUploadPrompt(
|
||||||
|
final GeckoSession session, final FolderUploadPrompt prompt) {
|
||||||
|
final Activity activity = mActivity;
|
||||||
|
if (activity == null) {
|
||||||
|
return GeckoResult.fromValue(prompt.dismiss());
|
||||||
|
}
|
||||||
|
final AlertDialog.Builder builder =
|
||||||
|
new AlertDialog.Builder(activity)
|
||||||
|
.setTitle(R.string.folder_upload_title)
|
||||||
|
.setMessage(R.string.folder_upload_message);
|
||||||
|
|
||||||
|
final GeckoResult<PromptResponse> res = new GeckoResult<>();
|
||||||
|
|
||||||
|
final DialogInterface.OnClickListener listener =
|
||||||
|
(dialog, which) -> {
|
||||||
|
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||||
|
res.complete(prompt.confirm(AllowOrDeny.ALLOW));
|
||||||
|
} else if (which == DialogInterface.BUTTON_NEGATIVE) {
|
||||||
|
res.complete(prompt.confirm(AllowOrDeny.DENY));
|
||||||
|
} else {
|
||||||
|
res.complete(prompt.dismiss());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
builder.setPositiveButton(R.string.folder_upload_confirm_accept, listener);
|
||||||
|
builder.setNegativeButton(R.string.folder_upload_confirm_cancel, listener);
|
||||||
|
|
||||||
|
createStandardDialog(builder, prompt, res).show();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
private int getViewPadding(final AlertDialog.Builder builder) {
|
private int getViewPadding(final AlertDialog.Builder builder) {
|
||||||
final TypedArray attr =
|
final TypedArray attr =
|
||||||
builder
|
builder
|
||||||
@@ -857,6 +890,24 @@ final class BasicGeckoViewPrompt implements GeckoSession.PromptDelegate {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GeckoResult<PromptResponse> onFolderPrompt(
|
||||||
|
final GeckoSession session, final FilePrompt prompt) {
|
||||||
|
final Activity activity = mActivity;
|
||||||
|
final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
|
intent.addCategory(Intent.CATEGORY_DEFAULT);
|
||||||
|
final GeckoResult<PromptResponse> res = new GeckoResult<PromptResponse>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
mFileResponse = res;
|
||||||
|
mFilePrompt = prompt;
|
||||||
|
activity.startActivityForResult(intent, filePickerRequestCode);
|
||||||
|
} catch (final ActivityNotFoundException e) {
|
||||||
|
Log.e(LOGTAG, "Cannot launch activity", e);
|
||||||
|
return GeckoResult.fromValue(prompt.dismiss());
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@TargetApi(19)
|
@TargetApi(19)
|
||||||
public GeckoResult<PromptResponse> onFilePrompt(GeckoSession session, FilePrompt prompt) {
|
public GeckoResult<PromptResponse> onFilePrompt(GeckoSession session, FilePrompt prompt) {
|
||||||
@@ -865,6 +916,10 @@ final class BasicGeckoViewPrompt implements GeckoSession.PromptDelegate {
|
|||||||
return GeckoResult.fromValue(prompt.dismiss());
|
return GeckoResult.fromValue(prompt.dismiss());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prompt.type == FilePrompt.Type.FOLDER) {
|
||||||
|
return onFolderPrompt(session, prompt);
|
||||||
|
}
|
||||||
|
|
||||||
// Merge all given MIME types into one, using wildcard if needed.
|
// Merge all given MIME types into one, using wildcard if needed.
|
||||||
String mimeType = null;
|
String mimeType = null;
|
||||||
String mimeSubtype = null;
|
String mimeSubtype = null;
|
||||||
@@ -951,6 +1006,8 @@ final class BasicGeckoViewPrompt implements GeckoSession.PromptDelegate {
|
|||||||
uris.add(clip.getItemAt(i).getUri());
|
uris.add(clip.getItemAt(i).getUri());
|
||||||
}
|
}
|
||||||
res.complete(prompt.confirm(mActivity, uris.toArray(new Uri[uris.size()])));
|
res.complete(prompt.confirm(mActivity, uris.toArray(new Uri[uris.size()])));
|
||||||
|
} else if (prompt.type == FilePrompt.Type.FOLDER) {
|
||||||
|
res.complete(prompt.confirm(mActivity, uri));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,4 +175,9 @@
|
|||||||
<string name="repost_confirm_title">Are you sure?</string>
|
<string name="repost_confirm_title">Are you sure?</string>
|
||||||
<string name="repost_confirm_resend">Resend</string>
|
<string name="repost_confirm_resend">Resend</string>
|
||||||
<string name="repost_confirm_cancel">Cancel</string>
|
<string name="repost_confirm_cancel">Cancel</string>
|
||||||
|
|
||||||
|
<string name="folder_upload_message">Are you sure you want to upload all files? Only do this if you trust the site.</string>
|
||||||
|
<string name="folder_upload_title">Confirm Upload</string>
|
||||||
|
<string name="folder_upload_confirm_accept">Upload</string>
|
||||||
|
<string name="folder_upload_confirm_cancel">Cancel</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -16,10 +16,18 @@ const { debug, warn } = GeckoViewUtils.initLogging("FilePickerDelegate");
|
|||||||
export class FilePickerDelegate {
|
export class FilePickerDelegate {
|
||||||
/* ---------- nsIFilePicker ---------- */
|
/* ---------- nsIFilePicker ---------- */
|
||||||
init(aBrowsingContext, aTitle, aMode) {
|
init(aBrowsingContext, aTitle, aMode) {
|
||||||
if (
|
let mode;
|
||||||
aMode === Ci.nsIFilePicker.modeGetFolder ||
|
switch (aMode) {
|
||||||
aMode === Ci.nsIFilePicker.modeSave
|
case Ci.nsIFilePicker.modeOpen:
|
||||||
) {
|
mode = "single";
|
||||||
|
break;
|
||||||
|
case Ci.nsIFilePicker.modeGetFolder:
|
||||||
|
mode = "folder";
|
||||||
|
break;
|
||||||
|
case Ci.nsIFilePicker.modeOpenMultiple:
|
||||||
|
mode = "multiple";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||||
}
|
}
|
||||||
this._browsingContext = aBrowsingContext;
|
this._browsingContext = aBrowsingContext;
|
||||||
@@ -27,7 +35,7 @@ export class FilePickerDelegate {
|
|||||||
this._msg = {
|
this._msg = {
|
||||||
type: "file",
|
type: "file",
|
||||||
title: aTitle,
|
title: aTitle,
|
||||||
mode: aMode === Ci.nsIFilePicker.modeOpenMultiple ? "multiple" : "single",
|
mode,
|
||||||
};
|
};
|
||||||
this._mode = aMode;
|
this._mode = aMode;
|
||||||
this._mimeTypes = [];
|
this._mimeTypes = [];
|
||||||
@@ -68,10 +76,10 @@ export class FilePickerDelegate {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
for (const file of aFiles) {
|
for (const file of aFiles) {
|
||||||
const domFile = await this._getDOMFile(file);
|
const domFileOrDir = await this._getDOMFileOrDir(file);
|
||||||
fileData.push({
|
fileData.push({
|
||||||
file,
|
file,
|
||||||
domFile,
|
domFileOrDir,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@@ -106,7 +114,7 @@ export class FilePickerDelegate {
|
|||||||
|
|
||||||
for (const fileData of this._fileData) {
|
for (const fileData of this._fileData) {
|
||||||
if (aDOMFile) {
|
if (aDOMFile) {
|
||||||
yield fileData.domFile;
|
yield fileData.domFileOrDir;
|
||||||
}
|
}
|
||||||
yield new lazy.FileUtils.File(fileData.file);
|
yield new lazy.FileUtils.File(fileData.file);
|
||||||
}
|
}
|
||||||
@@ -116,6 +124,20 @@ export class FilePickerDelegate {
|
|||||||
return this._getEnumerator(/* aDOMFile */ false);
|
return this._getEnumerator(/* aDOMFile */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getDOMFileOrDir(aPath) {
|
||||||
|
if (this.mode == Ci.nsIFilePicker.modeGetFolder) {
|
||||||
|
return this._getDOMDir(aPath);
|
||||||
|
}
|
||||||
|
return this._getDOMFile(aPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getDOMDir(aPath) {
|
||||||
|
if (this._prompt.domWin) {
|
||||||
|
return new this._prompt.domWin.Directory(aPath);
|
||||||
|
}
|
||||||
|
return new Directory(aPath);
|
||||||
|
}
|
||||||
|
|
||||||
_getDOMFile(aPath) {
|
_getDOMFile(aPath) {
|
||||||
if (this._prompt.domWin) {
|
if (this._prompt.domWin) {
|
||||||
return this._prompt.domWin.File.createFromFileName(aPath);
|
return this._prompt.domWin.File.createFromFileName(aPath);
|
||||||
@@ -127,7 +149,7 @@ export class FilePickerDelegate {
|
|||||||
if (!this._fileData) {
|
if (!this._fileData) {
|
||||||
throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
|
throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
|
||||||
}
|
}
|
||||||
return this._fileData[0] ? this._fileData[0].domFile : null;
|
return this._fileData[0]?.domFileOrDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
get domFileOrDirectoryEnumerator() {
|
get domFileOrDirectoryEnumerator() {
|
||||||
|
|||||||
@@ -32,9 +32,14 @@ export class PromptCollection {
|
|||||||
}).then(result => !!result?.allow);
|
}).then(result => !!result?.allow);
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmFolderUpload() {
|
confirmFolderUpload(browsingContext, directoryName) {
|
||||||
// Folder upload is not supported by GeckoView yet, see Bug 1674428.
|
const msg = {
|
||||||
return false;
|
type: "folderUpload",
|
||||||
|
directoryName,
|
||||||
|
};
|
||||||
|
const prompter = new lazy.GeckoViewPrompter(browsingContext);
|
||||||
|
const result = prompter.showPrompt(msg);
|
||||||
|
return !!result?.allow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user