Commit ba6c2318 authored by Makoto Kato's avatar Makoto Kato
Browse files

Bug 1674428 - Part 1. Implement folder upload picker API. r=geckoview-reviewers,owlish,ohall

parent 42917d14
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -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();
  }
+2 −0
Original line number Diff line number Diff line
@@ -30,6 +30,8 @@
      accept="image/*,.pdf"
    />

    <input type="file" id="direxample" webkitdirectory />

    <datalist id="colorlist">
      <option>#000000</option>
      <option>#808080</option>
+41 −0
Original line number Diff line number Diff line
@@ -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)
+99 −0
Original line number Diff line number Diff line
@@ -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;
  }
}
+63 −30
Original line number Diff line number Diff line
@@ -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;
          if (DocumentsContract.isDocumentUri(context, uri)) {
            return IntentUtils.resolveDocumentUri(context, uri);
          }
            } catch (final Exception e) {
            }
          } while (cur.moveToNext());
        } finally {
          cur.close();
          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.
     *
Loading