Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
accompanist = "0.36.0"
activityKtx = "1.12.1"
android-googleid = "1.1.1"
androidGradlePlugin = "8.13.1"
androidGradlePlugin = "8.13.2"
androidx-activity-compose = "1.12.1"
androidx-appcompat = "1.7.0"
androidx-compose-bom = "2025.12.00"
Expand Down Expand Up @@ -96,6 +96,7 @@ wearOngoing = "1.1.0"
wearToolingPreview = "1.0.0"
webkit = "1.14.0"
wearPhoneInteractions = "1.1.0"
wearRemoteInteractions = "1.1.0"

[libraries]
accompanist-adaptive = "com.google.accompanist:accompanist-adaptive:0.37.3"
Expand Down Expand Up @@ -237,6 +238,7 @@ validator-push = { module = "com.google.android.wearable.watchface.validator:val
wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" }
wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wearComposeMaterial3" }
androidx-wear-phone-interactions = { group = "androidx.wear", name = "wear-phone-interactions", version.ref = "wearPhoneInteractions" }
androidx-wear-remote-interactions = { group = "androidx.wear", name = "wear-remote-interactions", version.ref = "wearRemoteInteractions" }

[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
Expand Down
3 changes: 2 additions & 1 deletion wear/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/build
/build
local.properties
12 changes: 8 additions & 4 deletions wear/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ android {
}
kotlin {
jvmToolchain(21)
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
}
}

buildFeatures {
Expand All @@ -49,10 +52,6 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
kotlinOptions {
jvmTarget = "21"
}

testOptions {
unitTests {
isIncludeAndroidResources = true
Expand All @@ -62,9 +61,14 @@ android {

dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.credentials)
implementation((libs.androidx.credentials.play.services.auth))
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui)
implementation(libs.androidx.wear.input)
implementation(libs.androidx.wear.phone.interactions)
implementation(libs.android.identity.googleid)
implementation(libs.androidx.wear.remote.interactions)
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
androidTestImplementation(composeBom)
Expand Down
12 changes: 12 additions & 0 deletions wear/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,18 @@
</service>
<!-- [END android_wear_datalayerlistener_intent_filter] -->

<service
android:name=".snippets.datalayer.AuthDataListenerService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
<data
android:scheme="wear"
android:host="*"
android:pathPrefix="/auth" />
</intent-filter>
</service>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2025 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
*
* 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.example.wear.snippets.auth

import android.app.Activity
import android.content.Context
import android.os.Bundle
import androidx.credentials.Credential
import androidx.credentials.CredentialManager
import androidx.credentials.CustomCredential
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
import androidx.credentials.GetPasswordOption
import androidx.credentials.GetPublicKeyCredentialOption
import androidx.credentials.PasswordCredential
import androidx.credentials.PublicKeyCredential
import androidx.credentials.exceptions.GetCredentialCancellationException
import com.google.android.libraries.identity.googleid.GetGoogleIdOption

/**
* Handles authentication operations using the Android Credential Manager API.
*
* This class interacts with an [AuthenticationServer] to facilitate sign-in processes
* using Passkeys, Passwords, and Sign-In With Google credentials.
*
* @param context The Android [Context] used to create the [CredentialManager].
* @param authenticationServer The [AuthenticationServer] responsible for handling authentication requests.
*/
class CredentialManagerAuthenticator(
applicationContext: Context,
private val authenticationServer: AuthenticationServer,
) {
private val credentialManager: CredentialManager = CredentialManager.create(applicationContext)

internal suspend fun signInWithCredentialManager(activity: Activity): Boolean {
// [START android_wear_credential_manager_secondary_fallback]
try {
val getCredentialResponse: GetCredentialResponse =
credentialManager.getCredential(activity, createGetCredentialRequest())
return authenticate(getCredentialResponse.credential)
} catch (_: GetCredentialCancellationException) {
navigateToSecondaryAuthentication()
}
// [END android_wear_credential_manager_secondary_fallback]
return false
}

/**
* Creates a [GetCredentialRequest] with standard Wear Credential types.
*
* @return A configured [GetCredentialRequest] ready to be used with [CredentialManager.getCredential].
*/
private fun createGetCredentialRequest(): GetCredentialRequest {
return GetCredentialRequest(
credentialOptions = listOf(
GetPublicKeyCredentialOption(authenticationServer.getPublicKeyRequestOptions()),
GetPasswordOption(),
GetGoogleIdOption.Builder()
.setServerClientId("<Your Google Sign in Server Client ID here.").build(),
),
)
}

/**
* Routes the credential received from `getCredential` to the appropriate authentication
* type handler on the [AuthenticationServer].
*
* @param credential The selected cre
* @return `true` if the credential was successfully processed and authenticated, else 'false'.
*/
private fun authenticate(credential: Credential): Boolean {
when (credential) {
is PublicKeyCredential -> {
return authenticationServer.loginWithPasskey(credential.authenticationResponseJson)
}

is PasswordCredential -> {
return authenticationServer.loginWithPassword(
credential.id,
credential.password,
)
}

is CustomCredential -> {
return authenticationServer.loginWithCustomCredential(
credential.type,
credential.data,
)
}

else -> {
return false
}
}
}
}

/** Placeholder authentication server would make network calls to your authentication server.*/
class AuthenticationServer {

/** Retrieves the public key credential request options from the authentication server.*/
internal fun getPublicKeyRequestOptions(): String {
return "result of network call"
}

fun loginWithPasskey(passkeyResponseJSON: String): Boolean {
return true
}

fun loginWithPassword(username: String, password: String): Boolean {
return true
}

fun loginWithCustomCredential(type: String, data: Bundle): Boolean {
return true
}
}

/** placeholder navigation function. */
fun navigateToSecondaryAuthentication() {
}
78 changes: 78 additions & 0 deletions wear/src/main/java/com/example/wear/snippets/auth/OAuthDAG.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2025 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
*
* 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.example.wear.snippets.auth

import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.wear.remote.interactions.RemoteActivityHelper

class DeviceGrantManager(private val context: Context) {

/** Executes the full Device Grant flow. */
suspend fun startAuthFlow(): Result<String> {
// 1: Get device info from server
val deviceVerificationInfo = getFakeVerificationInfo()

// 2: Show user code UI and open URL on phone
verifyDeviceAuthGrant(deviceVerificationInfo.verificationUri)

// 3: Fetch for the DAG token
val token = fetchToken(deviceVerificationInfo.deviceCode)

// Step 3: Use token to get profile
return retrieveUserProfile(token)
}

// The response data when retrieving the verification
data class VerificationInfo(
val verificationUri: String,
val userCode: String,
val deviceCode: String,
)

/* A fake server call to retrieve */
private fun getFakeVerificationInfo(): VerificationInfo {
// An example of a verificationURI w
return VerificationInfo(
"your client backend",
userCode = "placeholderUser",
deviceCode = "myDeviceCode",
)
}

// [START android_wear_auth_oauth_dag_authorize_device]
// Request access from the authorization server and receive Device Authorization Response.
private fun verifyDeviceAuthGrant(verificationUri: String) {
RemoteActivityHelper(context).startRemoteActivity(
Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.parse(verificationUri)
},
null
)
}
// [END android_wear_auth_oauth_dag_authorize_device]

private fun fetchToken(deviceCode: String): Result<String> {
return Result.success("placeholderToken")
}

private fun retrieveUserProfile(token: Result<String>): Result<String> {
return Result.success("placeholderProfile")
}
}
Loading