diff --git a/.github/workflows/apply_spotless.yml b/.github/workflows/apply_spotless.yml index 0c8dcce4f..d69f817b0 100644 --- a/.github/workflows/apply_spotless.yml +++ b/.github/workflows/apply_spotless.yml @@ -42,16 +42,7 @@ jobs: java-version: '17' - name: Run spotlessApply - run: ./gradlew :compose:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace - - - name: Run spotlessApply for Wear - run: ./gradlew :wear:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace - - - name: Run spotlessApply for Misc - run: ./gradlew :misc:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace - - - name: Run spotlessApply for XR - run: ./gradlew :xr:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace + run: ./gradlew spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace - name: Auto-commit if spotlessApply has changes uses: stefanzweifel/git-auto-commit-action@v5 diff --git a/.gitignore b/.gitignore index 30e5f7bdc..9b10be5e8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ /.idea/modules.xml /.idea/workspace.xml .DS_Store -/build +build /captures .externalNativeBuild .idea/* diff --git a/identity/credentialmanager/build.gradle.kts b/identity/credentialmanager/build.gradle.kts index b0a93839c..2e35608a7 100644 --- a/identity/credentialmanager/build.gradle.kts +++ b/identity/credentialmanager/build.gradle.kts @@ -39,6 +39,13 @@ android { buildFeatures { compose = true } + sourceSets { + named("main") { + java { + srcDir("src/main/java") + } + } + } } dependencies { diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialManagerHandler.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialManagerHandler.kt index 36f4ee175..381bc8fc3 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialManagerHandler.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialManagerHandler.kt @@ -1,3 +1,19 @@ +/* + * 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.identity.credentialmanager import android.app.Activity @@ -13,39 +29,39 @@ import androidx.credentials.exceptions.GetCredentialException // This class is mostly copied from https://github.com/android/identity-samples/blob/main/WebView/CredentialManagerWebView/CredentialManagerHandler.kt. class CredentialManagerHandler(private val activity: Activity) { - private val mCredMan = CredentialManager.create(activity.applicationContext) - private val TAG = "CredentialManagerHandler" - /** - * Encapsulates the create passkey API for credential manager in a less error-prone manner. - * - * @param request a create public key credential request JSON required by [CreatePublicKeyCredentialRequest]. - * @return [CreatePublicKeyCredentialResponse] containing the result of the credential creation. - */ - suspend fun createPasskey(request: String): CreatePublicKeyCredentialResponse { - val createRequest = CreatePublicKeyCredentialRequest(request) - try { - return mCredMan.createCredential(activity, createRequest) as CreatePublicKeyCredentialResponse - } catch (e: CreateCredentialException) { - // For error handling use guidance from https://developer.android.com/training/sign-in/passkeys - Log.i(TAG, "Error creating credential: ErrMessage: ${e.errorMessage}, ErrType: ${e.type}") - throw e + private val mCredMan = CredentialManager.create(activity.applicationContext) + private val TAG = "CredentialManagerHandler" + /** + * Encapsulates the create passkey API for credential manager in a less error-prone manner. + * + * @param request a create public key credential request JSON required by [CreatePublicKeyCredentialRequest]. + * @return [CreatePublicKeyCredentialResponse] containing the result of the credential creation. + */ + suspend fun createPasskey(request: String): CreatePublicKeyCredentialResponse { + val createRequest = CreatePublicKeyCredentialRequest(request) + try { + return mCredMan.createCredential(activity, createRequest) as CreatePublicKeyCredentialResponse + } catch (e: CreateCredentialException) { + // For error handling use guidance from https://developer.android.com/training/sign-in/passkeys + Log.i(TAG, "Error creating credential: ErrMessage: ${e.errorMessage}, ErrType: ${e.type}") + throw e + } } - } - /** - * Encapsulates the get passkey API for credential manager in a less error-prone manner. - * - * @param request a get public key credential request JSON required by [GetCredentialRequest]. - * @return [GetCredentialResponse] containing the result of the credential retrieval. - */ - suspend fun getPasskey(request: String): GetCredentialResponse { - val getRequest = GetCredentialRequest(listOf(GetPublicKeyCredentialOption(request, null))) - try { - return mCredMan.getCredential(activity, getRequest) - } catch (e: GetCredentialException) { - // For error handling use guidance from https://developer.android.com/training/sign-in/passkeys - Log.i(TAG, "Error retrieving credential: ${e.message}") - throw e + /** + * Encapsulates the get passkey API for credential manager in a less error-prone manner. + * + * @param request a get public key credential request JSON required by [GetCredentialRequest]. + * @return [GetCredentialResponse] containing the result of the credential retrieval. + */ + suspend fun getPasskey(request: String): GetCredentialResponse { + val getRequest = GetCredentialRequest(listOf(GetPublicKeyCredentialOption(request, null))) + try { + return mCredMan.getCredential(activity, getRequest) + } catch (e: GetCredentialException) { + // For error handling use guidance from https://developer.android.com/training/sign-in/passkeys + Log.i(TAG, "Error retrieving credential: ${e.message}") + throw e + } } - } } diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialProviderDummyActivity.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialProviderDummyActivity.kt index 04e4fb91a..ed65bd831 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialProviderDummyActivity.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialProviderDummyActivity.kt @@ -1,3 +1,19 @@ +/* + * 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.identity.credentialmanager import android.annotation.SuppressLint @@ -46,7 +62,7 @@ import java.security.spec.ECParameterSpec import java.security.spec.ECPoint import java.security.spec.EllipticCurve -class CredentialProviderDummyActivity: FragmentActivity() { +class CredentialProviderDummyActivity : FragmentActivity() { private val PERSONAL_ACCOUNT_ID: String = "" private val FAMILY_ACCOUNT_ID: String = "" @@ -85,7 +101,7 @@ class CredentialProviderDummyActivity: FragmentActivity() { val biometricPrompt = BiometricPrompt( this, - { }, // Pass in your own executor + { }, // Pass in your own executor object : AuthenticationCallback() { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) @@ -109,7 +125,7 @@ class CredentialProviderDummyActivity: FragmentActivity() { // Generate a credential key pair val spec = ECGenParameterSpec("secp256r1") - val keyPairGen = KeyPairGenerator.getInstance("EC"); + val keyPairGen = KeyPairGenerator.getInstance("EC") keyPairGen.initialize(spec) val keyPair = keyPairGen.genKeyPair() @@ -165,7 +181,7 @@ class CredentialProviderDummyActivity: FragmentActivity() { @RequiresApi(VERSION_CODES.P) fun appInfoToOrigin(info: CallingAppInfo): String { val cert = info.signingInfo.apkContentsSigners[0].toByteArray() - val md = MessageDigest.getInstance("SHA-256"); + val md = MessageDigest.getInstance("SHA-256") val certHash = md.digest(cert) // This is the format for origin return "android:apk-key-hash:${b64Encode(certHash)}" @@ -240,7 +256,7 @@ class CredentialProviderDummyActivity: FragmentActivity() { ) ) - //Set the final response back + // Set the final response back val result = Intent() val response = CreatePasswordResponse() PendingIntentHandler.setCreateCredentialResponse(result, response) @@ -300,10 +316,11 @@ class CredentialProviderDummyActivity: FragmentActivity() { val biometricPrompt = BiometricPrompt( this, - { }, // Pass in your own executor + { }, // Pass in your own executor object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationError( - errorCode: Int, errString: CharSequence + errorCode: Int, + errString: CharSequence ) { super.onAuthenticationError(errorCode, errString) finish() @@ -330,7 +347,7 @@ class CredentialProviderDummyActivity: FragmentActivity() { packageName = packageName ) - val sig = Signature.getInstance("SHA256withECDSA"); + val sig = Signature.getInstance("SHA256withECDSA") sig.initSign(privateKey) sig.update(response.dataToSign()) response.signature = sig.sign() @@ -401,9 +418,7 @@ class CredentialProviderDummyActivity: FragmentActivity() { } // [START android_identity_credential_pending_intent] - fun createSettingsPendingIntent(): PendingIntent - // [END android_identity_credential_pending_intent] - { + fun createSettingsPendingIntent(): PendingIntent { // [END android_identity_credential_pending_intent] return PendingIntent.getBroadcast(this, 0, Intent(), PendingIntent.FLAG_IMMUTABLE) } @@ -468,7 +483,7 @@ data class CredentialsInfo( val passwords: List = listOf() ) -class ECPrivateKeyImpl: ECPrivateKey { +class ECPrivateKeyImpl : ECPrivateKey { override fun getAlgorithm(): String = "" override fun getFormat(): String = "" override fun getEncoded(): ByteArray = byteArrayOf() diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/Fido2ToCredmanMigration.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/Fido2ToCredmanMigration.kt index 2e21bec4d..05b06cce7 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/Fido2ToCredmanMigration.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/Fido2ToCredmanMigration.kt @@ -1,3 +1,19 @@ +/* + * 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.identity.credentialmanager import android.app.Activity @@ -16,6 +32,7 @@ import androidx.credentials.GetPublicKeyCredentialOption import androidx.credentials.PublicKeyCredential import androidx.credentials.exceptions.CreateCredentialException import com.example.identity.credentialmanager.ApiResult.Success +import java.io.StringWriter import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.Request.Builder @@ -24,7 +41,6 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import okhttp3.ResponseBody import org.json.JSONObject -import java.io.StringWriter import ru.gildor.coroutines.okhttp.await class Fido2ToCredmanMigration( @@ -44,12 +60,15 @@ class Fido2ToCredmanMigration( // ... val call = client.newCall( Builder() - .method("POST", jsonRequestBody { - name("attestation").value("none") - name("authenticatorSelection").objectValue { - name("residentKey").value("required") + .method( + "POST", + jsonRequestBody { + name("attestation").value("none") + name("authenticatorSelection").objectValue { + name("residentKey").value("required") + } } - }).build() + ).build() ) // ... } @@ -61,14 +80,17 @@ class Fido2ToCredmanMigration( Builder() .url("$BASE_URL/") .addHeader("Cookie", formatCookie(sessionId)) - .method("POST", jsonRequestBody { - name("attestation").value("none") - name("authenticatorSelection").objectValue { - name("authenticatorAttachment").value("platform") - name("userVerification").value("required") - name("residentKey").value("required") + .method( + "POST", + jsonRequestBody { + name("attestation").value("none") + name("authenticatorSelection").objectValue { + name("authenticatorAttachment").value("platform") + name("userVerification").value("required") + name("residentKey").value("required") + } } - }).build() + ).build() ) val response = call.await() return response.result("Error calling the api") { @@ -108,10 +130,13 @@ class Fido2ToCredmanMigration( * @return a JSON object. */ suspend fun signinRequest(): ApiResult { - val call = client.newCall(Builder().url(buildString { - append("$BASE_URL/signinRequest") - }).method("POST", jsonRequestBody {}) - .build() + val call = client.newCall( + Builder().url( + buildString { + append("$BASE_URL/signinRequest") + } + ).method("POST", jsonRequestBody {}) + .build() ) val response = call.await() return response.result("Error calling /signinRequest") { @@ -129,31 +154,36 @@ class Fido2ToCredmanMigration( * including the newly-registered one. */ suspend fun signinResponse( - sessionId: String, response: JSONObject, credentialId: String + sessionId: String, + response: JSONObject, + credentialId: String ): ApiResult { val call = client.newCall( Builder().url("$BASE_URL/signinResponse") - .addHeader("Cookie",formatCookie(sessionId)) - .method("POST", jsonRequestBody { - name("id").value(credentialId) - name("type").value(PUBLIC_KEY.toString()) - name("rawId").value(credentialId) - name("response").objectValue { - name("clientDataJSON").value( - response.getString("clientDataJSON") - ) - name("authenticatorData").value( - response.getString("authenticatorData") - ) - name("signature").value( - response.getString("signature") - ) - name("userHandle").value( - response.getString("userHandle") - ) + .addHeader("Cookie", formatCookie(sessionId)) + .method( + "POST", + jsonRequestBody { + name("id").value(credentialId) + name("type").value(PUBLIC_KEY.toString()) + name("rawId").value(credentialId) + name("response").objectValue { + name("clientDataJSON").value( + response.getString("clientDataJSON") + ) + name("authenticatorData").value( + response.getString("authenticatorData") + ) + name("signature").value( + response.getString("signature") + ) + name("userHandle").value( + response.getString("userHandle") + ) + } } - }).build() + ).build() ) val apiResponse = call.await() return apiResponse.result("Error calling /signingResponse") { @@ -169,11 +199,12 @@ class Fido2ToCredmanMigration( Toast.makeText( activity, "Fetching previously stored credentials", - Toast.LENGTH_SHORT) + Toast.LENGTH_SHORT + ) .show() var result: GetCredentialResponse? = null try { - val request= GetCredentialRequest( + val request = GetCredentialRequest( listOf( GetPublicKeyCredentialOption( creationResult.toString(), @@ -234,7 +265,7 @@ class Fido2ToCredmanMigration( } sealed class ApiResult { - class Success: ApiResult() + class Success : ApiResult() } class ApiException(message: String) : RuntimeException(message) diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/MyCredentialProviderService.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/MyCredentialProviderService.kt index 50beedaeb..77db763d3 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/MyCredentialProviderService.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/MyCredentialProviderService.kt @@ -1,3 +1,19 @@ +/* + * 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.identity.credentialmanager import android.annotation.SuppressLint @@ -33,7 +49,7 @@ import androidx.credentials.provider.PublicKeyCredentialEntry import androidx.credentials.webauthn.PublicKeyCredentialRequestOptions @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) -class MyCredentialProviderService: CredentialProviderService() { +class MyCredentialProviderService : CredentialProviderService() { private val PERSONAL_ACCOUNT_ID: String = "" private val FAMILY_ACCOUNT_ID: String = "" private val CREATE_PASSKEY_INTENT: String = "" @@ -78,15 +94,19 @@ class MyCredentialProviderService: CredentialProviderService() { // account, and one for storing them to the 'Family' account. These // accounts are local to this sample app only. val createEntries: MutableList = mutableListOf() - createEntries.add( CreateEntry( - PERSONAL_ACCOUNT_ID, - createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSKEY_INTENT) - )) + createEntries.add( + CreateEntry( + PERSONAL_ACCOUNT_ID, + createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSKEY_INTENT) + ) + ) - createEntries.add( CreateEntry( - FAMILY_ACCOUNT_ID, - createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSKEY_INTENT) - )) + createEntries.add( + CreateEntry( + FAMILY_ACCOUNT_ID, + createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSKEY_INTENT) + ) + ) return BeginCreateCredentialResponse(createEntries) } @@ -101,7 +121,8 @@ class MyCredentialProviderService: CredentialProviderService() { return PendingIntent.getActivity( applicationContext, UNIQUE_REQ_CODE, - intent, ( + intent, + ( PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) @@ -120,10 +141,12 @@ class MyCredentialProviderService: CredentialProviderService() { callback: OutcomeReceiver, ) { if (isAppLocked()) { - callback.onResult(BeginGetCredentialResponse( - authenticationActions = mutableListOf( - AuthenticationAction( - unlockEntryTitle, createUnlockPendingIntent()) + callback.onResult( + BeginGetCredentialResponse( + authenticationActions = mutableListOf( + AuthenticationAction( + unlockEntryTitle, createUnlockPendingIntent() + ) ) ) ) @@ -142,7 +165,8 @@ class MyCredentialProviderService: CredentialProviderService() { private fun createUnlockPendingIntent(): PendingIntent { val intent = Intent(UNLOCK_INTENT).setPackage(PACKAGE_NAME) return PendingIntent.getActivity( - applicationContext, UNIQUE_REQUEST_CODE, intent, ( + applicationContext, UNIQUE_REQUEST_CODE, intent, + ( PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) @@ -156,7 +180,6 @@ class MyCredentialProviderService: CredentialProviderService() { // that are to be invoked through the PendingIntent(s) private const val GET_PASSKEY_INTENT_ACTION = "PACKAGE_NAME.GET_PASSKEY" private const val GET_PASSWORD_INTENT_ACTION = "PACKAGE_NAME.GET_PASSWORD" - } fun processGetCredentialRequest( diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt index f6dfe4485..747f72c15 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt @@ -14,7 +14,6 @@ * limitations under the License. */ - package com.example.identity.credentialmanager import android.content.Context @@ -46,283 +45,287 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runBlocking import org.json.JSONObject -class PasskeyAndPasswordFunctions ( - context: Context, +class PasskeyAndPasswordFunctions( + context: Context, ) { - // [START android_identity_initialize_credman] - // Use your app or activity context to instantiate a client instance of - // CredentialManager. - private val credentialManager = CredentialManager.create(context) - // [END android_identity_initialize_credman] - private val activityContext = context + // [START android_identity_initialize_credman] + // Use your app or activity context to instantiate a client instance of + // CredentialManager. + private val credentialManager = CredentialManager.create(context) + // [END android_identity_initialize_credman] + private val activityContext = context - // Placeholder for TAG log value. - val TAG = "" - /** - * Retrieves a passkey from the credential manager. - * - * @param creationResult The result of the passkey creation operation. - * @param context The activity context from the Composable, to be used in Credential Manager APIs - * @return The [GetCredentialResponse] object containing the passkey, or null if an error occurred. - */ - @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - fun signInFlow( - creationResult: JSONObject - ) { - val requestJson = creationResult.toString() - // [START android_identity_get_password_passkey_options] - // Retrieves the user's saved password for your app from their - // password provider. - val getPasswordOption = GetPasswordOption() + // Placeholder for TAG log value. + val TAG = "" + /** + * Retrieves a passkey from the credential manager. + * + * @param creationResult The result of the passkey creation operation. + * @param context The activity context from the Composable, to be used in Credential Manager APIs + * @return The [GetCredentialResponse] object containing the passkey, or null if an error occurred. + */ + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + fun signInFlow( + creationResult: JSONObject + ) { + val requestJson = creationResult.toString() + // [START android_identity_get_password_passkey_options] + // Retrieves the user's saved password for your app from their + // password provider. + val getPasswordOption = GetPasswordOption() - // Get passkey from the user's public key credential provider. - val getPublicKeyCredentialOption = GetPublicKeyCredentialOption( - requestJson = requestJson - ) - // [END android_identity_get_password_passkey_options] - var result: GetCredentialResponse - // [START android_identity_get_credential_request] - val credentialRequest = GetCredentialRequest( - listOf(getPasswordOption, getPublicKeyCredentialOption), - ) - // [END android_identity_get_credential_request] - runBlocking { - // getPrepareCredential request - // [START android_identity_prepare_get_credential] - coroutineScope { - val response = credentialManager.prepareGetCredential( - GetCredentialRequest( - listOf( - getPublicKeyCredentialOption, - getPasswordOption - ) - ) + // Get passkey from the user's public key credential provider. + val getPublicKeyCredentialOption = GetPublicKeyCredentialOption( + requestJson = requestJson ) - } - // [END android_identity_prepare_get_credential] - // getCredential request without handling exception. - // [START android_identity_launch_sign_in_flow_1] - coroutineScope { - try { - result = credentialManager.getCredential( - // Use an activity-based context to avoid undefined system UI - // launching behavior. - context = activityContext, - request = credentialRequest - ) - handleSignIn(result) - } catch (e: GetCredentialException) { - // Handle failure - } - } - // [END android_identity_launch_sign_in_flow_1] - // getCredential request adding some exception handling. - // [START android_identity_handle_exceptions_no_credential] - coroutineScope { - try { - result = credentialManager.getCredential( - context = activityContext, - request = credentialRequest - ) - } catch (e: GetCredentialException) { - Log.e("CredentialManager", "No credential available", e) + // [END android_identity_get_password_passkey_options] + var result: GetCredentialResponse + // [START android_identity_get_credential_request] + val credentialRequest = GetCredentialRequest( + listOf(getPasswordOption, getPublicKeyCredentialOption), + ) + // [END android_identity_get_credential_request] + runBlocking { + // getPrepareCredential request + // [START android_identity_prepare_get_credential] + coroutineScope { + val response = credentialManager.prepareGetCredential( + GetCredentialRequest( + listOf( + getPublicKeyCredentialOption, + getPasswordOption + ) + ) + ) + } + // [END android_identity_prepare_get_credential] + // getCredential request without handling exception. + // [START android_identity_launch_sign_in_flow_1] + coroutineScope { + try { + result = credentialManager.getCredential( + // Use an activity-based context to avoid undefined system UI + // launching behavior. + context = activityContext, + request = credentialRequest + ) + handleSignIn(result) + } catch (e: GetCredentialException) { + // Handle failure + } + } + // [END android_identity_launch_sign_in_flow_1] + // getCredential request adding some exception handling. + // [START android_identity_handle_exceptions_no_credential] + coroutineScope { + try { + result = credentialManager.getCredential( + context = activityContext, + request = credentialRequest + ) + } catch (e: GetCredentialException) { + Log.e("CredentialManager", "No credential available", e) + } + } + // [END android_identity_handle_exceptions_no_credential] } - } - // [END android_identity_handle_exceptions_no_credential] } - } - fun autofillImplementation( - requestJson: String - ) { - // [START android_identity_autofill_construct_request] - // Retrieves the user's saved password for your app. - val getPasswordOption = GetPasswordOption() + fun autofillImplementation( + requestJson: String + ) { + // [START android_identity_autofill_construct_request] + // Retrieves the user's saved password for your app. + val getPasswordOption = GetPasswordOption() - // Get a passkey from the user's public key credential provider. - val getPublicKeyCredentialOption = GetPublicKeyCredentialOption( - requestJson = requestJson - ) + // Get a passkey from the user's public key credential provider. + val getPublicKeyCredentialOption = GetPublicKeyCredentialOption( + requestJson = requestJson + ) - val getCredRequest = GetCredentialRequest( - listOf(getPasswordOption, getPublicKeyCredentialOption) - ) - // [END android_identity_autofill_construct_request] + val getCredRequest = GetCredentialRequest( + listOf(getPasswordOption, getPublicKeyCredentialOption) + ) + // [END android_identity_autofill_construct_request] - runBlocking { - // [START android_identity_autofill_get_credential_api] - coroutineScope { - try { - val result = credentialManager.getCredential( - context = activityContext, // Use an activity-based context. - request = getCredRequest - ) - handleSignIn(result); - } catch (e: GetCredentialException) { - handleFailure(e); + runBlocking { + // [START android_identity_autofill_get_credential_api] + coroutineScope { + try { + val result = credentialManager.getCredential( + context = activityContext, // Use an activity-based context. + request = getCredRequest + ) + handleSignIn(result) + } catch (e: GetCredentialException) { + handleFailure(e) + } + } + // [END android_identity_autofill_get_credential_api] } - } - // [END android_identity_autofill_get_credential_api] - } - val usernameEditText: androidx.appcompat.widget.AppCompatEditText = AppCompatEditText(activityContext) - val passwordEditText: androidx.appcompat.widget.AppCompatEditText = AppCompatEditText(activityContext) + val usernameEditText: androidx.appcompat.widget.AppCompatEditText = AppCompatEditText(activityContext) + val passwordEditText: androidx.appcompat.widget.AppCompatEditText = AppCompatEditText(activityContext) - // [START android_identity_autofill_enable_edit_text] - usernameEditText.pendingGetCredentialRequest = PendingGetCredentialRequest( - getCredRequest) { response -> handleSignIn(response) - } + // [START android_identity_autofill_enable_edit_text] + usernameEditText.pendingGetCredentialRequest = PendingGetCredentialRequest( + getCredRequest + ) { response -> + handleSignIn(response) + } - passwordEditText.pendingGetCredentialRequest = PendingGetCredentialRequest( - getCredRequest) { response -> handleSignIn(response) + passwordEditText.pendingGetCredentialRequest = PendingGetCredentialRequest( + getCredRequest + ) { response -> + handleSignIn(response) + } + // [END android_identity_autofill_enable_edit_text] } - // [END android_identity_autofill_enable_edit_text] - } - // [START android_identity_launch_sign_in_flow_2] - fun handleSignIn(result: GetCredentialResponse) { - // Handle the successfully returned credential. - val credential = result.credential + // [START android_identity_launch_sign_in_flow_2] + fun handleSignIn(result: GetCredentialResponse) { + // Handle the successfully returned credential. + val credential = result.credential - when (credential) { - is PublicKeyCredential -> { - val responseJson = credential.authenticationResponseJson - // Share responseJson i.e. a GetCredentialResponse on your server to - // validate and authenticate - } + when (credential) { + is PublicKeyCredential -> { + val responseJson = credential.authenticationResponseJson + // Share responseJson i.e. a GetCredentialResponse on your server to + // validate and authenticate + } - is PasswordCredential -> { - val username = credential.id - val password = credential.password - // Use id and password to send to your server to validate - // and authenticate - } + is PasswordCredential -> { + val username = credential.id + val password = credential.password + // Use id and password to send to your server to validate + // and authenticate + } - is CustomCredential -> { - // If you are also using any external sign-in libraries, parse them - // here with the utility functions provided. - if (credential.type == ExampleCustomCredential.TYPE) { - try { - val ExampleCustomCredential = - ExampleCustomCredential.createFrom(credential.data) - // Extract the required credentials and complete the authentication as per - // the federated sign in or any external sign in library flow - } catch (e: ExampleCustomCredential.ExampleCustomCredentialParsingException) { - // Unlikely to happen. If it does, you likely need to update the dependency - // version of your external sign-in library. - Log.e(TAG, "Failed to parse an ExampleCustomCredential", e) - } - } else { - // Catch any unrecognized custom credential type here. - Log.e(TAG, "Unexpected type of credential") + is CustomCredential -> { + // If you are also using any external sign-in libraries, parse them + // here with the utility functions provided. + if (credential.type == ExampleCustomCredential.TYPE) { + try { + val ExampleCustomCredential = + ExampleCustomCredential.createFrom(credential.data) + // Extract the required credentials and complete the authentication as per + // the federated sign in or any external sign in library flow + } catch (e: ExampleCustomCredential.ExampleCustomCredentialParsingException) { + // Unlikely to happen. If it does, you likely need to update the dependency + // version of your external sign-in library. + Log.e(TAG, "Failed to parse an ExampleCustomCredential", e) + } + } else { + // Catch any unrecognized custom credential type here. + Log.e(TAG, "Unexpected type of credential") + } + } + else -> { + // Catch any unrecognized credential type here. + Log.e(TAG, "Unexpected type of credential") + } } - } - else -> { - // Catch any unrecognized credential type here. - Log.e(TAG, "Unexpected type of credential") - } } - } - // [END android_identity_launch_sign_in_flow_2] + // [END android_identity_launch_sign_in_flow_2] - // [START android_identity_create_passkey] - suspend fun createPasskey(requestJson: String, preferImmediatelyAvailableCredentials: Boolean) { - val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest( - // Contains the request in JSON format. Uses the standard WebAuthn - // web JSON spec. - requestJson = requestJson, - // Defines whether you prefer to use only immediately available - // credentials, not hybrid credentials, to fulfill this request. - // This value is false by default. - preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials, - ) - - // Execute CreateCredentialRequest asynchronously to register credentials - // for a user account. Handle success and failure cases with the result and - // exceptions, respectively. - coroutineScope { - try { - val result = credentialManager.createCredential( - // Use an activity-based context to avoid undefined system - // UI launching behavior - context = activityContext, - request = createPublicKeyCredentialRequest, + // [START android_identity_create_passkey] + suspend fun createPasskey(requestJson: String, preferImmediatelyAvailableCredentials: Boolean) { + val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest( + // Contains the request in JSON format. Uses the standard WebAuthn + // web JSON spec. + requestJson = requestJson, + // Defines whether you prefer to use only immediately available + // credentials, not hybrid credentials, to fulfill this request. + // This value is false by default. + preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials, ) - // Handle passkey creation result - } catch (e : CreateCredentialException){ - handleFailure(e) - } + + // Execute CreateCredentialRequest asynchronously to register credentials + // for a user account. Handle success and failure cases with the result and + // exceptions, respectively. + coroutineScope { + try { + val result = credentialManager.createCredential( + // Use an activity-based context to avoid undefined system + // UI launching behavior + context = activityContext, + request = createPublicKeyCredentialRequest, + ) + // Handle passkey creation result + } catch (e: CreateCredentialException) { + handleFailure(e) + } + } } - } - // [END android_identity_create_passkey] + // [END android_identity_create_passkey] - // [START android_identity_handle_create_passkey_failure] - fun handleFailure(e: CreateCredentialException) { - when (e) { - is CreatePublicKeyCredentialDomException -> { - // Handle the passkey DOM errors thrown according to the - // WebAuthn spec. - } - is CreateCredentialCancellationException -> { - // The user intentionally canceled the operation and chose not - // to register the credential. - } - is CreateCredentialInterruptedException -> { - // Retry-able error. Consider retrying the call. - } - is CreateCredentialProviderConfigurationException -> { - // Your app is missing the provider configuration dependency. - // Most likely, you're missing the - // "credentials-play-services-auth" module. - } - is CreateCredentialCustomException -> { - // You have encountered an error from a 3rd-party SDK. If you - // make the API call with a request object that's a subclass of - // CreateCustomCredentialRequest using a 3rd-party SDK, then you - // should check for any custom exception type constants within - // that SDK to match with e.type. Otherwise, drop or log the - // exception. - } - else -> Log.w(TAG, "Unexpected exception type ${e::class.java.name}") + // [START android_identity_handle_create_passkey_failure] + fun handleFailure(e: CreateCredentialException) { + when (e) { + is CreatePublicKeyCredentialDomException -> { + // Handle the passkey DOM errors thrown according to the + // WebAuthn spec. + } + is CreateCredentialCancellationException -> { + // The user intentionally canceled the operation and chose not + // to register the credential. + } + is CreateCredentialInterruptedException -> { + // Retry-able error. Consider retrying the call. + } + is CreateCredentialProviderConfigurationException -> { + // Your app is missing the provider configuration dependency. + // Most likely, you're missing the + // "credentials-play-services-auth" module. + } + is CreateCredentialCustomException -> { + // You have encountered an error from a 3rd-party SDK. If you + // make the API call with a request object that's a subclass of + // CreateCustomCredentialRequest using a 3rd-party SDK, then you + // should check for any custom exception type constants within + // that SDK to match with e.type. Otherwise, drop or log the + // exception. + } + else -> Log.w(TAG, "Unexpected exception type ${e::class.java.name}") + } } - } - // [END android_identity_handle_create_passkey_failure] + // [END android_identity_handle_create_passkey_failure] - fun handleFailure(e: GetCredentialException) { } + fun handleFailure(e: GetCredentialException) { } - // [START android_identity_register_password] - suspend fun registerPassword(username: String, password: String) { - // Initialize a CreatePasswordRequest object. - val createPasswordRequest = - CreatePasswordRequest(id = username, password = password) + // [START android_identity_register_password] + suspend fun registerPassword(username: String, password: String) { + // Initialize a CreatePasswordRequest object. + val createPasswordRequest = + CreatePasswordRequest(id = username, password = password) - // Create credential and handle result. - coroutineScope { - try { - val result = - credentialManager.createCredential( - // Use an activity based context to avoid undefined - // system UI launching behavior. - activityContext, - createPasswordRequest - ) - // Handle register password result - } catch (e: CreateCredentialException) { - handleFailure(e) - } + // Create credential and handle result. + coroutineScope { + try { + val result = + credentialManager.createCredential( + // Use an activity based context to avoid undefined + // system UI launching behavior. + activityContext, + createPasswordRequest + ) + // Handle register password result + } catch (e: CreateCredentialException) { + handleFailure(e) + } + } } - } - // [END android_identity_register_password] + // [END android_identity_register_password] } sealed class ExampleCustomCredential { - class ExampleCustomCredentialParsingException : Throwable() {} + class ExampleCustomCredentialParsingException : Throwable() - companion object { - fun createFrom(data: Bundle): PublicKeyCredential { - return PublicKeyCredential("") - } + companion object { + fun createFrom(data: Bundle): PublicKeyCredential { + return PublicKeyCredential("") + } - const val TYPE: String = "" - } -} \ No newline at end of file + const val TYPE: String = "" + } +} diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyWebListener.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyWebListener.kt index 05119cd96..f13052b49 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyWebListener.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyWebListener.kt @@ -1,3 +1,19 @@ +/* + * 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.identity.credentialmanager import android.app.Activity @@ -25,213 +41,216 @@ const val TAG = "" // [START android_identity_create_listener_passkeys] // The class talking to Javascript should inherit: class PasskeyWebListener( - private val activity: Activity, - private val coroutineScope: CoroutineScope, - private val credentialManagerHandler: CredentialManagerHandler + private val activity: Activity, + private val coroutineScope: CoroutineScope, + private val credentialManagerHandler: CredentialManagerHandler ) : WebViewCompat.WebMessageListener { - /** havePendingRequest is true if there is an outstanding WebAuthn request. - There is only ever one request outstanding at a time. */ - private var havePendingRequest = false - - /** pendingRequestIsDoomed is true if the WebView has navigated since - starting a request. The FIDO module cannot be canceled, but the response - will never be delivered in this case. */ - private var pendingRequestIsDoomed = false - - /** replyChannel is the port that the page is listening for a response on. - It is valid if havePendingRequest is true. */ - private var replyChannel: ReplyChannel? = null - - /** - * Called by the page during a WebAuthn request. - * - * @param view Creates the WebView. - * @param message The message sent from the client using injected JavaScript. - * @param sourceOrigin The origin of the HTTPS request. Should not be null. - * @param isMainFrame Should be set to true. Embedded frames are not - supported. - * @param replyProxy Passed in by JavaScript. Allows replying when wrapped in - the Channel. - * @return The message response. - */ - @UiThread - override fun onPostMessage( - view: WebView, - message: WebMessageCompat, - sourceOrigin: Uri, - isMainFrame: Boolean, - replyProxy: JavaScriptReplyProxy, - ) { - val messageData = message.data ?: return - onRequest( - messageData, - sourceOrigin, - isMainFrame, - JavaScriptReplyChannel(replyProxy) - ) - } - - private fun onRequest( - msg: String, - sourceOrigin: Uri, - isMainFrame: Boolean, - reply: ReplyChannel, - ) { - msg?.let { - val jsonObj = JSONObject(msg); - val type = jsonObj.getString(TYPE_KEY) - val message = jsonObj.getString(REQUEST_KEY) - - if (havePendingRequest) { - postErrorMessage(reply, "The request already in progress", type) - return - } - - replyChannel = reply - if (!isMainFrame) { - reportFailure("Requests from subframes are not supported", type) - return - } - val originScheme = sourceOrigin.scheme - if (originScheme == null || originScheme.lowercase() != "https") { - reportFailure("WebAuthn not permitted for current URL", type) - return - } - - // Verify that origin belongs to your website, - // it's because the unknown origin may gain credential info. - // if (isUnknownOrigin(originScheme)) { - // return - // } - - havePendingRequest = true - pendingRequestIsDoomed = false - - // Use a temporary "replyCurrent" variable to send the data back, while - // resetting the main "replyChannel" variable to null so it’s ready for - // the next request. - val replyCurrent = replyChannel - if (replyCurrent == null) { - Log.i(TAG, "The reply channel was null, cannot continue") - return; - } - - when (type) { - CREATE_UNIQUE_KEY -> - this.coroutineScope.launch { - handleCreateFlow(credentialManagerHandler, message, replyCurrent) - } - - GET_UNIQUE_KEY -> this.coroutineScope.launch { - handleGetFlow(credentialManagerHandler, message, replyCurrent) - } + /** havePendingRequest is true if there is an outstanding WebAuthn request. + There is only ever one request outstanding at a time. */ + private var havePendingRequest = false + + /** pendingRequestIsDoomed is true if the WebView has navigated since + starting a request. The FIDO module cannot be canceled, but the response + will never be delivered in this case. */ + private var pendingRequestIsDoomed = false + + /** replyChannel is the port that the page is listening for a response on. + It is valid if havePendingRequest is true. */ + private var replyChannel: ReplyChannel? = null - else -> Log.i(TAG, "Incorrect request json") - } + /** + * Called by the page during a WebAuthn request. + * + * @param view Creates the WebView. + * @param message The message sent from the client using injected JavaScript. + * @param sourceOrigin The origin of the HTTPS request. Should not be null. + * @param isMainFrame Should be set to true. Embedded frames are not + supported. + * @param replyProxy Passed in by JavaScript. Allows replying when wrapped in + the Channel. + * @return The message response. + */ + @UiThread + override fun onPostMessage( + view: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + replyProxy: JavaScriptReplyProxy, + ) { + val messageData = message.data ?: return + onRequest( + messageData, + sourceOrigin, + isMainFrame, + JavaScriptReplyChannel(replyProxy) + ) } - } - - private suspend fun handleCreateFlow( - credentialManagerHandler: CredentialManagerHandler, - message: String, - reply: ReplyChannel, - ) { - try { - havePendingRequest = false - pendingRequestIsDoomed = false - val response = credentialManagerHandler.createPasskey(message) - val successArray = ArrayList(); - successArray.add("success"); - successArray.add(JSONObject(response.registrationResponseJson)); - successArray.add(CREATE_UNIQUE_KEY); - reply.send(JSONArray(successArray).toString()) - replyChannel = null // setting initial replyChannel for the next request - } catch (e: CreateCredentialException) { - reportFailure( - "Error: ${e.errorMessage} w type: ${e.type} w obj: $e", - CREATE_UNIQUE_KEY - ) - } catch (t: Throwable) { - reportFailure("Error: ${t.message}", CREATE_UNIQUE_KEY) + + private fun onRequest( + msg: String, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: ReplyChannel, + ) { + msg?.let { + val jsonObj = JSONObject(msg) + val type = jsonObj.getString(TYPE_KEY) + val message = jsonObj.getString(REQUEST_KEY) + + if (havePendingRequest) { + postErrorMessage(reply, "The request already in progress", type) + return + } + + replyChannel = reply + if (!isMainFrame) { + reportFailure("Requests from subframes are not supported", type) + return + } + val originScheme = sourceOrigin.scheme + if (originScheme == null || originScheme.lowercase() != "https") { + reportFailure("WebAuthn not permitted for current URL", type) + return + } + + // Verify that origin belongs to your website, + // it's because the unknown origin may gain credential info. + // if (isUnknownOrigin(originScheme)) { + // return + // } + + havePendingRequest = true + pendingRequestIsDoomed = false + + // Use a temporary "replyCurrent" variable to send the data back, while + // resetting the main "replyChannel" variable to null so it’s ready for + // the next request. + val replyCurrent = replyChannel + if (replyCurrent == null) { + Log.i(TAG, "The reply channel was null, cannot continue") + return + } + + when (type) { + CREATE_UNIQUE_KEY -> + this.coroutineScope.launch { + handleCreateFlow(credentialManagerHandler, message, replyCurrent) + } + + GET_UNIQUE_KEY -> this.coroutineScope.launch { + handleGetFlow(credentialManagerHandler, message, replyCurrent) + } + + else -> Log.i(TAG, "Incorrect request json") + } + } } - } - - companion object { - /** INTERFACE_NAME is the name of the MessagePort that must be injected into pages. */ - const val INTERFACE_NAME = "__webauthn_interface__" - const val TYPE_KEY = "type" - const val REQUEST_KEY = "request" - const val CREATE_UNIQUE_KEY = "create" - const val GET_UNIQUE_KEY = "get" - /** INJECTED_VAL is the minified version of the JavaScript code described at this class - * heading. The non minified form is found at credmanweb/javascript/encode.js.*/ - const val INJECTED_VAL = """ + + private suspend fun handleCreateFlow( + credentialManagerHandler: CredentialManagerHandler, + message: String, + reply: ReplyChannel, + ) { + try { + havePendingRequest = false + pendingRequestIsDoomed = false + val response = credentialManagerHandler.createPasskey(message) + val successArray = ArrayList() + successArray.add("success") + successArray.add(JSONObject(response.registrationResponseJson)) + successArray.add(CREATE_UNIQUE_KEY) + reply.send(JSONArray(successArray).toString()) + replyChannel = null // setting initial replyChannel for the next request + } catch (e: CreateCredentialException) { + reportFailure( + "Error: ${e.errorMessage} w type: ${e.type} w obj: $e", + CREATE_UNIQUE_KEY + ) + } catch (t: Throwable) { + reportFailure("Error: ${t.message}", CREATE_UNIQUE_KEY) + } + } + + companion object { + /** INTERFACE_NAME is the name of the MessagePort that must be injected into pages. */ + const val INTERFACE_NAME = "__webauthn_interface__" + const val TYPE_KEY = "type" + const val REQUEST_KEY = "request" + const val CREATE_UNIQUE_KEY = "create" + const val GET_UNIQUE_KEY = "get" + /** INJECTED_VAL is the minified version of the JavaScript code described at this class + * heading. The non minified form is found at credmanweb/javascript/encode.js.*/ + const val INJECTED_VAL = """ var __webauthn_interface__,__webauthn_hooks__;!function(e){console.log("In the hook."),__webauthn_interface__.addEventListener("message",function e(n){var r=JSON.parse(n.data),t=r[2];"get"===t?o(r):"create"===t?u(r):console.log("Incorrect response format for reply")});var n=null,r=null,t=null,a=null;function o(e){if(null!==n&&null!==t){if("success"!=e[0]){var r=t;n=null,t=null,r(new DOMException(e[1],"NotAllowedError"));return}var a=i(e[1]),o=n;n=null,t=null,o(a)}}function l(e){var n=e.length%4;return Uint8Array.from(atob(e.replace(/-/g,"+").replace(/_/g,"/").padEnd(e.length+(0===n?0:4-n),"=")),function(e){return e.charCodeAt(0)}).buffer}function s(e){return btoa(Array.from(new Uint8Array(e),function(e){return String.fromCharCode(e)}).join("")).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+${'$'}/,"")}function u(e){if(null===r||null===a){console.log("Here: "+r+" and reject: "+a);return}if(console.log("Output back: "+e),"success"!=e[0]){var n=a;r=null,a=null,n(new DOMException(e[1],"NotAllowedError"));return}var t=i(e[1]),o=r;r=null,a=null,o(t)}function i(e){return console.log("Here is the response from credential manager: "+e),e.rawId=l(e.rawId),e.response.clientDataJSON=l(e.response.clientDataJSON),e.response.hasOwnProperty("attestationObject")&&(e.response.attestationObject=l(e.response.attestationObject)),e.response.hasOwnProperty("authenticatorData")&&(e.response.authenticatorData=l(e.response.authenticatorData)),e.response.hasOwnProperty("signature")&&(e.response.signature=l(e.response.signature)),e.response.hasOwnProperty("userHandle")&&(e.response.userHandle=l(e.response.userHandle)),e.getClientExtensionResults=function e(){return{}},e}e.create=function n(t){if(!("publicKey"in t))return e.originalCreateFunction(t);var o=new Promise(function(e,n){r=e,a=n}),l=t.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}if(l.hasOwnProperty("user")&&l.user.hasOwnProperty("id")){var i=s(l.user.id);l.user.id=i}var c=JSON.stringify({type:"create",request:l});return __webauthn_interface__.postMessage(c),o},e.get=function r(a){if(!("publicKey"in a))return e.originalGetFunction(a);var o=new Promise(function(e,r){n=e,t=r}),l=a.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}var i=JSON.stringify({type:"get",request:l});return __webauthn_interface__.postMessage(i),o},e.onReplyGet=o,e.CM_base64url_decode=l,e.CM_base64url_encode=s,e.onReplyCreate=u}(__webauthn_hooks__||(__webauthn_hooks__={})),__webauthn_hooks__.originalGetFunction=navigator.credentials.get,__webauthn_hooks__.originalCreateFunction=navigator.credentials.create,navigator.credentials.get=__webauthn_hooks__.get,navigator.credentials.create=__webauthn_hooks__.create,window.PublicKeyCredential=function(){},window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable=function(){return Promise.resolve(!1)}; """ - } - // [END android_identity_create_listener_passkeys] - - // Handles the get flow in a less error-prone way - private suspend fun handleGetFlow( - credentialManagerHandler: CredentialManagerHandler, - message: String, - reply: ReplyChannel, - ) { - try { - havePendingRequest = false - pendingRequestIsDoomed = false - val r = credentialManagerHandler.getPasskey(message) - val successArray = ArrayList(); - successArray.add("success"); - successArray.add(JSONObject( - (r.credential as PublicKeyCredential).authenticationResponseJson)) - successArray.add(GET_UNIQUE_KEY); - reply.send(JSONArray(successArray).toString()) - replyChannel = null // setting initial replyChannel for next request given temp 'reply' - } catch (e: GetCredentialException) { - reportFailure("Error: ${e.errorMessage} w type: ${e.type} w obj: $e", GET_UNIQUE_KEY) - } catch (t: Throwable) { - reportFailure("Error: ${t.message}", GET_UNIQUE_KEY) } - } - - /** Sends an error result to the page. */ - private fun reportFailure(message: String, type: String) { - havePendingRequest = false - pendingRequestIsDoomed = false - val reply: ReplyChannel = replyChannel!! // verifies non null by throwing NPE - replyChannel = null - postErrorMessage(reply, message, type) - } - - private fun postErrorMessage(reply: ReplyChannel, errorMessage: String, type: String) { - Log.i(TAG, "Sending error message back to the page via replyChannel $errorMessage"); - val array: MutableList = ArrayList() - array.add("error") - array.add(errorMessage) - array.add(type) - reply.send(JSONArray(array).toString()) - var toastMsg = errorMessage - Toast.makeText(this.activity.applicationContext, toastMsg, Toast.LENGTH_SHORT).show() - } - - // [START android_identity_javascript_reply_channel] - // The setup for the reply channel allows communication with JavaScript. - private class JavaScriptReplyChannel(private val reply: JavaScriptReplyProxy) : - ReplyChannel { - override fun send(message: String?) { - try { - reply.postMessage(message!!) - } catch (t: Throwable) { - Log.i(TAG, "Reply failure due to: " + t.message); - } + // [END android_identity_create_listener_passkeys] + + // Handles the get flow in a less error-prone way + private suspend fun handleGetFlow( + credentialManagerHandler: CredentialManagerHandler, + message: String, + reply: ReplyChannel, + ) { + try { + havePendingRequest = false + pendingRequestIsDoomed = false + val r = credentialManagerHandler.getPasskey(message) + val successArray = ArrayList() + successArray.add("success") + successArray.add( + JSONObject( + (r.credential as PublicKeyCredential).authenticationResponseJson + ) + ) + successArray.add(GET_UNIQUE_KEY) + reply.send(JSONArray(successArray).toString()) + replyChannel = null // setting initial replyChannel for next request given temp 'reply' + } catch (e: GetCredentialException) { + reportFailure("Error: ${e.errorMessage} w type: ${e.type} w obj: $e", GET_UNIQUE_KEY) + } catch (t: Throwable) { + reportFailure("Error: ${t.message}", GET_UNIQUE_KEY) + } + } + + /** Sends an error result to the page. */ + private fun reportFailure(message: String, type: String) { + havePendingRequest = false + pendingRequestIsDoomed = false + val reply: ReplyChannel = replyChannel!! // verifies non null by throwing NPE + replyChannel = null + postErrorMessage(reply, message, type) + } + + private fun postErrorMessage(reply: ReplyChannel, errorMessage: String, type: String) { + Log.i(TAG, "Sending error message back to the page via replyChannel $errorMessage") + val array: MutableList = ArrayList() + array.add("error") + array.add(errorMessage) + array.add(type) + reply.send(JSONArray(array).toString()) + var toastMsg = errorMessage + Toast.makeText(this.activity.applicationContext, toastMsg, Toast.LENGTH_SHORT).show() + } + + // [START android_identity_javascript_reply_channel] + // The setup for the reply channel allows communication with JavaScript. + private class JavaScriptReplyChannel(private val reply: JavaScriptReplyProxy) : + ReplyChannel { + override fun send(message: String?) { + try { + reply.postMessage(message!!) + } catch (t: Throwable) { + Log.i(TAG, "Reply failure due to: " + t.message) + } + } + } + + // ReplyChannel is the interface where replies to the embedded site are + // sent. This allows for testing since AndroidX bans mocking its objects. + interface ReplyChannel { + fun send(message: String?) } - } - - // ReplyChannel is the interface where replies to the embedded site are - // sent. This allows for testing since AndroidX bans mocking its objects. - interface ReplyChannel { - fun send(message: String?) - } - // [END android_identity_javascript_reply_channel] -} \ No newline at end of file + // [END android_identity_javascript_reply_channel] +} diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SignInWithGoogleFunctions.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SignInWithGoogleFunctions.kt index f051e7355..28c58ef25 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SignInWithGoogleFunctions.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SignInWithGoogleFunctions.kt @@ -30,153 +30,151 @@ import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import kotlinx.coroutines.coroutineScope -import kotlin.math.sign const val WEB_CLIENT_ID = "" -class SignInWithGoogleFunctions ( - context: Context, +class SignInWithGoogleFunctions( + context: Context, ) { - private val credentialManager = CredentialManager.create(context) - private val activityContext = context - // Placeholder for TAG log value. - val TAG = "" - - fun createGoogleIdOption(nonce: String): GetGoogleIdOption { - // [START android_identity_siwg_instantiate_request] - val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder() - .setFilterByAuthorizedAccounts(true) - .setServerClientId(WEB_CLIENT_ID) - .setAutoSelectEnabled(true) - // nonce string to use when generating a Google ID token - .setNonce(nonce) - .build() - // [END android_identity_siwg_instantiate_request] - - return googleIdOption - } - - private val googleIdOption = createGoogleIdOption("") - - suspend fun signInUser() { - // [START android_identity_siwg_signin_flow_create_request] - val request: GetCredentialRequest = GetCredentialRequest.Builder() - .addCredentialOption(googleIdOption) - .build() - - coroutineScope { - try { - val result = credentialManager.getCredential( - request = request, - context = activityContext, - ) - handleSignIn(result) - } catch (e: GetCredentialException) { - // Handle failure - } + private val credentialManager = CredentialManager.create(context) + private val activityContext = context + // Placeholder for TAG log value. + val TAG = "" + + fun createGoogleIdOption(nonce: String): GetGoogleIdOption { + // [START android_identity_siwg_instantiate_request] + val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(true) + .setServerClientId(WEB_CLIENT_ID) + .setAutoSelectEnabled(true) + // nonce string to use when generating a Google ID token + .setNonce(nonce) + .build() + // [END android_identity_siwg_instantiate_request] + + return googleIdOption } - // [END android_identity_siwg_signin_flow_create_request] - } - - // [START android_identity_siwg_signin_flow_handle_signin] - fun handleSignIn(result: GetCredentialResponse) { - // Handle the successfully returned credential. - val credential = result.credential - val responseJson: String - - when (credential) { - - // Passkey credential - is PublicKeyCredential -> { - // Share responseJson such as a GetCredentialResponse to your server to validate and - // authenticate - responseJson = credential.authenticationResponseJson - } - - // Password credential - is PasswordCredential -> { - // Send ID and password to your server to validate and authenticate. - val username = credential.id - val password = credential.password - } - - // GoogleIdToken credential - is CustomCredential -> { - if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { - try { - // Use googleIdTokenCredential and extract the ID to validate and - // authenticate on your server. - val googleIdTokenCredential = GoogleIdTokenCredential - .createFrom(credential.data) - // You can use the members of googleIdTokenCredential directly for UX - // purposes, but don't use them to store or control access to user - // data. For that you first need to validate the token: - // pass googleIdTokenCredential.getIdToken() to the backend server. - // see [validation instructions](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token) - } catch (e: GoogleIdTokenParsingException) { - Log.e(TAG, "Received an invalid google id token response", e) - } - } else { - // Catch any unrecognized custom credential type here. - Log.e(TAG, "Unexpected type of credential") - } - } - else -> { - // Catch any unrecognized credential type here. - Log.e(TAG, "Unexpected type of credential") - } - } - } - // [END android_identity_siwg_signin_flow_handle_signin] - - fun createGoogleSignInWithGoogleOption(nonce: String): GetSignInWithGoogleOption { - // [START android_identity_siwg_get_siwg_option] - val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder( - serverClientId = WEB_CLIENT_ID - ).setNonce(nonce) - .build() - // [END android_identity_siwg_get_siwg_option] - - return signInWithGoogleOption - } - - // [START android_identity_handle_siwg_option] - fun handleSignInWithGoogleOption(result: GetCredentialResponse) { - // Handle the successfully returned credential. - val credential = result.credential - - when (credential) { - is CustomCredential -> { - if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { - try { - // Use googleIdTokenCredential and extract id to validate and - // authenticate on your server. - val googleIdTokenCredential = GoogleIdTokenCredential - .createFrom(credential.data) - } catch (e: GoogleIdTokenParsingException) { - Log.e(TAG, "Received an invalid google id token response", e) - } + private val googleIdOption = createGoogleIdOption("") + + suspend fun signInUser() { + // [START android_identity_siwg_signin_flow_create_request] + val request: GetCredentialRequest = GetCredentialRequest.Builder() + .addCredentialOption(googleIdOption) + .build() + + coroutineScope { + try { + val result = credentialManager.getCredential( + request = request, + context = activityContext, + ) + handleSignIn(result) + } catch (e: GetCredentialException) { + // Handle failure + } } - else { - // Catch any unrecognized credential type here. - Log.e(TAG, "Unexpected type of credential") + // [END android_identity_siwg_signin_flow_create_request] + } + + // [START android_identity_siwg_signin_flow_handle_signin] + fun handleSignIn(result: GetCredentialResponse) { + // Handle the successfully returned credential. + val credential = result.credential + val responseJson: String + + when (credential) { + + // Passkey credential + is PublicKeyCredential -> { + // Share responseJson such as a GetCredentialResponse to your server to validate and + // authenticate + responseJson = credential.authenticationResponseJson + } + + // Password credential + is PasswordCredential -> { + // Send ID and password to your server to validate and authenticate. + val username = credential.id + val password = credential.password + } + + // GoogleIdToken credential + is CustomCredential -> { + if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + try { + // Use googleIdTokenCredential and extract the ID to validate and + // authenticate on your server. + val googleIdTokenCredential = GoogleIdTokenCredential + .createFrom(credential.data) + // You can use the members of googleIdTokenCredential directly for UX + // purposes, but don't use them to store or control access to user + // data. For that you first need to validate the token: + // pass googleIdTokenCredential.getIdToken() to the backend server. + // see [validation instructions](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token) + } catch (e: GoogleIdTokenParsingException) { + Log.e(TAG, "Received an invalid google id token response", e) + } + } else { + // Catch any unrecognized custom credential type here. + Log.e(TAG, "Unexpected type of credential") + } + } + + else -> { + // Catch any unrecognized credential type here. + Log.e(TAG, "Unexpected type of credential") + } } - } + } + // [END android_identity_siwg_signin_flow_handle_signin] - else -> { - // Catch any unrecognized credential type here. - Log.e(TAG, "Unexpected type of credential") - } + fun createGoogleSignInWithGoogleOption(nonce: String): GetSignInWithGoogleOption { + // [START android_identity_siwg_get_siwg_option] + val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder( + serverClientId = WEB_CLIENT_ID + ).setNonce(nonce) + .build() + // [END android_identity_siwg_get_siwg_option] + + return signInWithGoogleOption + } + + // [START android_identity_handle_siwg_option] + fun handleSignInWithGoogleOption(result: GetCredentialResponse) { + // Handle the successfully returned credential. + val credential = result.credential + + when (credential) { + is CustomCredential -> { + if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + try { + // Use googleIdTokenCredential and extract id to validate and + // authenticate on your server. + val googleIdTokenCredential = GoogleIdTokenCredential + .createFrom(credential.data) + } catch (e: GoogleIdTokenParsingException) { + Log.e(TAG, "Received an invalid google id token response", e) + } + } else { + // Catch any unrecognized credential type here. + Log.e(TAG, "Unexpected type of credential") + } + } + + else -> { + // Catch any unrecognized credential type here. + Log.e(TAG, "Unexpected type of credential") + } + } + } + // [END android_identity_handle_siwg_option] + + fun googleIdOptionFalseFilter() { + // [START android_identity_siwg_instantiate_request_2] + val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(false) + .setServerClientId(WEB_CLIENT_ID) + .build() + // [END android_identity_siwg_instantiate_request_2] } - } - // [END android_identity_handle_siwg_option] - - fun googleIdOptionFalseFilter() { - // [START android_identity_siwg_instantiate_request_2] - val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder() - .setFilterByAuthorizedAccounts(false) - .setServerClientId(WEB_CLIENT_ID) - .build() - // [END android_identity_siwg_instantiate_request_2] - } -} \ No newline at end of file +} diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SingleTap.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SingleTap.kt index 52445b6df..b61ddbe5c 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SingleTap.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SingleTap.kt @@ -1,3 +1,19 @@ +/* + * 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.identity.credentialmanager import android.os.Build.VERSION_CODES @@ -11,7 +27,7 @@ import androidx.credentials.provider.BiometricPromptData import androidx.credentials.provider.CallingAppInfo import androidx.credentials.provider.PendingIntentHandler -class SingleTap: ComponentActivity() { +class SingleTap : ComponentActivity() { private val x: Any? = null private val TAG: String = "" @@ -35,7 +51,7 @@ class SingleTap: ComponentActivity() { allowedAuthenticators = allowedAuthenticator ) ) - // [END android_identity_single_tap_set_biometric_prompt_data] + // [END android_identity_single_tap_set_biometric_prompt_data] when (x) { // [START android_identity_single_tap_pk_creation] @@ -105,8 +121,7 @@ class SingleTap: ComponentActivity() { if (biometricPromptResult == null) { // Do your own authentication flow, if needed - } - else if (biometricPromptResult.isSuccessful) { + } else if (biometricPromptResult.isSuccessful) { createPasskey( publicKeyRequest.requestJson, createRequest.callingAppInfo, @@ -150,8 +165,7 @@ class SingleTap: ComponentActivity() { // Add your logic based on what needs to be done // after getting biometrics - if (biometricPromptResult == null) - { + if (biometricPromptResult == null) { // Do your own authentication flow, if necessary } else if (biometricPromptResult.isSuccessful) { diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SmartLockToCredMan.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SmartLockToCredMan.kt index 0dc66ceba..f37f6d9da 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SmartLockToCredMan.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SmartLockToCredMan.kt @@ -1,11 +1,23 @@ +/* + * 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.identity.credentialmanager -import android.annotation.SuppressLint import android.content.Context import android.util.Log -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.rememberCoroutineScope import androidx.credentials.CredentialManager import androidx.credentials.GetCredentialRequest import androidx.credentials.GetCredentialResponse diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt index 725ad7ff8..935da4971 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt @@ -1,3 +1,19 @@ +/* + * 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.identity.credentialmanager import android.graphics.Bitmap @@ -13,70 +29,73 @@ import androidx.webkit.WebViewFeature import kotlinx.coroutines.CoroutineScope class WebViewMainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) - // [START android_identity_initialize_the_webview] - val credentialManagerHandler = CredentialManagerHandler(this) + // [START android_identity_initialize_the_webview] + val credentialManagerHandler = CredentialManagerHandler(this) - setContent { - val coroutineScope = rememberCoroutineScope() - AndroidView(factory = { - WebView(it).apply { - settings.javaScriptEnabled = true + setContent { + val coroutineScope = rememberCoroutineScope() + AndroidView( + factory = { + WebView(it).apply { + settings.javaScriptEnabled = true - // Test URL: - val url = "https://passkeys-codelab.glitch.me/" - val listenerSupported = WebViewFeature.isFeatureSupported( - WebViewFeature.WEB_MESSAGE_LISTENER - ) - if (listenerSupported) { - // Inject local JavaScript that calls Credential Manager. - hookWebAuthnWithListener( - this, this@WebViewMainActivity, - coroutineScope, credentialManagerHandler + // Test URL: + val url = "https://passkeys-codelab.glitch.me/" + val listenerSupported = WebViewFeature.isFeatureSupported( + WebViewFeature.WEB_MESSAGE_LISTENER + ) + if (listenerSupported) { + // Inject local JavaScript that calls Credential Manager. + hookWebAuthnWithListener( + this, this@WebViewMainActivity, + coroutineScope, credentialManagerHandler + ) + } else { + // Fallback routine for unsupported API levels. + } + loadUrl(url) + } + } ) - } else { - // Fallback routine for unsupported API levels. - } - loadUrl(url) } - } - ) + // [END android_identity_initialize_the_webview] } - // [END android_identity_initialize_the_webview] - } - /** - * Connects the local app logic with the web page via injection of javascript through a - * WebListener. Handles ensuring the [PasskeyWebListener] is hooked up to the webView page - * if compatible. - */ - fun hookWebAuthnWithListener( - webView: WebView, - activity: WebViewMainActivity, - coroutineScope: CoroutineScope, - credentialManagerHandler: CredentialManagerHandler - ) { - // [START android_identity_create_webview_object] - val passkeyWebListener = PasskeyWebListener(activity, coroutineScope, credentialManagerHandler) + /** + * Connects the local app logic with the web page via injection of javascript through a + * WebListener. Handles ensuring the [PasskeyWebListener] is hooked up to the webView page + * if compatible. + */ + fun hookWebAuthnWithListener( + webView: WebView, + activity: WebViewMainActivity, + coroutineScope: CoroutineScope, + credentialManagerHandler: CredentialManagerHandler + ) { + // [START android_identity_create_webview_object] + val passkeyWebListener = PasskeyWebListener(activity, coroutineScope, credentialManagerHandler) - val webViewClient = object : WebViewClient() { - override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { - super.onPageStarted(view, url, favicon) - webView.evaluateJavascript(PasskeyWebListener.INJECTED_VAL, null) - } - } + val webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + webView.evaluateJavascript(PasskeyWebListener.INJECTED_VAL, null) + } + } - webView.webViewClient = webViewClient - // [END android_identity_create_webview_object] + webView.webViewClient = webViewClient + // [END android_identity_create_webview_object] - // [START android_identity_set_web] - val rules = setOf("*") - if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { - WebViewCompat.addWebMessageListener(webView, PasskeyWebListener.INTERFACE_NAME, - rules, passkeyWebListener) + // [START android_identity_set_web] + val rules = setOf("*") + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + WebViewCompat.addWebMessageListener( + webView, PasskeyWebListener.INTERFACE_NAME, + rules, passkeyWebListener + ) + } + // [END android_identity_set_web] } - // [END android_identity_set_web] - } -} \ No newline at end of file +} diff --git a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/HomeViewModel.kt b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/HomeViewModel.kt index 9dea9a720..0b804c985 100644 --- a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/HomeViewModel.kt +++ b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/HomeViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022 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. diff --git a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/Repository.kt b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/Repository.kt index babf205c6..0d6252d77 100644 --- a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/Repository.kt +++ b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/Repository.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022 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. @@ -16,6 +16,7 @@ package com.example.android.coroutines.testing +import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -23,7 +24,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.util.concurrent.atomic.AtomicBoolean // [START coroutine_test_repo_dispatcher_injection] // Example class demonstrating dispatcher use cases diff --git a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserRepository.kt b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserRepository.kt index b53dee883..26b888543 100644 --- a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserRepository.kt +++ b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022 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. diff --git a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserState.kt b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserState.kt index e06b7240c..7ac89ce50 100644 --- a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserState.kt +++ b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserState.kt @@ -1,3 +1,19 @@ +/* + * 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.android.coroutines.testing.scope import kotlinx.coroutines.CoroutineScope diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/CreatingYourOwn.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/CreatingYourOwn.kt index 727a7c8a7..05a6794c0 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/CreatingYourOwn.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/CreatingYourOwn.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022 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. diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatcherTypesTest.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatcherTypesTest.kt index bbe6b117e..81f206ad1 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatcherTypesTest.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatcherTypesTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022 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. diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatchersOutsideTests.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatchersOutsideTests.kt index 2f3228ccf..c586f2570 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatchersOutsideTests.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatchersOutsideTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022 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. @@ -32,7 +32,6 @@ import org.junit.runner.RunWith // Helper function to let code below compile private fun ExampleRepository(): Repository = Repository(Dispatchers.IO) - // [START coroutine_test_repo_with_rule_blank] class ExampleRepository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ } @@ -89,5 +88,3 @@ class DispatchersOutsideTests { } // [END coroutine_test_repo_without_rule] } - - diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTest.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTest.kt index d4efc2e58..b588e90ca 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTest.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022 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. diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTestUsingRule.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTestUsingRule.kt index 54a5a9bdd..b7b926edb 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTestUsingRule.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTestUsingRule.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022 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. diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/RepositoryTest.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/RepositoryTest.kt index ec0a47c49..5cd4b2bab 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/RepositoryTest.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/RepositoryTest.kt @@ -1,3 +1,19 @@ +/* + * 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.android.coroutines.testing import kotlinx.coroutines.test.StandardTestDispatcher diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/StandardTestDispatcherTest.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/StandardTestDispatcherTest.kt index bf749c095..da07e4936 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/StandardTestDispatcherTest.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/StandardTestDispatcherTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022 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. @@ -51,4 +51,4 @@ class StandardTestDispatcherTest_Fixed { assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes } // [END coroutine_test_standard_fixed] -} \ No newline at end of file +} diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/SuspendingFunctionTests.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/SuspendingFunctionTests.kt index 90e0ab79f..2dc83b6a3 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/SuspendingFunctionTests.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/SuspendingFunctionTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022 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. diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UnconfinedTestDispatcherTest.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UnconfinedTestDispatcherTest.kt index ae488974a..b2230c034 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UnconfinedTestDispatcherTest.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UnconfinedTestDispatcherTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022 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. diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UserStateTest.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UserStateTest.kt index e2c625e4e..3b2ec514b 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UserStateTest.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UserStateTest.kt @@ -1,3 +1,19 @@ +/* + * 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. + */ + @file:OptIn(ExperimentalCoroutinesApi::class) package com.example.android.coroutines.testing diff --git a/views/src/main/java/insets/SystemBarProtectionSnippet.kt b/views/src/main/java/insets/SystemBarProtectionSnippet.kt index bd44cd4c9..c21011b44 100644 --- a/views/src/main/java/insets/SystemBarProtectionSnippet.kt +++ b/views/src/main/java/insets/SystemBarProtectionSnippet.kt @@ -40,7 +40,7 @@ class SystemBarProtectionSnippet : AppCompatActivity() { ) { v: View, insets: WindowInsetsCompat -> val innerPadding = insets.getInsets( WindowInsetsCompat.Type.systemBars() or - WindowInsetsCompat.Type.displayCutout() + WindowInsetsCompat.Type.displayCutout() ) v.setPadding( innerPadding.left, diff --git a/watchfacepush/validator/build.gradle.kts b/watchfacepush/validator/build.gradle.kts index 66cb693bf..0b9289a5c 100644 --- a/watchfacepush/validator/build.gradle.kts +++ b/watchfacepush/validator/build.gradle.kts @@ -26,6 +26,14 @@ application { mainClass.set("com.example.validator.Main") } +sourceSets { + named("main") { + java { + srcDir("src/main/java") + } + } +} + dependencies { implementation(libs.validator.push) } \ No newline at end of file diff --git a/watchfacepush/validator/src/main/kotlin/com/example/validator/Main.kt b/watchfacepush/validator/src/main/kotlin/com/example/validator/Main.kt index f046ebe09..c1aa20ef0 100644 --- a/watchfacepush/validator/src/main/kotlin/com/example/validator/Main.kt +++ b/watchfacepush/validator/src/main/kotlin/com/example/validator/Main.kt @@ -1,3 +1,19 @@ +/* + * 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.validator import com.google.android.wearable.watchface.validator.client.DwfValidatorFactory @@ -56,4 +72,4 @@ private fun obtainTempWatchFaceFile(): File { inputStream.copyTo(fos) } return tempFile -} \ No newline at end of file +}