diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e5a5196db..f3afdb4df 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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"
@@ -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"
@@ -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" }
diff --git a/wear/.gitignore b/wear/.gitignore
index 42afabfd2..9f014db8c 100644
--- a/wear/.gitignore
+++ b/wear/.gitignore
@@ -1 +1,2 @@
-/build
\ No newline at end of file
+/build
+local.properties
diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts
index 23efcf637..f27403e35 100644
--- a/wear/build.gradle.kts
+++ b/wear/build.gradle.kts
@@ -38,6 +38,9 @@ android {
}
kotlin {
jvmToolchain(21)
+ compilerOptions {
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
+ }
}
buildFeatures {
@@ -49,10 +52,6 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
- kotlinOptions {
- jvmTarget = "21"
- }
-
testOptions {
unitTests {
isIncludeAndroidResources = true
@@ -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)
diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml
index ea4f18966..87f3bdffa 100644
--- a/wear/src/main/AndroidManifest.xml
+++ b/wear/src/main/AndroidManifest.xml
@@ -376,6 +376,18 @@
+
+
+
+
+
+
+
diff --git a/wear/src/main/java/com/example/wear/snippets/auth/CredentialManager.kt b/wear/src/main/java/com/example/wear/snippets/auth/CredentialManager.kt
new file mode 100644
index 000000000..09145528c
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/auth/CredentialManager.kt
@@ -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(" {
+ 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() {
+}
diff --git a/wear/src/main/java/com/example/wear/snippets/auth/OAuthDAG.kt b/wear/src/main/java/com/example/wear/snippets/auth/OAuthDAG.kt
new file mode 100644
index 000000000..04de6fe53
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/auth/OAuthDAG.kt
@@ -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 {
+ // 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 {
+ return Result.success("placeholderToken")
+ }
+
+ private fun retrieveUserProfile(token: Result): Result {
+ return Result.success("placeholderProfile")
+ }
+}
diff --git a/wear/src/main/java/com/example/wear/snippets/auth/OAuthPKCE.kt b/wear/src/main/java/com/example/wear/snippets/auth/OAuthPKCE.kt
new file mode 100644
index 000000000..7324d4e0b
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/auth/OAuthPKCE.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.net.Uri
+import androidx.wear.phone.interactions.authentication.CodeChallenge
+import androidx.wear.phone.interactions.authentication.CodeVerifier
+import androidx.wear.phone.interactions.authentication.OAuthRequest
+import androidx.wear.phone.interactions.authentication.OAuthResponse
+import androidx.wear.phone.interactions.authentication.RemoteAuthClient
+import java.io.IOException
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+private const val CLIENT_ID = "my_fake_client_id"
+private const val GOOGLE_OAUTH_BACKEND = "https://accounts.google.com/o/oauth2/v2/auth"
+private const val GOOGLE_USER_INFO_SCOPE = "https://www.googleapis.com/auth/userinfo.profile"
+
+class WearOAuthPKCEManager(private val context: Context) {
+
+ /** Start the authentication flow. */
+ suspend fun startAuthFlow() {
+ val codeChallenge = CodeChallenge(CodeVerifier())
+
+ // Create the authorization Uri that will be shown to the user on the phone. This will
+ // be different depending on the OAuth backend your app uses, we use the google backend.
+ val uri = Uri.Builder()
+ .encodedPath(GOOGLE_OAUTH_BACKEND)
+ .appendQueryParameter("scope", GOOGLE_USER_INFO_SCOPE)
+ .build()
+
+ // [START android_wear_auth_oauth_pkce_create_request]
+ val oauthRequest = OAuthRequest.Builder(context)
+ .setAuthProviderUrl(uri)
+ .setCodeChallenge(codeChallenge)
+ .setClientId(CLIENT_ID)
+ .build()
+ // [END android_wear_auth_oauth_pkce_create_request]
+
+ // 1. Retrieve oauth code
+ val code = retrieveOAuthCode(oauthRequest, context).getOrElse {}
+
+ // 2. Retrieve auth token from your backend.
+ val token = retrieveToken(code as String, oauthRequest).getOrElse {}
+
+ // 3. Request user profile from your backend
+ retrieveUserProfile(token as String).getOrElse {}
+ }
+
+ /**
+ * Use the [RemoteAuthClient] class to authorize the user. The library will handle the
+ * communication with the paired device, where the user can log in.
+ */
+ private suspend fun retrieveOAuthCode(
+ oauthRequest: OAuthRequest,
+ context: Context
+ ): Result {
+ return suspendCoroutine { continuation ->
+ // [START android_wear_auth_oauth_pkce_send_request]
+ RemoteAuthClient.create(context).sendAuthorizationRequest(
+ request = oauthRequest,
+ executor = { command -> command?.run() },
+ clientCallback = object : RemoteAuthClient.Callback() {
+ override fun onAuthorizationResponse(
+ request: OAuthRequest,
+ response: OAuthResponse
+ ) {
+ // Extract the token from the response, store it, and use it in requests.
+ continuation.resume(parseCodeFromResponse(response))
+ }
+ override fun onAuthorizationError(request: OAuthRequest, errorCode: Int) {
+ // Handle Errors
+ continuation.resume(Result.failure(IOException("Authorization failed")))
+ }
+ }
+ )
+ // [END android_wear_auth_oauth_pkce_send_request]
+ }
+ }
+
+ private fun parseCodeFromResponse(response: OAuthResponse): Result {
+ val responseUrl = response.responseUrl
+ val code = responseUrl?.getQueryParameter("code")
+ return if (code.isNullOrBlank()) {
+ Result.failure(IOException("Authorization failed"))
+ } else {
+ Result.success(code)
+ }
+ }
+
+ /** placeholder token retrieval function. */
+ private fun retrieveToken(code: String, oauthRequest: OAuthRequest): Result {
+ return Result.success("placeholderToken")
+ }
+
+ /** placeholder user profile retrieval. */
+ private fun retrieveUserProfile(token: String): Result {
+ return Result.success("placeholderProfile")
+ }
+}
diff --git a/wear/src/main/java/com/example/wear/snippets/auth/WristDetectionUtility.kt b/wear/src/main/java/com/example/wear/snippets/auth/WristDetectionUtility.kt
new file mode 100644
index 000000000..12a636aa5
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/auth/WristDetectionUtility.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+import android.app.KeyguardManager
+import android.content.Context
+import androidx.core.content.getSystemService
+
+// This setting differs across OEMs, so you may want to check several in your detection function
+// This one works for the pixel watch.
+private const val PIXEL_WRIST_AUTOLOCK_SETTING_STATE = "wrist_detection_auto_locking_enabled"
+
+// [START android_wear_auth_wrist_detection]
+fun isWristDetectionAutoLockingEnabled(context: Context): Boolean {
+ // [END android_wear_auth_wrist_detection]
+ // Use the keyguard manager to check for the presence of a lock mechanism
+ val keyguardManager = context.getSystemService()
+ val isSecured = keyguardManager?.isDeviceSecure == true
+
+ // Use OEM-specific system settings to verify that on-body autolock is enabled.
+ val isWristDetectionOn = android.provider.Settings.Global.getInt(
+ context.contentResolver, PIXEL_WRIST_AUTOLOCK_SETTING_STATE,
+ 0
+ ) == 1
+
+ return isSecured && isWristDetectionOn
+}
diff --git a/wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerActivity.kt b/wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerActivity.kt
index ecf3ff7e3..a0aebf591 100644
--- a/wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerActivity.kt
+++ b/wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerActivity.kt
@@ -124,6 +124,20 @@ private fun Context.sendImagePutDataMapRequest(): Task {
}
// [END android_wear_datalayer_imageputdatamap]
+private fun Context.sendAuthTokenPutDataMapRequest(): Task {
+ // [START android_wear_datalayer_auth_token_sharing]
+
+ val token = "..." // Auth token to transmit to the Wear OS device.
+ val putDataReq: PutDataRequest = PutDataMapRequest.create("/auth").run {
+ dataMap.putString("token", token)
+ asPutDataRequest()
+ }
+ val putDataTask: Task = Wearable.getDataClient(this).putDataItem(putDataReq)
+ // [END android_wear_datalayer_auth_token_sharing]
+
+ return putDataTask
+}
+
class DataLayerActivity2 : ComponentActivity(), DataClient.OnDataChangedListener {
// [START android_wear_datalayer_ondatachanged_assetextract]
override fun onDataChanged(dataEvents: DataEventBuffer) {
diff --git a/wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerService.kt b/wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerService.kt
index c69d5114b..5197db3ae 100644
--- a/wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerService.kt
+++ b/wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerService.kt
@@ -21,6 +21,7 @@ import android.util.Log
import com.google.android.gms.wearable.DataClient
import com.google.android.gms.wearable.DataEvent
import com.google.android.gms.wearable.DataEventBuffer
+import com.google.android.gms.wearable.DataMapItem
import com.google.android.gms.wearable.Wearable
import com.google.android.gms.wearable.WearableListenerService
@@ -58,6 +59,29 @@ class DataLayerListenerService : WearableListenerService() {
}
// [END android_wear_datalayer_datalayerlistenerservice]
+// [START android_wear_datalayer_auth_token_sharing_listener]
+class AuthDataListenerService : WearableListenerService() {
+ override fun onDataChanged(dataEvents: DataEventBuffer) {
+ dataEvents.forEach { event ->
+ if (event.type == DataEvent.TYPE_CHANGED) {
+ val dataItemPath = event.dataItem.uri.path ?: ""
+
+ if (dataItemPath.startsWith("/auth")) {
+ val token = DataMapItem.fromDataItem(event.dataItem)
+ .dataMap
+ .getString("token")
+ // Display an interstitial screen to notify the user that they're being signed
+ // in. Then, store the token and use it in network requests.
+// [END android_wear_datalayer_auth_token_sharing_listener]
+ handleSignInSequence(token)
+ }
+ }
+ }
+ }
+ /** placeholder sign in handler. */
+ fun handleSignInSequence(token: String?) {}
+}
+
// [START android_wear_datalayer_ondatachangedlisteneer]
class MainActivity : Activity(), DataClient.OnDataChangedListener {