From 843e9c70c9ea0da67e46dd4c1d6b72dd0ab235c2 Mon Sep 17 00:00:00 2001 From: Erik Rodriguez Date: Tue, 3 Dec 2024 02:30:17 -0600 Subject: [PATCH] AuthTab project example --- androidbrowserhelper/build.gradle | 1 + build.gradle | 8 +- demos/custom-tabs-authview/.gitignore | 1 + demos/custom-tabs-authview/build.gradle | 51 ++++++ .../src/main/AndroidManifest.xml | 35 ++++ .../demos/customtabsauthview/AuthManager.java | 99 ++++++++++ .../demos/customtabsauthview/GithubApi.java | 43 +++++ .../customtabsauthview/MainActivity.java | 101 +++++++++++ .../demos/customtabsauthview/Utils.java | 49 +++++ .../res/drawable/ic_launcher_background.xml | 171 ++++++++++++++++++ .../res/drawable/ic_launcher_foreground.xml | 31 ++++ .../src/main/res/layout/activity_main.xml | 42 +++++ .../main/res/mipmap-anydpi/ic_launcher.xml | 6 + .../res/mipmap-anydpi/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../src/main/res/values-night/themes.xml | 7 + .../src/main/res/values/colors.xml | 18 ++ .../src/main/res/values/strings.xml | 20 ++ .../src/main/res/values/themes.xml | 7 + .../src/main/res/xml/backup_rules.xml | 14 ++ .../main/res/xml/data_extraction_rules.xml | 20 ++ demos/custom-tabs-example-app/build.gradle | 1 + demos/custom-tabs-headers/build.gradle | 1 + .../build.gradle | 1 + demos/custom-tabs-oauth/build.gradle | 1 + demos/custom-tabs-session/build.gradle | 1 + demos/twa-basic/build.gradle | 1 + demos/twa-custom-launcher/build.gradle | 1 + demos/twa-firebase-analytics/build.gradle | 1 + demos/twa-location-delegation/build.gradle | 1 + demos/twa-multi-domain/build.gradle | 1 + .../twa-notification-delegation/build.gradle | 1 + demos/twa-offline-first/build.gradle | 1 + demos/twa-orientation/build.gradle | 1 + demos/twa-play-billing/build.gradle | 1 + demos/twa-post-message/build.gradle | 1 + demos/twa-web-share-target/build.gradle | 1 + demos/twa-webview-fallback/build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- locationdelegation/build.gradle | 1 + playbilling/build.gradle | 1 + settings.gradle | 1 + 51 files changed, 751 insertions(+), 4 deletions(-) create mode 100644 demos/custom-tabs-authview/.gitignore create mode 100644 demos/custom-tabs-authview/build.gradle create mode 100644 demos/custom-tabs-authview/src/main/AndroidManifest.xml create mode 100644 demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/AuthManager.java create mode 100644 demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/GithubApi.java create mode 100644 demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/MainActivity.java create mode 100644 demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/Utils.java create mode 100644 demos/custom-tabs-authview/src/main/res/drawable/ic_launcher_background.xml create mode 100644 demos/custom-tabs-authview/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 demos/custom-tabs-authview/src/main/res/layout/activity_main.xml create mode 100644 demos/custom-tabs-authview/src/main/res/mipmap-anydpi/ic_launcher.xml create mode 100644 demos/custom-tabs-authview/src/main/res/mipmap-anydpi/ic_launcher_round.xml create mode 100644 demos/custom-tabs-authview/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 demos/custom-tabs-authview/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 demos/custom-tabs-authview/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 demos/custom-tabs-authview/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 demos/custom-tabs-authview/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 demos/custom-tabs-authview/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 demos/custom-tabs-authview/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 demos/custom-tabs-authview/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 demos/custom-tabs-authview/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 demos/custom-tabs-authview/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 demos/custom-tabs-authview/src/main/res/values-night/themes.xml create mode 100644 demos/custom-tabs-authview/src/main/res/values/colors.xml create mode 100644 demos/custom-tabs-authview/src/main/res/values/strings.xml create mode 100644 demos/custom-tabs-authview/src/main/res/values/themes.xml create mode 100644 demos/custom-tabs-authview/src/main/res/xml/backup_rules.xml create mode 100644 demos/custom-tabs-authview/src/main/res/xml/data_extraction_rules.xml diff --git a/androidbrowserhelper/build.gradle b/androidbrowserhelper/build.gradle index 11698ee6..6f41763f 100644 --- a/androidbrowserhelper/build.gradle +++ b/androidbrowserhelper/build.gradle @@ -20,6 +20,7 @@ apply plugin: 'maven-publish' def VERSION = "2.5.0"; android { + namespace "com.google.androidbrowserhelper" compileSdkVersion 31 defaultConfig { diff --git a/build.gradle b/build.gradle index c5faeab2..7deb04de 100644 --- a/build.gradle +++ b/build.gradle @@ -20,9 +20,12 @@ buildscript { repositories { google() jcenter() + maven { + url = uri("https://androidx.dev/snapshots/builds/12502757/artifacts/repository") + } } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' + classpath 'com.android.tools.build:gradle:8.7.2' } } @@ -30,6 +33,9 @@ allprojects { repositories { google() jcenter() + maven { + url = uri("https://androidx.dev/snapshots/builds/12502757/artifacts/repository") + } } } diff --git a/demos/custom-tabs-authview/.gitignore b/demos/custom-tabs-authview/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/demos/custom-tabs-authview/.gitignore @@ -0,0 +1 @@ +/build diff --git a/demos/custom-tabs-authview/build.gradle b/demos/custom-tabs-authview/build.gradle new file mode 100644 index 00000000..ddea45eb --- /dev/null +++ b/demos/custom-tabs-authview/build.gradle @@ -0,0 +1,51 @@ +/* + * Copyright 2024 Google LLC + * + * 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 + * + * https://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. + */ + +apply plugin: 'com.android.application' + +android { + namespace 'com.google.androidbrowserhelper.demos.customtabsauthview' + compileSdkVersion 35 + defaultConfig { + applicationId "com.google.androidbrowserhelper.demos.customtabsauthview" + minSdkVersion 26 + targetSdkVersion 35 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } +} + +dependencies { + implementation project(path: ':androidbrowserhelper') + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.activity:activity:1.9.3' + implementation 'androidx.browser:browser:1.9.0-SNAPSHOT' + implementation 'com.google.android.material:material:1.12.0' + implementation 'androidx.annotation:annotation:1.9.1' + implementation 'androidx.constraintlayout:constraintlayout:2.2.0' +} diff --git a/demos/custom-tabs-authview/src/main/AndroidManifest.xml b/demos/custom-tabs-authview/src/main/AndroidManifest.xml new file mode 100644 index 00000000..3a5a52c6 --- /dev/null +++ b/demos/custom-tabs-authview/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/AuthManager.java b/demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/AuthManager.java new file mode 100644 index 00000000..86c0e903 --- /dev/null +++ b/demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/AuthManager.java @@ -0,0 +1,99 @@ +package com.google.androidbrowserhelper.demos.customtabsauthview; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.annotation.NonNull; +import androidx.browser.auth.AuthTabIntent; +import androidx.annotation.OptIn; +import androidx.browser.auth.ExperimentalAuthTab; + +import java.io.IOException; +import java.util.UUID; + +@OptIn(markerClass = ExperimentalAuthTab.class) +public class AuthManager { + private static final String TAG = "OAuthManager"; + + private final String mClientId; + private final String mClientSecret; + private final String mAuthorizationEndpoint; + private final String mRedirectScheme; + + public interface OAuthCallback { + void auth(String accessToken, String scope, String tokenType); + } + + public AuthManager(String clientId, String clientSecret, String authorizationEndpoint, + String redirectScheme) { + mClientId = clientId; + mClientSecret = clientSecret; + mAuthorizationEndpoint = authorizationEndpoint; + mRedirectScheme = redirectScheme; + } + + public void authorize(Context context, ActivityResultLauncher launcher, String scope) { + // Generate a random state. + String state = UUID.randomUUID().toString(); + + // Save the state so we can verify later. + SharedPreferences preferences = + context.getSharedPreferences("OAUTH_STORAGE", Context.MODE_PRIVATE); + preferences.edit() + .putString("OAUTH_STATE", state) + .apply(); + + // Create an authorization URI to the OAuth Endpoint. + Uri uri = Uri.parse(mAuthorizationEndpoint) + .buildUpon() + .appendQueryParameter("response_type", "code") + .appendQueryParameter("client_id", mClientId) + .appendQueryParameter("scope", scope) + .appendQueryParameter("state", state) + .build(); + + // Open the Authorization URI in a Chrome Custom Auth Tab. + AuthTabIntent authTabIntent = new AuthTabIntent.Builder().build(); + authTabIntent.launch(launcher, uri, mRedirectScheme); + } + + public void continueAuthFlow(@NonNull Context context, Uri uri, @NonNull OAuthCallback callback) { + String code = uri.getQueryParameter("code"); + SharedPreferences preferences = + context.getSharedPreferences("OAUTH_STORAGE", Context.MODE_PRIVATE); + String state = preferences.getString("OAUTH_STATE", ""); + Uri tokenUri = Uri.parse("https://github.com/login/oauth/access_token") + .buildUpon() + .appendQueryParameter("client_id", mClientId) + .appendQueryParameter("client_secret", mClientSecret) + .appendQueryParameter("code", code) + .appendQueryParameter("state", state) + .build(); + + // Run the network request off the UI thread. + new Thread(() -> { + try { + String response = Utils.fetch(tokenUri); + // The response is a query-string. We concatenate with a valid domain to be + // able to easily parse and extract values. + Uri responseUri = Uri.parse("http://example.com?" + response); + String accessToken = responseUri.getQueryParameter("access_token"); + String tokenType = responseUri.getQueryParameter("token_type"); + String scope = responseUri.getQueryParameter("scope"); + + // Invoke the callback in the main thread. + new Handler(Looper.getMainLooper()).post( + () -> callback.auth(accessToken, scope, tokenType)); + + } catch (IOException e) { + Log.e(TAG, "Error requesting access token: " + e.getMessage()); + } + }).start(); + } +} diff --git a/demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/GithubApi.java b/demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/GithubApi.java new file mode 100644 index 00000000..4b102ae3 --- /dev/null +++ b/demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/GithubApi.java @@ -0,0 +1,43 @@ +package com.google.androidbrowserhelper.demos.customtabsauthview; + +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +public class GithubApi { + private static final String TAG = "GithubAPI"; + private static final String API_ENDPOINT = "https://api.github.com/user"; + private static final String AUTH_HEADER_KEY = "Authorization"; + + public interface UserCallback { + void onUserData(String username); + } + + public static void requestGithubUsername(String token, UserCallback callback) { + new Thread(() -> { + try { + Uri uri = Uri.parse(API_ENDPOINT); + Map headers = + Collections.singletonMap(AUTH_HEADER_KEY, "token " + token); + String response = Utils.fetch(uri, headers); + JSONObject user = new JSONObject(response); + String username = user.getString("name"); + + // Invoke the callback in the main thread. + new Handler(Looper.getMainLooper()).post(() -> { + callback.onUserData(username); + }); + } catch (IOException | JSONException ex) { + Log.e(TAG, "Error fetching GitHub user: " + ex.getMessage()); + } + }).start(); + } +} diff --git a/demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/MainActivity.java b/demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/MainActivity.java new file mode 100644 index 00000000..1f2f0100 --- /dev/null +++ b/demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/MainActivity.java @@ -0,0 +1,101 @@ +/* + * Copyright 2024 Google LLC + * + * 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 + * + * https://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 com.google.androidbrowserhelper.demos.customtabsauthview; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.annotation.OptIn; +import androidx.appcompat.app.AppCompatActivity; +import androidx.browser.auth.AuthTabIntent; +import androidx.browser.auth.ExperimentalAuthTab; + +@OptIn(markerClass = ExperimentalAuthTab.class) +public class MainActivity extends AppCompatActivity { + private static final String TAG = "MainActivity"; + + private static final String AUTHORIZATION_ENDPOINT = "https://github.com/login/oauth/authorize"; + private static final String CLIENT_ID = ""; + private static final String CLIENT_SECRET = ""; + private static final String REDIRECT_SCHEME = "auth"; + + private static final AuthManager O_AUTH_MANAGER = + new AuthManager(CLIENT_ID, CLIENT_SECRET, AUTHORIZATION_ENDPOINT, REDIRECT_SCHEME); + + private final ActivityResultLauncher mLauncher = + AuthTabIntent.registerActivityResultLauncher(this, this::handleAuthResult); + + private Button mLoginButton; + private TextView mUserText; + private ProgressBar mProgressBar; + private boolean mLoggedIn; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mLoginButton = findViewById(R.id.login_button); + mUserText = findViewById(R.id.user_text); + mProgressBar = findViewById(R.id.progress_bar); + + Intent intent = getIntent(); + if (intent != null) { + Uri data = intent.getData(); + if (data != null && data.getHost() != null + && data.getHost().startsWith("callback")) { + mProgressBar.setVisibility(View.VISIBLE); + mLoginButton.setEnabled(false); + completeAuth(data); + } + } + } + + public void login(View v) { + if (mLoggedIn) { + mLoginButton.setText(R.string.login); + mUserText.setText(R.string.logged_out); + mLoggedIn = false; + } else { + O_AUTH_MANAGER.authorize(this, mLauncher, "user"); + } + } + + private void handleAuthResult(AuthTabIntent.AuthResult result) { + if (result.resultCode == AuthTabIntent.RESULT_OK) { + completeAuth(result.resultUri); + } + } + + private void completeAuth(Uri uri) { + O_AUTH_MANAGER.continueAuthFlow(this, uri, (accessToken, scope, tokenType) -> { + GithubApi.requestGithubUsername(accessToken, (username -> { + mLoginButton.setText(R.string.logout); + mLoginButton.setEnabled(true); + mProgressBar.setVisibility(View.INVISIBLE); + mUserText.setText(getString(R.string.logged_in, username)); + mLoggedIn = true; + })); + }); + } +} \ No newline at end of file diff --git a/demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/Utils.java b/demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/Utils.java new file mode 100644 index 00000000..94007468 --- /dev/null +++ b/demos/custom-tabs-authview/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthview/Utils.java @@ -0,0 +1,49 @@ +package com.google.androidbrowserhelper.demos.customtabsauthview; + +import android.net.Uri; + +import androidx.annotation.NonNull; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Collections; +import java.util.Map; + +public class Utils { + public static String fetch(@NonNull Uri uri) throws IOException { + return fetch(uri, Collections.emptyMap()); + } + + public static String fetch(@NonNull Uri uri, Map headers) throws IOException { + HttpURLConnection connection = null; + try { + URL url = new URL(uri.toString()); + connection = (HttpURLConnection)url.openConnection(); + for(Map.Entry entry: headers.entrySet()) { + connection.setRequestProperty(entry.getKey(), entry.getValue()); + } + connection.setDoOutput(true); + return inputStreamToString(connection.getInputStream()); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + @NonNull + public static String inputStreamToString(@NonNull InputStream inputStream) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + StringBuilder builder = new StringBuilder(); + while ((line = reader.readLine()) != null) { + builder.append(line).append('\n'); + } + return builder.toString(); + } + } +} \ No newline at end of file diff --git a/demos/custom-tabs-authview/src/main/res/drawable/ic_launcher_background.xml b/demos/custom-tabs-authview/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..9e983cb3 --- /dev/null +++ b/demos/custom-tabs-authview/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/custom-tabs-authview/src/main/res/drawable/ic_launcher_foreground.xml b/demos/custom-tabs-authview/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..9be1fda8 --- /dev/null +++ b/demos/custom-tabs-authview/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/demos/custom-tabs-authview/src/main/res/layout/activity_main.xml b/demos/custom-tabs-authview/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..ad1f1ec3 --- /dev/null +++ b/demos/custom-tabs-authview/src/main/res/layout/activity_main.xml @@ -0,0 +1,42 @@ + + + + +