Merge "Add experimental Auth Tab APIs" into androidx-main
diff --git a/browser/browser/api/current.txt b/browser/browser/api/current.txt
index 08b4165..9771220 100644
--- a/browser/browser/api/current.txt
+++ b/browser/browser/api/current.txt
@@ -1,4 +1,23 @@
 // Signature format: 4.0
+package androidx.browser.auth {
+
+  @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public class AuthTabIntent {
+    method public void launch(androidx.activity.result.ActivityResultLauncher<android.content.Intent!>, android.net.Uri, String);
+    method public static androidx.activity.result.ActivityResultLauncher<android.content.Intent!> registerActivityResultLauncher(androidx.activity.result.ActivityResultCaller, androidx.activity.result.ActivityResultCallback<android.net.Uri!>);
+    field public static final String EXTRA_LAUNCH_AUTH_TAB = "androidx.browser.auth.extra.LAUNCH_AUTH_TAB";
+    field public static final String EXTRA_REDIRECT_SCHEME = "androidx.browser.auth.extra.REDIRECT_SCHEME";
+  }
+
+  public static final class AuthTabIntent.Builder {
+    ctor public AuthTabIntent.Builder();
+    method public androidx.browser.auth.AuthTabIntent build();
+  }
+
+  @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAuthTab {
+  }
+
+}
+
 package androidx.browser.browseractions {
 
   @Deprecated public class BrowserActionItem {
diff --git a/browser/browser/api/restricted_current.txt b/browser/browser/api/restricted_current.txt
index 8df6fe2..985731c 100644
--- a/browser/browser/api/restricted_current.txt
+++ b/browser/browser/api/restricted_current.txt
@@ -1,4 +1,23 @@
 // Signature format: 4.0
+package androidx.browser.auth {
+
+  @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public class AuthTabIntent {
+    method public void launch(androidx.activity.result.ActivityResultLauncher<android.content.Intent!>, android.net.Uri, String);
+    method public static androidx.activity.result.ActivityResultLauncher<android.content.Intent!> registerActivityResultLauncher(androidx.activity.result.ActivityResultCaller, androidx.activity.result.ActivityResultCallback<android.net.Uri!>);
+    field public static final String EXTRA_LAUNCH_AUTH_TAB = "androidx.browser.auth.extra.LAUNCH_AUTH_TAB";
+    field public static final String EXTRA_REDIRECT_SCHEME = "androidx.browser.auth.extra.REDIRECT_SCHEME";
+  }
+
+  public static final class AuthTabIntent.Builder {
+    ctor public AuthTabIntent.Builder();
+    method public androidx.browser.auth.AuthTabIntent build();
+  }
+
+  @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAuthTab {
+  }
+
+}
+
 package androidx.browser.browseractions {
 
   @Deprecated public class BrowserActionItem {
diff --git a/browser/browser/build.gradle b/browser/browser/build.gradle
index eab3e3a..6c1f70b 100644
--- a/browser/browser/build.gradle
+++ b/browser/browser/build.gradle
@@ -32,6 +32,7 @@
     api("androidx.annotation:annotation-experimental:1.4.1")
     api(libs.guavaListenableFuture)
 
+    implementation("androidx.activity:activity:1.9.0")
     implementation("androidx.collection:collection:1.4.2")
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
     implementation("androidx.interpolator:interpolator:1.0.0")
diff --git a/browser/browser/src/main/java/androidx/browser/auth/AuthTabIntent.java b/browser/browser/src/main/java/androidx/browser/auth/AuthTabIntent.java
new file mode 100644
index 0000000..68459de
--- /dev/null
+++ b/browser/browser/src/main/java/androidx/browser/auth/AuthTabIntent.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.browser.auth;
+
+import static android.app.Activity.RESULT_OK;
+
+import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_SESSION;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.activity.result.ActivityResultCallback;
+import androidx.activity.result.ActivityResultCaller;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContract;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Class holding an {@link Intent} and other data necessary to start an Auth Tab Activity.
+ *
+ * <p> After creating an instance of this class, you can call {@link #launch} to present the user
+ * with the authentication page. You should create an {@link ActivityResultLauncher} using
+ * {@link #registerActivityResultLauncher} unconditionally before every fragment or activity
+ * creation. Once the user completes the authentication flow, or cancels it, the
+ * {@link ActivityResultCallback} provided when creating the {@link ActivityResultLauncher} will be
+ * called with the result. The returned {@link Uri} will be null if the user closes the Auth Tab
+ * without completing the authentication.
+ *
+ * <p>Note: The constants below are public for the browser implementation's benefit. You are
+ * strongly encouraged to use {@link AuthTabIntent.Builder}.
+ */
+@ExperimentalAuthTab
+public class AuthTabIntent {
+    /** Boolean extra that triggers an Auth Tab launch. */
+    public static final String EXTRA_LAUNCH_AUTH_TAB =
+            "androidx.browser.auth.extra.LAUNCH_AUTH_TAB";
+    /** String extra that determines the redirect scheme. */
+    public static final String EXTRA_REDIRECT_SCHEME =
+            "androidx.browser.auth.extra.REDIRECT_SCHEME";
+
+    private final Intent mIntent;
+
+    /**
+     * Launches an Auth Tab Activity. Should be used for flows that result in a redirect with a
+     * custom scheme.
+     *
+     * @param launcher       The {@link ActivityResultLauncher} used to launch the Auth Tab. Use
+     *                       {@link #registerActivityResultLauncher} to create this. See the class
+     *                       documentation for more details.
+     * @param url            The url to load in the Auth Tab.
+     * @param redirectScheme The scheme of the resulting redirect.
+     */
+    public void launch(@NonNull ActivityResultLauncher<Intent> launcher, @NonNull Uri url,
+            @NonNull String redirectScheme) {
+        mIntent.setData(url);
+        mIntent.putExtra(EXTRA_REDIRECT_SCHEME, redirectScheme);
+        launcher.launch(mIntent);
+    }
+
+    private AuthTabIntent(@NonNull Intent intent) {
+        mIntent = intent;
+    }
+
+    /**
+     * Builder class for {@link AuthTabIntent} objects.
+     */
+    public static final class Builder {
+        private final Intent mIntent = new Intent(Intent.ACTION_VIEW);
+
+        public Builder() {
+        }
+
+        /**
+         * Combines all the options that have been set and returns a new {@link AuthTabIntent}
+         * object.
+         */
+        @NonNull
+        public AuthTabIntent build() {
+            mIntent.putExtra(EXTRA_LAUNCH_AUTH_TAB, true);
+
+            // Put a null EXTRA_SESSION as a fallback so that this is interpreted as a Custom Tab
+            // intent by browser implementations that don't support Auth Tab.
+            Bundle bundle = new Bundle();
+            bundle.putBinder(EXTRA_SESSION, null);
+            mIntent.putExtras(bundle);
+
+            return new AuthTabIntent(mIntent);
+        }
+    }
+
+    /**
+     * Registers a request to launch an Auth Tab and returns an {@link ActivityResultLauncher} that
+     * can be used to launch it. Should be called unconditionally before the fragment or activity is
+     * created.
+     *
+     * @param caller   An {@link ActivityResultCaller}, e.g. a
+     *                 {@link androidx.activity.ComponentActivity} or a
+     *                 {@link androidx.fragment.app.Fragment}.
+     * @param callback An {@link ActivityResultCallback} to be called with the auth result.
+     * @return An {@link ActivityResultLauncher} to be passed to {@link #launch}.
+     */
+    @NonNull
+    public static ActivityResultLauncher<Intent> registerActivityResultLauncher(
+            @NonNull ActivityResultCaller caller, @NonNull ActivityResultCallback<Uri> callback) {
+        return caller.registerForActivityResult(new AuthenticateUserResultContract(), callback);
+    }
+
+    private static class AuthenticateUserResultContract extends
+            ActivityResultContract<Intent, Uri> {
+        @NonNull
+        @Override
+        public Intent createIntent(@NonNull Context context, @NonNull Intent input) {
+            return input;
+        }
+
+        @NonNull
+        @Override
+        public Uri parseResult(int resultCode, @Nullable Intent intent) {
+            if (resultCode == RESULT_OK && intent != null) {
+                return Objects.requireNonNullElse(intent.getData(), Uri.EMPTY);
+            }
+            return Uri.EMPTY;
+        }
+    }
+}
diff --git a/browser/browser/src/main/java/androidx/browser/auth/ExperimentalAuthTab.java b/browser/browser/src/main/java/androidx/browser/auth/ExperimentalAuthTab.java
new file mode 100644
index 0000000..78aead3
--- /dev/null
+++ b/browser/browser/src/main/java/androidx/browser/auth/ExperimentalAuthTab.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.browser.auth;
+
+import androidx.annotation.RequiresOptIn;
+
+/**
+ * Denotes that the annotated declaration is part of the experimental Auth Tab.
+ */
+@RequiresOptIn
+public @interface ExperimentalAuthTab {
+}
diff --git a/browser/browser/src/test/java/androidx/browser/auth/AuthTabIntentTest.java b/browser/browser/src/test/java/androidx/browser/auth/AuthTabIntentTest.java
new file mode 100644
index 0000000..4992c80
--- /dev/null
+++ b/browser/browser/src/test/java/androidx/browser/auth/AuthTabIntentTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.browser.auth;
+
+import static androidx.browser.auth.AuthTabIntent.EXTRA_LAUNCH_AUTH_TAB;
+import static androidx.browser.auth.AuthTabIntent.EXTRA_REDIRECT_SCHEME;
+import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_SESSION;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.net.Uri;
+
+import androidx.activity.result.ActivityResultLauncher;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+/** Tests for {@link AuthTabIntent}. */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class AuthTabIntentTest {
+    @Rule
+    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    private static final Uri URI = Uri.parse("https://www.google.com");
+
+    @Captor
+    private ArgumentCaptor<Intent> mIntentCaptor;
+    @Mock
+    private ActivityResultLauncher<Intent> mLauncher;
+
+    @Test
+    public void testIntentHasNecessaryData() {
+        AuthTabIntent intent = new AuthTabIntent.Builder().build();
+        intent.launch(mLauncher, URI, "myscheme");
+
+        verify(mLauncher).launch(mIntentCaptor.capture());
+        Intent launchIntent = mIntentCaptor.getValue();
+
+        assertTrue(launchIntent.getBooleanExtra(EXTRA_LAUNCH_AUTH_TAB, false));
+        assertTrue(launchIntent.hasExtra(EXTRA_SESSION));
+        assertEquals(URI.toString(), launchIntent.getDataString());
+        assertEquals("myscheme", launchIntent.getStringExtra(EXTRA_REDIRECT_SCHEME));
+    }
+}