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> 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> 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> 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);
@@ -1643,10 +1644,17 @@ package org.mozilla.geckoview {
public static class GeckoSession.PromptDelegate.FilePrompt.Type {
ctor protected Type();
field public static final int FOLDER = 3;
field public static final int MULTIPLE = 2;
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 {
ctor public IdentityCredential();
}

View File

@@ -30,6 +30,8 @@
accept="image/*,.pdf"
/>
<input type="file" id="direxample" webkitdirectory />
<datalist id="colorlist">
<option>#000000</option>
<option>#808080</option>

View File

@@ -4,9 +4,11 @@
package org.mozilla.geckoview.test
import android.net.Uri
import android.view.KeyEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import org.hamcrest.Matchers.* // ktlint-disable no-wildcard-imports
import org.junit.Assert
import org.junit.Test
@@ -1102,6 +1104,7 @@ class PromptDelegateTest : BaseSessionTest(
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("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())
}
})
@@ -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() {
sessionRule.setPrefsUntilTestEnd(mapOf("dom.webshare.requireinteraction" to false))
mainSession.loadTestPath(HELLO_HTML_PATH)

View File

@@ -6,13 +6,26 @@
package org.mozilla.gecko.util;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
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.util.Locale;
/** Utilities for Intents. */
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() {}
/**
@@ -117,4 +130,90 @@ public class IntentUtils {
private static void nullIntentSelector(final Intent intent) {
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.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Point;
@@ -27,6 +25,7 @@ import android.os.IInterface;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.provider.DocumentsContract;
import android.text.TextUtils;
import android.util.Base64;
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
* data (e.g. due to page refresh).
@@ -6372,7 +6407,7 @@ public class GeckoSession {
*/
class FilePrompt extends BasePrompt {
@Retention(RetentionPolicy.SOURCE)
@IntDef({Type.SINGLE, Type.MULTIPLE})
@IntDef({Type.SINGLE, Type.MULTIPLE, Type.FOLDER})
public @interface FileType {}
/** Types of file prompts. */
@@ -6383,6 +6418,9 @@ public class GeckoSession {
/** Prompt for multiple files. */
public static final int MULTIPLE = 2;
/** Prompt for directory. */
public static final int FOLDER = 3;
protected Type() {}
}
@@ -6458,7 +6496,7 @@ public class GeckoSession {
@UiThread
public @NonNull PromptResponse confirm(
@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();
}
@@ -6483,33 +6521,14 @@ public class GeckoSession {
if ("file".equals(uri.getScheme())) {
return uri.getPath();
}
final ContentResolver cr = context.getContentResolver();
final Cursor cur =
cr.query(
uri,
new String[] {"_data"}, /* selection */
null,
/* args */ null, /* sort */
null);
if (cur == null) {
return null;
}
try {
final int idx = cur.getColumnIndex("_data");
if (idx < 0 || !cur.moveToFirst()) {
return null;
if ("content".equals(uri.getScheme())) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && DocumentsContract.isTreeUri(uri)) {
return IntentUtils.resolveTreeUri(context, uri);
}
do {
try {
final String path = cur.getString(idx);
if (path != null && !path.isEmpty()) {
return path;
}
} catch (final Exception e) {
}
} while (cur.moveToNext());
} finally {
cur.close();
if (DocumentsContract.isDocumentUri(context, uri)) {
return IntentUtils.resolveDocumentUri(context, uri);
}
return IntentUtils.resolveContentUri(context, uri);
}
return null;
}
@@ -6709,6 +6728,20 @@ public class GeckoSession {
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.
*

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.DateTimePrompt;
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.PrivacyPolicyPrompt;
import org.mozilla.geckoview.GeckoSession.PromptDelegate.IdentityCredential.ProviderSelectorPrompt;
@@ -362,6 +363,8 @@ import org.mozilla.geckoview.GeckoSession.PromptDelegate.TextPrompt;
intMode = FilePrompt.Type.SINGLE;
} else if ("multiple".equals(mode)) {
intMode = FilePrompt.Type.MULTIPLE;
} else if ("folder".equals(mode)) {
intMode = FilePrompt.Type.FOLDER;
} else {
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> {
@Override
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 DateTimeHandler(), "datetime");
sPromptHandlers.register(new FileHandler(), "file");
sPromptHandlers.register(new FolderUploadHandler(), "folderUpload");
sPromptHandlers.register(new PopupHandler(), "popup");
sPromptHandlers.register(new RepostHandler(), "repost");
sPromptHandlers.register(new ShareHandler(), "share");

View File

@@ -13,6 +13,18 @@ exclude: true
⚠️ 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
- ⚠️ [`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]
@@ -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.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;
}
@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) {
final TypedArray attr =
builder
@@ -857,6 +890,24 @@ final class BasicGeckoViewPrompt implements GeckoSession.PromptDelegate {
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
@TargetApi(19)
public GeckoResult<PromptResponse> onFilePrompt(GeckoSession session, FilePrompt prompt) {
@@ -865,6 +916,10 @@ final class BasicGeckoViewPrompt implements GeckoSession.PromptDelegate {
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.
String mimeType = null;
String mimeSubtype = null;
@@ -951,6 +1006,8 @@ final class BasicGeckoViewPrompt implements GeckoSession.PromptDelegate {
uris.add(clip.getItemAt(i).getUri());
}
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_resend">Resend</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>

View File

@@ -16,18 +16,26 @@ const { debug, warn } = GeckoViewUtils.initLogging("FilePickerDelegate");
export class FilePickerDelegate {
/* ---------- nsIFilePicker ---------- */
init(aBrowsingContext, aTitle, aMode) {
if (
aMode === Ci.nsIFilePicker.modeGetFolder ||
aMode === Ci.nsIFilePicker.modeSave
) {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
let mode;
switch (aMode) {
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);
}
this._browsingContext = aBrowsingContext;
this._prompt = new lazy.GeckoViewPrompter(aBrowsingContext);
this._msg = {
type: "file",
title: aTitle,
mode: aMode === Ci.nsIFilePicker.modeOpenMultiple ? "multiple" : "single",
mode,
};
this._mode = aMode;
this._mimeTypes = [];
@@ -68,10 +76,10 @@ export class FilePickerDelegate {
try {
for (const file of aFiles) {
const domFile = await this._getDOMFile(file);
const domFileOrDir = await this._getDOMFileOrDir(file);
fileData.push({
file,
domFile,
domFileOrDir,
});
}
} catch (ex) {
@@ -106,7 +114,7 @@ export class FilePickerDelegate {
for (const fileData of this._fileData) {
if (aDOMFile) {
yield fileData.domFile;
yield fileData.domFileOrDir;
}
yield new lazy.FileUtils.File(fileData.file);
}
@@ -116,6 +124,20 @@ export class FilePickerDelegate {
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) {
if (this._prompt.domWin) {
return this._prompt.domWin.File.createFromFileName(aPath);
@@ -127,7 +149,7 @@ export class FilePickerDelegate {
if (!this._fileData) {
throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
}
return this._fileData[0] ? this._fileData[0].domFile : null;
return this._fileData[0]?.domFileOrDir;
}
get domFileOrDirectoryEnumerator() {

View File

@@ -32,9 +32,14 @@ export class PromptCollection {
}).then(result => !!result?.allow);
}
confirmFolderUpload() {
// Folder upload is not supported by GeckoView yet, see Bug 1674428.
return false;
confirmFolderUpload(browsingContext, directoryName) {
const msg = {
type: "folderUpload",
directoryName,
};
const prompter = new lazy.GeckoViewPrompter(browsingContext);
const result = prompter.showPrompt(msg);
return !!result?.allow;
}
}