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