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:
Makoto Kato
2025-03-07 12:44:07 +00:00
parent c849312638
commit 3a1ce7ffd2
11 changed files with 348 additions and 44 deletions

View File

@@ -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();
} }

View File

@@ -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>

View File

@@ -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)

View File

@@ -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;
}
} }

View File

@@ -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.
* *

View File

@@ -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");

View File

@@ -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

View File

@@ -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));
} }
} }

View File

@@ -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>

View File

@@ -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() {

View File

@@ -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;
} }
} }