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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/android-release-signing-certificate.sha256
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
c2a2cde113ba53cafa2d111dc79698a4d2295256b1ad6e83e9acf3e8eb179384
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,46 @@ jobs:
path: app/build/outputs/apk/debug/app-debug.apk
if-no-files-found: error
retention-days: 14

instrumented:
name: Instrumented tests (API 34)
needs: verify
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Check out source
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Set up JDK 17
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: temurin
java-version: "17"

- name: Validate the Gradle wrapper
uses: gradle/actions/wrapper-validation@0f4528296b4bc09e8ae0fc7be30185a4ab435545 # v6.0.0

- name: Set up Gradle
uses: gradle/actions/setup-gradle@0f4528296b4bc09e8ae0fc7be30185a4ab435545 # v6.0.0

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Run connected tests on the minimum supported Android version
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
with:
api-level: "37.0"
system-image-api-level: 34
target: google_apis
arch: x86_64
profile: pixel_7_pro
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew --no-daemon connectedDebugAndroidTest
67 changes: 61 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,60 @@ on:
tags:
- "v*"

permissions:
contents: write

concurrency:
group: android-release-${{ github.ref }}
cancel-in-progress: false

jobs:
instrumented:
name: Instrumented release gate (API 34)
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Check out source
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Set up JDK 17
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: temurin
java-version: "17"

- name: Validate the Gradle wrapper
uses: gradle/actions/wrapper-validation@0f4528296b4bc09e8ae0fc7be30185a4ab435545 # v6.0.0

- name: Set up Gradle
uses: gradle/actions/setup-gradle@0f4528296b4bc09e8ae0fc7be30185a4ab435545 # v6.0.0

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Run connected tests on the minimum supported Android version
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
with:
api-level: "37.0"
system-image-api-level: 34
target: google_apis
arch: x86_64
profile: pixel_7_pro
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew --no-daemon connectedDebugAndroidTest

release:
name: Verify, sign, and publish APK
needs: instrumented
permissions:
contents: write
runs-on: ubuntu-latest
timeout-minutes: 45
env:
Expand Down Expand Up @@ -125,7 +169,7 @@ jobs:
- name: Test, lint, and build signed release APK
run: >-
./gradlew --no-daemon
test testDebugUnitTest lintDebug assembleRelease
test testDebugUnitTest lintDebug lintRelease assembleRelease
-PreleaseVersionName=${{ steps.release_metadata.outputs.version_name }}
-PreleaseVersionCode=${{ github.run_number }}

Expand All @@ -136,15 +180,24 @@ jobs:
python3 tools/validate_protocol_vectors.py
python3 tools/retired_identity_audit.py
python3 tools/collector_assurance.py
python3 -m unittest discover -s tools/tests -v
./gradlew --no-daemon \
-I tools/protocol-conformance.init.gradle \
:core:export:test \
--tests particeps.conformance.ProtocolConformanceTest

- name: Extract versioned release notes
shell: bash
run: >-
python3 tools/changelog_release_notes.py
"$GITHUB_REF_NAME"
"$RUNNER_TEMP/release-notes.md"

- name: Verify and package signed APK
id: signed_apk
shell: bash
run: |
set -euo pipefail
build_tools="$ANDROID_SDK_ROOT/build-tools/$ANDROID_BUILD_TOOLS_VERSION"
signed_apk="app/build/outputs/apk/release/app-release.apk"
release_dir="$RUNNER_TEMP/android-release"
Expand All @@ -153,7 +206,7 @@ jobs:
test -f "$signed_apk"
mkdir -p "$release_dir"
cp "$signed_apk" "$release_apk"
"$build_tools/apksigner" verify --verbose --print-certs "$release_apk"
python3 tools/verify_release_apk.py "$build_tools/apksigner" "$release_apk"

