1package com.peachpasswords.passkeys23import android.os.Build4import android.os.CancellationSignal5import android.graphics.drawable.Icon6import androidx.credentials.provider.BeginCreateCredentialRequest7import androidx.credentials.provider.BeginCreateCredentialResponse8import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest9import androidx.credentials.provider.BeginGetCredentialRequest10import androidx.credentials.provider.BeginGetCredentialResponse11import androidx.credentials.provider.BeginGetPublicKeyCredentialOption12import androidx.credentials.provider.CreateEntry13import androidx.credentials.provider.AuthenticationAction14import androidx.credentials.provider.CredentialProviderService15import androidx.credentials.provider.ProviderClearCredentialStateRequest16import androidx.credentials.provider.PublicKeyCredentialEntry17import android.os.OutcomeReceiver18import androidx.credentials.exceptions.ClearCredentialException19import androidx.credentials.exceptions.CreateCredentialException20import androidx.credentials.exceptions.GetCredentialException21import androidx.annotation.RequiresApi22import com.peachpasswords.R23import com.peachpasswords.license.LicenseRepository24import com.peachpasswords.license.ProFeature25import com.peachpasswords.license.ProNoticeStore26import com.peachpasswords.vault.VaultRepository27import dagger.hilt.android.AndroidEntryPoint28import java.time.Instant29import javax.inject.Inject3031323334353637383940414243@AndroidEntryPoint44@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)45class PeachCredentialProviderService : CredentialProviderService() {4647 @Inject lateinit var vault: VaultRepository48 @Inject lateinit var license: LicenseRepository49 @Inject lateinit var proNoticeStore: ProNoticeStore5051 override fun onBeginGetCredentialRequest(52 request: BeginGetCredentialRequest,53 cancellationSignal: CancellationSignal,54 callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,55 ) {56 if (!vault.settings.value.passkeyProviderEnabled) {57 callback.onResult(BeginGetCredentialResponse())58 return59 }60 if (!license.hasFeature(ProFeature.Passkeys)) {61 proNoticeStore.record(ProFeature.Passkeys)62 callback.onResult(BeginGetCredentialResponse())63 return64 }65 if (!vault.isUnlocked.value) {66 callback.onResult(67 BeginGetCredentialResponse(68 authenticationActions = listOf(69 AuthenticationAction(70 title = "Unlock Peach",71 pendingIntent = PasskeyUnlockIntent.pendingIntent(this),72 ),73 ),74 ),75 )76 return77 }78 val passkeyOptions = request.beginGetCredentialOptions79 .filterIsInstance<BeginGetPublicKeyCredentialOption>()80 if (passkeyOptions.isEmpty()) {81 callback.onResult(BeginGetCredentialResponse())82 return83 }8485 val icon = Icon.createWithResource(this, R.drawable.ic_peach_mark)86 val entries = passkeyOptions.flatMap { option ->87 PasskeyCredentialPlanner.candidates(vault.getEntries(), option.requestJson).map { candidate ->88 PublicKeyCredentialEntry(89 context = this,90 username = candidate.username,91 pendingIntent = PasskeyCredentialSelectionActivity.pendingIntent(92 this,93 candidate.entryId,94 option.requestJson,95 ),96 beginGetPublicKeyCredentialOption = option,97 displayName = candidate.displayName,98 lastUsedTime = Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L),99 icon = icon,100 isAutoSelectAllowed = false,101 )102 }103 }104 callback.onResult(BeginGetCredentialResponse(credentialEntries = entries))105 }106107 override fun onBeginCreateCredentialRequest(108 request: BeginCreateCredentialRequest,109 cancellationSignal: CancellationSignal,110 callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,111 ) {112 val publicKeyRequest = request as? BeginCreatePublicKeyCredentialRequest113 if (publicKeyRequest == null || !vault.settings.value.passkeyProviderEnabled || !vault.isUnlocked.value) {114 callback.onResult(BeginCreateCredentialResponse())115 return116 }117 if (!license.hasFeature(ProFeature.Passkeys)) {118 proNoticeStore.record(ProFeature.Passkeys)119 callback.onResult(BeginCreateCredentialResponse())120 return121 }122 val count = vault.getEntries().count { it.passkey != null }123 val entry = CreateEntry(124 accountName = "Peach",125 pendingIntent = PasskeyCredentialSelectionActivity.createPendingIntent(this, publicKeyRequest.requestJson),126 description = "Save passkey to Peach",127 lastUsedTime = Instant.now(),128 icon = Icon.createWithResource(this, R.drawable.ic_peach_mark),129 passwordCredentialCount = null,130 publicKeyCredentialCount = count,131 totalCredentialCount = vault.getEntries().size,132 isAutoSelectAllowed = false,133 )134 callback.onResult(BeginCreateCredentialResponse(createEntries = listOf(entry)))135 }136137 override fun onClearCredentialStateRequest(138 request: ProviderClearCredentialStateRequest,139 cancellationSignal: CancellationSignal,140 callback: OutcomeReceiver<Void?, ClearCredentialException>,141 ) {142 callback.onResult(null)143 }144}