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> 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();
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
accept="image/*,.pdf"
|
||||
/>
|
||||
|
||||
<input type="file" id="direxample" webkitdirectory />
|
||||
|
||||
<datalist id="colorlist">
|
||||
<option>#000000</option>
|
||||
<option>#808080</option>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user