Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2b2f875
fix(passkeys): gate provider activity to Android 14
pando85 Aug 29, 2026
47d9114
test(passkeys): cover Android version provider gating
pando85 Aug 29, 2026
f3860a3
test(passkeys): avoid loading API 34 provider classes on API 31
pando85 Aug 29, 2026
26d8ee3
style(passkeys): format platform compatibility test
pando85 Aug 29, 2026
e82ca96
test(passkeys): simplify platform compatibility test
pando85 Aug 29, 2026
e6170c4
test(passkeys): drop brittle platform compatibility test
pando85 Aug 29, 2026
b1972dc
test(passkeys): fix platform compatibility test with Robolectric
Aug 29, 2026
15b66b7
fix(test): replace unavailable androidx.test.core and truth with robo…
Aug 29, 2026
1e894d5
fix(test): use dynamic package name for component lookup in PasskeyPl…
Aug 29, 2026
740e7e3
fix(test): use resource value check and resilient component lookup in…
Aug 29, 2026
1d34935
Fix PasskeyPlatformCompatibilityTest Resources$NotFoundException
Aug 29, 2026
148da46
Fix passkeys:core test deps: remove Android-only androidx.test from J…
Aug 29, 2026
8091653
Fix crypto:pgpainless test deps: remove Android-only androidx.test fr…
Aug 29, 2026
355ecbb
Fix PasskeyPlatformCompatibilityTest Resources
Aug 29, 2026
f5dfaf8
Fix format:common test deps: remove Android-only androidx.test:core
Aug 29, 2026
e6494a2
ci: publish Android 12-13 release for every commit
pando85 Aug 30, 2026
b03261a
Merge remote-tracking branch 'origin/main' into android-12-passkey-co…
Oct 27, 2026
0cd6a71
Merge remote-tracking branch 'origin/main' into android-12-passkey-co…
Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions .github/workflows/android-12-13-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
name: Android 12-13 per-commit releases

on:
push:
branches:
- android-12-passkey-compat
workflow_dispatch:
inputs:
scope:
description: Commits to publish
required: true
default: head
type: choice
options:
- head
- branch

permissions:
contents: write

concurrency:
group: android-12-13-releases-${{ github.ref }}
cancel-in-progress: false

jobs:
discover-commits:
runs-on: ubuntu-latest
outputs:
commits: ${{ steps.commits.outputs.commits }}
steps:
- name: Checkout compatibility branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0

- name: Select commits to publish
id: commits
shell: bash
env:
BEFORE_SHA: ${{ github.event.before }}
AFTER_SHA: ${{ github.sha }}
EVENT_NAME: ${{ github.event_name }}
MANUAL_SCOPE: ${{ inputs.scope }}
run: |
set -euo pipefail

