Skip to content

Commit 843e9c7

Browse files
AuthTab project example
1 parent 653bfd2 commit 843e9c7

File tree

51 files changed

+751
-4
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+751
-4
lines changed

androidbrowserhelper/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ apply plugin: 'maven-publish'
2020
def VERSION = "2.5.0";
2121

2222
android {
23+
namespace "com.google.androidbrowserhelper"
2324
compileSdkVersion 31
2425

2526
defaultConfig {

build.gradle

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,22 @@ buildscript {
2020
repositories {
2121
google()
2222
jcenter()
23+
maven {
24+
url = uri("https://androidx.dev/snapshots/builds/12502757/artifacts/repository")
25+
}
2326
}
2427
dependencies {
25-
classpath 'com.android.tools.build:gradle:4.1.1'
28+
classpath 'com.android.tools.build:gradle:8.7.2'
2629
}
2730
}
2831

2932
allprojects {
3033
repositories {
3134
google()
3235
jcenter()
36+
maven {
37+
url = uri("https://androidx.dev/snapshots/builds/12502757/artifacts/repository")
38+
}
3339
}
3440
}
3541

demos/custom-tabs-authview/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
apply plugin: 'com.android.application'
18+
19+
android {
20+
namespace 'com.google.androidbrowserhelper.demos.customtabsauthview'
21+
compileSdkVersion 35
22+
defaultConfig {
23+
applicationId "com.google.androidbrowserhelper.demos.customtabsauthview"
24+
minSdkVersion 26
25+
targetSdkVersion 35
26+
versionCode 1
27+
versionName "1.0"
28+
}
29+
30+
buildTypes {
31+
release {
32+
minifyEnabled false
33+
}
34+
}
35+
36+
compileOptions {
37+
sourceCompatibility JavaVersion.VERSION_17
38+
targetCompatibility JavaVersion.VERSION_17
39+
}
40+
}
41+
42+
dependencies {
43+
implementation project(path: ':androidbrowserhelper')
44+
implementation fileTree(dir: "libs", include: ["*.jar"])
45+
implementation 'androidx.appcompat:appcompat:1.7.0'
46+
implementation 'androidx.activity:activity:1.9.3'
47+
implementation 'androidx.browser:browser:1.9.0-SNAPSHOT'
48+
implementation 'com.google.android.material:material:1.12.0'
49+
implementation 'androidx.annotation:annotation:1.9.1'
50+
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
51+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools"
4+
package="com.google.androidbrowserhelper.demos.customtabsauthview">
5+
<uses-permission android:name="android.permission.INTERNET" />
6+
7+
<application
8+
android:allowBackup="true"
9+
android:dataExtractionRules="@xml/data_extraction_rules"
10+
android:fullBackupContent="@xml/backup_rules"
11+
android:icon="@mipmap/ic_launcher"
12+
android:label="@string/app_name"
13+
android:roundIcon="@mipmap/ic_launcher_round"
14+
android:supportsRtl="true"
15+
android:theme="@style/Theme.AuthTab"
16+
tools:targetApi="31">
17+
<activity
18+
android:name=".MainActivity"
19+
android:exported="true">
20+
<intent-filter>
21+
<action android:name="android.intent.action.MAIN" />
22+
<category android:name="android.intent.category.LAUNCHER" />
23+
</intent-filter>
24+
25+
<!-- An intent-filter for fallback to Chrome Custom Tabs -->
26+
<intent-filter android:autoVerify="true">
27+
<action android:name="android.intent.action.VIEW" />
28+
<category android:name="android.intent.category.DEFAULT" />
29+
<category android:name="android.intent.category.BROWSABLE" />
30+
<data android:scheme="auth" />
31+
<data android:host="callback" />
32+
</intent-filter>
33+
</activity>
34+
</application>
35+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.google.androidbrowserhelper.demos.customtabsauthview;
2+
3+
import android.content.Context;
4+
import android.content.Intent;
5+
import android.content.SharedPreferences;
6+
import android.net.Uri;
7+
import android.os.Handler;
8+
import android.os.Looper;
9+
import android.util.Log;
10+
11+
import androidx.activity.result.ActivityResultLauncher;
12+
import androidx.annotation.NonNull;
13+
import androidx.browser.auth.AuthTabIntent;
14+
import androidx.annotation.OptIn;
15+
import androidx.browser.auth.ExperimentalAuthTab;
16+
17+
import java.io.IOException;
18+
import java.util.UUID;
19+
20+
@OptIn(markerClass = ExperimentalAuthTab.class)
21+
public class AuthManager {
22+
private static final String TAG = "OAuthManager";
23+
24+
private final String mClientId;
25+
private final String mClientSecret;
26+
private final String mAuthorizationEndpoint;
27+
private final String mRedirectScheme;
28+
29+
public interface OAuthCallback {
30+
void auth(String accessToken, String scope, String tokenType);
31+
}
32+
33+
public AuthManager(String clientId, String clientSecret, String authorizationEndpoint,
34+
String redirectScheme) {
35+
mClientId = clientId;
36+
mClientSecret = clientSecret;
37+
mAuthorizationEndpoint = authorizationEndpoint;
38+
mRedirectScheme = redirectScheme;
39+
}
40+
41+
public void authorize(Context context, ActivityResultLauncher<Intent> launcher, String scope) {
42+
// Generate a random state.
43+
String state = UUID.randomUUID().toString();
44+
45+
// Save the state so we can verify later.
46+
SharedPreferences preferences =
47+
context.getSharedPreferences("OAUTH_STORAGE", Context.MODE_PRIVATE);
48+
preferences.edit()
49+
.putString("OAUTH_STATE", state)
50+
.apply();
51+
52+
// Create an authorization URI to the OAuth Endpoint.
53+
Uri uri = Uri.parse(mAuthorizationEndpoint)
54+
.buildUpon()
55+
.appendQueryParameter("response_type", "code")
56+
.appendQueryParameter("client_id", mClientId)
57+
.appendQueryParameter("scope", scope)
58+
.appendQueryParameter("state", state)
59+
.build();
60+
61+
// Open the Authorization URI in a Chrome Custom Auth Tab.
62+
AuthTabIntent authTabIntent = new AuthTabIntent.Builder().build();
63+
authTabIntent.launch(launcher, uri, mRedirectScheme);
64+
}
65+
66+
public void continueAuthFlow(@NonNull Context context, Uri uri, @NonNull OAuthCallback callback) {
67+
String code = uri.getQueryParameter("code");
68+
SharedPreferences preferences =
69+
context.getSharedPreferences("OAUTH_STORAGE", Context.MODE_PRIVATE);
70+
String state = preferences.getString("OAUTH_STATE", "");
71+
Uri tokenUri = Uri.parse("https://github.com/login/oauth/access_token")
72+
.buildUpon()
73+
.appendQueryParameter("client_id", mClientId)
74+
.appendQueryParameter("client_secret", mClientSecret)
75+
.appendQueryParameter("code", code)
76+
.appendQueryParameter("state", state)
77+
.build();
78+
79+
// Run the network request off the UI thread.
80+
new Thread(() -> {
81+
try {
82+
String response = Utils.fetch(tokenUri);
83+
// The response is a query-string. We concatenate with a valid domain to be
84+
// able to easily parse and extract values.
85+
Uri responseUri = Uri.parse("http://example.com?" + response);
86+
String accessToken = responseUri.getQueryParameter("access_token");
87+
String tokenType = responseUri.getQueryParameter("token_type");
88+
String scope = responseUri.getQueryParameter("scope");
89+
90+
// Invoke the callback in the main thread.
91+
new Handler(Looper.getMainLooper()).post(
92+
() -> callback.auth(accessToken, scope, tokenType));
93+
94+
} catch (IOException e) {
95+
Log.e(TAG, "Error requesting access token: " + e.getMessage());
96+
}
97+
}).start();
98+
}
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.google.androidbrowserhelper.demos.customtabsauthview;
2+
3+
import android.net.Uri;
4+
import android.os.Handler;
5+
import android.os.Looper;
6+
import android.util.Log;
7+
8+
import org.json.JSONException;
9+
import org.json.JSONObject;
10+
11+
import java.io.IOException;
12+
import java.util.Collections;
13+
import java.util.Map;
14+
15+
public class GithubApi {
16+
private static final String TAG = "GithubAPI";
17+
private static final String API_ENDPOINT = "https://api.github.com/user";
18+
private static final String AUTH_HEADER_KEY = "Authorization";
19+
20+
public interface UserCallback {
21+
void onUserData(String username);
22+
}
23+
24+
public static void requestGithubUsername(String token, UserCallback callback) {
25+
new Thread(() -> {
26+
try {
27+
Uri uri = Uri.parse(API_ENDPOINT);
28+
Map<String, String> headers =
29+
Collections.singletonMap(AUTH_HEADER_KEY, "token " + token);
30+
String response = Utils.fetch(uri, headers);
31+
JSONObject user = new JSONObject(response);
32+
String username = user.getString("name");
33+
34+
// Invoke the callback in the main thread.
35+
new Handler(Looper.getMainLooper()).post(() -> {
36+
callback.onUserData(username);
37+
});
38+
} catch (IOException | JSONException ex) {
39+
Log.e(TAG, "Error fetching GitHub user: " + ex.getMessage());
40+
}
41+
}).start();
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.androidbrowserhelper.demos.customtabsauthview;
18+
19+
import android.content.Intent;
20+
import android.net.Uri;
21+
import android.os.Bundle;
22+
import android.view.View;
23+
import android.widget.Button;
24+
import android.widget.ProgressBar;
25+
import android.widget.TextView;
26+
27+
import androidx.activity.result.ActivityResultLauncher;
28+
import androidx.annotation.OptIn;
29+
import androidx.appcompat.app.AppCompatActivity;
30+
import androidx.browser.auth.AuthTabIntent;
31+
import androidx.browser.auth.ExperimentalAuthTab;
32+
33+
@OptIn(markerClass = ExperimentalAuthTab.class)
34+
public class MainActivity extends AppCompatActivity {
35+
private static final String TAG = "MainActivity";
36+
37+
private static final String AUTHORIZATION_ENDPOINT = "https://github.com/login/oauth/authorize";
38+
private static final String CLIENT_ID = "<github-client-id>";
39+
private static final String CLIENT_SECRET = "<github-client-secret>";
40+
private static final String REDIRECT_SCHEME = "auth";
41+
42+
private static final AuthManager O_AUTH_MANAGER =
43+
new AuthManager(CLIENT_ID, CLIENT_SECRET, AUTHORIZATION_ENDPOINT, REDIRECT_SCHEME);
44+
45+
private final ActivityResultLauncher<Intent> mLauncher =
46+
AuthTabIntent.registerActivityResultLauncher(this, this::handleAuthResult);
47+
48+
private Button mLoginButton;
49+
private TextView mUserText;
50+
private ProgressBar mProgressBar;
51+
private boolean mLoggedIn;
52+
53+
@Override
54+
protected void onCreate(Bundle savedInstanceState) {
55+
super.onCreate(savedInstanceState);
56+
setContentView(R.layout.activity_main);
57+
58+
mLoginButton = findViewById(R.id.login_button);
59+
mUserText = findViewById(R.id.user_text);
60+
mProgressBar = findViewById(R.id.progress_bar);
61+
62+
Intent intent = getIntent();
63+
if (intent != null) {
64+
Uri data = intent.getData();
65+
if (data != null && data.getHost() != null
66+
&& data.getHost().startsWith("callback")) {
67+
mProgressBar.setVisibility(View.VISIBLE);
68+
mLoginButton.setEnabled(false);
69+
completeAuth(data);
70+
}
71+
}
72+
}
73+
74+
public void login(View v) {
75+
if (mLoggedIn) {
76+
mLoginButton.setText(R.string.login);
77+
mUserText.setText(R.string.logged_out);
78+
mLoggedIn = false;
79+
} else {
80+
O_AUTH_MANAGER.authorize(this, mLauncher, "user");
81+
}
82+
}
83+
84+
private void handleAuthResult(AuthTabIntent.AuthResult result) {
85+
if (result.resultCode == AuthTabIntent.RESULT_OK) {
86+
completeAuth(result.resultUri);
87+
}
88+
}
89+
90+
private void completeAuth(Uri uri) {
91+
O_AUTH_MANAGER.continueAuthFlow(this, uri, (accessToken, scope, tokenType) -> {
92+
GithubApi.requestGithubUsername(accessToken, (username -> {
93+
mLoginButton.setText(R.string.logout);
94+
mLoginButton.setEnabled(true);
95+
mProgressBar.setVisibility(View.INVISIBLE);
96+
mUserText.setText(getString(R.string.logged_in, username));
97+
mLoggedIn = true;
98+
}));
99+
});
100+
}
101+
}

0 commit comments

Comments
 (0)