(
cd "$release_dir"
Expand All @@ -169,8 +222,10 @@ jobs:
APK: ${{ steps.signed_apk.outputs.apk }}
CHECKSUM: ${{ steps.signed_apk.outputs.checksum }}
IS_PRERELEASE: ${{ steps.release_metadata.outputs.prerelease }}
RELEASE_NOTES: ${{ runner.temp }}/release-notes.md
run: |
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
gh release edit "$GITHUB_REF_NAME" --notes-file "$RELEASE_NOTES"
gh release upload "$GITHUB_REF_NAME" "$APK" "$CHECKSUM" --clobber
else
release_kind=()
Expand All @@ -179,7 +234,7 @@ jobs:
fi
gh release create "$GITHUB_REF_NAME" "$APK" "$CHECKSUM" \
--verify-tag \
--generate-notes \
--notes-file "$RELEASE_NOTES" \
--title "$GITHUB_REF_NAME" \
"${release_kind[@]}"
fi
Expand Down
71 changes: 65 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,68 @@

What changed between releases, and what each change asks of someone who already installed one.

This project is pre-1.0. Every release so far is a release candidate, and each one below changed
something that a device treats as identity — the application ID, the file formats, or the signing
certificate. None of them can update an earlier install in place. That is stated once here rather
than in each document that touches it.
This project is pre-1.0. Several early release candidates changed something that a device treats as
identity — the application ID, the file formats, or the signing certificate. Do not infer update
compatibility from the version number; each release below states what an existing installation
must do.

## Unreleased

## v1.0.0-rc.6 — 2026-08-09

- Notification access is required for every study because the daily status reminder and ongoing
collection notification are app-level guarantees, not features that depend on interventions.
The app checks the Android permission, the app-wide notification switch, and each channel the
study needs. It rechecks required access before both start and resume and from the running
foreground service. A failed setup, Start, or Resume preflight leaves `ACCESS_SETUP`, `READY`, or
`PAUSED` unchanged; required access lost after the study is already `RUNNING` creates the typed
safety pause. An optional source is blocked and resumed independently.
- Collector access capabilities now live in each collector's static descriptor. The access step
keeps the collector owners when shared access is de-duplicated, orders dependent operations, and
shows app-authored English and Traditional Chinese instructions for background location, Usage
Access, and research-keyboard setup.
- Background location is no longer requested through an Android runtime dialog that cannot grant
it. After precise location is granted, Particeps first verifies the signed study's exact Fused
Location request against Android settings, then explains the background behaviour and opens the
app's Android settings page for the participant to choose Android's localized background option
manually.
- Start and Resume now wait until Android has acknowledged the foreground service with its exact
service types before any source may emit. A whole-study safety loss closes admission and records
its closed reason in an identity-free typed marker; reason-bearing WorkManager retry survives a
process restart, and each optional source has its own fail-closed event gate. Required access uses
`REQUIRED_ACCESS_MISSING`. Once a study is durably running, losing every acknowledged foreground
host during a type change uses `COLLECTION_HOST_FAILURE`; an untrustworthy store mutation uses
`STORAGE_FAILURE`; and a failed or cancelled source release uses `COLLECTION_TEARDOWN_FAILURE`.
An unacknowledged deadline, reminder, upload, intervention, or retry mutation uses
`WORK_SCHEDULING_FAILURE`. WorkManager enqueue and cancellation must be acknowledged before the App treats
the durable handoff or retry retirement as complete, so Resume cannot race a stale safety worker.
- The signed duration is now an absolute ceiling measured from the one durable participant Start.
Resume, time change, and same-boot process recovery recompute and replace the deadline from that
boundary; they cannot grant a fresh duration. Collector and occurrence admission independently
reject every observation at or beyond the exact monotonic deadline, and the deadline worker
rechecks due-ness before completing, so delayed or early WorkManager execution cannot widen or
shorten the signed window. The app trusts only the monotonic clock from the
participant-start boot. Any active study observed in another boot session fails closed with
`WORK_SCHEDULING_FAILURE` before a foreground service or collector can reopen; wall time is never
used as a cross-boot fallback.
- Safety-critical documents no longer rely on Android `AtomicFile`, which can log an `fsync` or
rename failure without returning it. The repo-owned acknowledged writer keeps independently
durable `.pending` and `.replacement` copies, preserves the first as an uncertainty witness while
atomically replacing the base with the second, and acknowledges only after exact readback and
directory sync. Any leftover witness or unknown event-directory entry blocks recovery instead of
being guessed away.
- The release workflow now requires the final APK to have exactly one signer whose certificate
matches the [rc.5 production identity anchor](.github/android-release-signing-certificate.sha256).
A different or additional certificate stops publication. The emulator gate has read-only repository
permission; only the dependent APK publication job receives write permission.

**Coming from `v1.0.0-rc.5`:** install the signed rc.6 APK over the existing app. The application ID
and release signing certificate are unchanged, so Android accepts it as an in-place update and the
active study and its local data remain in place. Do not uninstall rc.5 first.

**Coming from `v1.0.0-rc.4` or earlier:** none of those builds can update to rc.6 in place. Follow
the release-specific note below and export anything worth retaining with tooling that supports that
release before uninstalling it.

## v1.0.0-rc.5 — 2026-08-07

Expand All @@ -14,8 +72,9 @@ than in each document that touches it.
product. Either change alone stops a device accepting the build as an update; both apply.
- The status line reports when a pause started and how long it has lasted.
- One low-importance notification a day states whether the study is still collecting, or is paused
and since when. It names the application, never the study, so it discloses nothing to someone
reading a lock screen. Starting or stopping collection retracts a standing one.
and since when. It names the application and collection state, never the study; a lock-screen
reader can still infer that the phone uses Particeps. Starting or stopping collection retracts a
standing one.

**Coming from `v1.0.0-rc.4`:** uninstall it. Its data cannot be migrated, and its exports are in the
current format, so export anything worth keeping before you remove it and current tooling will read
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ license: MIT
# (v1.0.0-rc.1 through v1.0.0-rc.3) carry the pre-rename identity, a different applicationId, and
# different file formats; they are unsupported and are not versions of this software. Keep both
# fields in step with the tag at every release.
version: 1.0.0-rc.5
date-released: "2026-08-07"
version: 1.0.0-rc.6
date-released: "2026-08-09"
7 changes: 5 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ The debug APK lands at `app/build/outputs/apk/debug/app-debug.apk`. A clean chec
With an emulator or device attached:

```bash
./gradlew :core:storage:connectedDebugAndroidTest :app:connectedDebugAndroidTest
./gradlew connectedDebugAndroidTest
```

The app suite separates the Android signed-configuration regression
Expand All @@ -75,7 +75,10 @@ adb -s emulator-5554 emu sensor set proximity 1
-Pandroid.testInstrumentationRunnerArguments.p2SyntheticInputs=true
```

CI runs unit tests, Android lint, and debug and release builds on every pull request. Please check those pass locally first. Note that `allWarningsAsErrors` is on, so an unhandled branch in an exhaustive `when` is a build failure rather than a warning.
CI runs unit tests, Android lint, debug and release builds, then the complete connected suite on an
API 34 Google APIs emulator on every pull request. Please check the host-side and attached-device
commands above locally first. Note that `allWarningsAsErrors` is on, so an unhandled branch in an
exhaustive `when` is a build failure rather than a warning.

### Tests

Expand Down
Loading
Loading