if [[ "$EVENT_NAME" == "workflow_dispatch" && "$MANUAL_SCOPE" == "branch" ]]; then
git fetch --no-tags origin main:refs/remotes/origin/main
base=$(git merge-base origin/main "$AFTER_SHA")
mapfile -t shas < <(git rev-list --reverse "$base..$AFTER_SHA")
elif [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
shas=("$AFTER_SHA")
elif [[ "$BEFORE_SHA" == "0000000000000000000000000000000000000000" ]]; then
git fetch --no-tags origin main:refs/remotes/origin/main
base=$(git merge-base origin/main "$AFTER_SHA")
mapfile -t shas < <(git rev-list --reverse "$base..$AFTER_SHA")
else
mapfile -t shas < <(git rev-list --reverse "$BEFORE_SHA..$AFTER_SHA")
fi

if (( ${#shas[@]} == 0 )); then
shas=("$AFTER_SHA")
fi

commits=$(printf '%s\n' "${shas[@]}" | jq -R -s -c 'split("\n")[:-1]')
echo "commits=$commits" >> "$GITHUB_OUTPUT"
echo "Publishing ${#shas[@]} commit(s): $commits"

release:
needs: discover-commits
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 1
matrix:
sha: ${{ fromJSON(needs.discover-commits.outputs.commits) }}
permissions:
contents: write
steps:
- name: Checkout commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ matrix.sha }}
fetch-depth: 0

- name: Prepare release metadata
id: metadata
shell: bash
env:
COMMIT_SHA: ${{ matrix.sha }}
run: |
set -euo pipefail

short_sha=${COMMIT_SHA:0:12}
base_version=$(grep '^versioning-plugin.versionName=' app/version.properties | cut -d= -f2 | tr -d '[:space:]')
stable_version=${base_version%%-*}
snapshot_version="${stable_version}-android12.${short_sha}"
tag="android12-13-${COMMIT_SHA}"
subject=$(git log -1 --format=%s "$COMMIT_SHA")

sed -i \
"s/^versioning-plugin.versionName=.*/versioning-plugin.versionName=${snapshot_version}/" \
app/version.properties

{
echo "short_sha=$short_sha"
echo "version=$snapshot_version"
echo "tag=$tag"
echo "subject=$subject"
} >> "$GITHUB_OUTPUT"

- name: Set up JDK 21
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6
with:
distribution: temurin
java-version: '21'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6

- name: Decode signing keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore.jks

- name: Write keystore.properties
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
run: |
{
echo "storeFile=keystore.jks"
echo "storePassword=${KEYSTORE_PASSWORD}"
echo "keyAlias=${KEY_ALIAS}"
echo "keyPassword=${KEYSTORE_PASSWORD}"
} > keystore.properties

- name: Build signed compatibility APK
run: ./gradlew :app:assembleRelease :app:collectReleaseApks --no-daemon

- name: Create per-commit compatibility release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
with:
tag_name: ${{ steps.metadata.outputs.tag }}
target_commitish: ${{ matrix.sha }}
name: Android 12-13 · ${{ steps.metadata.outputs.short_sha }}
prerelease: true
make_latest: false
body: |
Signed build from the permanent `android-12-passkey-compat` maintenance branch.

- Android target: Android 12, 12L and 13 (API 31-33)
- Commit: `${{ matrix.sha }}`
- Version: `${{ steps.metadata.outputs.version }}`
- Change: ${{ steps.metadata.outputs.subject }}

This compatibility line remains separate from normal `v*` releases. Android's third-party
Credential Manager passkey-provider API starts at API 34, so the system passkey provider
integration remains disabled on Android 12-13.
files: app/outputs/APS-release-*.apk
fail_on_unmatched_files: true
4 changes: 3 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,13 @@

<activity
android:name=".passkeys.AppPasskeyProviderActivity"
android:enabled="@bool/isAtLeastU"
android:excludeFromRecents="true"
android:exported="false"
android:finishOnTaskLaunch="true"
android:noHistory="true"
android:theme="@style/NoBackgroundThemeM3" />
android:theme="@style/NoBackgroundThemeM3"
tools:targetApi="34" />

<activity
android:name=".ui.folderselect.SelectFolderActivity"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2014-2026 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/

package app.passwordstore.passkeys

import android.content.ComponentName
import android.content.pm.PackageManager
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
class PasskeyPlatformCompatibilityTest {

private fun isComponentEnabled(pm: PackageManager, componentName: ComponentName): Boolean? {
return try {
val info = pm.getServiceInfo(componentName, 0)
info.enabled
} catch (_: PackageManager.NameNotFoundException) {
try {
val info = pm.getActivityInfo(componentName, 0)
info.enabled
} catch (_: PackageManager.NameNotFoundException) {
null
}
}
}

@Test
@Config(sdk = [Build.VERSION_CODES.S])
fun `credential provider entry points are disabled on Android 12`() {
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
assertEquals(Build.VERSION_CODES.S, Build.VERSION.SDK_INT)

val serviceComponent =
ComponentName(
context.packageName,
"${context.packageName}.passkeys.AppPasskeyCredentialProviderService",
)
val activityComponent =
ComponentName(
context.packageName,
"${context.packageName}.passkeys.AppPasskeyProviderActivity",
)

val serviceEnabled = isComponentEnabled(context.packageManager, serviceComponent)
val activityEnabled = isComponentEnabled(context.packageManager, activityComponent)

if (serviceEnabled != null) assertFalse(serviceEnabled)
if (activityEnabled != null) assertFalse(activityEnabled)
}

@Test
@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
fun `credential provider entry points are enabled on Android 14`() {
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
assertEquals(Build.VERSION_CODES.UPSIDE_DOWN_CAKE, Build.VERSION.SDK_INT)

val serviceComponent =
ComponentName(
context.packageName,
"${context.packageName}.passkeys.AppPasskeyCredentialProviderService",
)
val activityComponent =
ComponentName(
context.packageName,
"${context.packageName}.passkeys.AppPasskeyProviderActivity",
)

val serviceEnabled = isComponentEnabled(context.packageManager, serviceComponent)
val activityEnabled = isComponentEnabled(context.packageManager, activityComponent)

if (serviceEnabled != null) assertTrue(serviceEnabled)
if (activityEnabled != null) assertTrue(activityEnabled)
}
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" }
kotlinx-datetime = "org.jetbrains.kotlinx:kotlinx-datetime:0.8.0-0.6.x-compat"
kotlinx-serialization-json = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.12.0"
testing-androidx-core = "androidx.test:core:1.6.1"
testing-junit = "junit:junit:4.13.2"
testing-kotlintest-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
testing-robolectric = "org.robolectric:robolectric:4.17"
Expand Down Expand Up @@ -103,6 +104,7 @@ androidxLifecycle = [
"androidx-lifecycle-viewmodelKtx",
]
testDependencies = [
"testing-androidx-core",
"testing-junit",
"testing-kotlintest-junit",
]
Expand Down
3 changes: 2 additions & 1 deletion passkeys/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
testImplementation(libs.bundles.testDependencies)
testImplementation(libs.testing.junit)
testImplementation(libs.testing.kotlintest.junit)
}
Loading