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
2 changes: 0 additions & 2 deletions .github/workflows/receiver.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@ on:
paths:
- "receiver/**"
- "protocol/v1/**"
- "docs/p0-p2-implementation-contract.md"
- ".github/workflows/receiver.yml"
pull_request:
paths:
- "receiver/**"
- "protocol/v1/**"
- "docs/p0-p2-implementation-contract.md"
- ".github/workflows/receiver.yml"
workflow_dispatch:

Expand Down
61 changes: 61 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Changelog

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.

## Unreleased

- The application ID moved from `cool.linc.particeps` to `cool.jacoblin.particeps`, and the release
signing key was rotated so that the certificate names Particeps rather than the pre-rename
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.

**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
it. See [the participant guide](docs/participant-guide.md) for the participant-facing version.

## v1.0.0-rc.4 — 2026-08-06

The project was renamed from Android Data Collector to Particeps. Application ID
`cool.linc.particeps`.

Protocol v1 keeps `schema_version: 1` and gains no second dialect; every identity string was
replaced at once:

| | Was | Now |
| --- | --- | --- |
| Signed configuration | `.adccfg`, `ADCCFG01` | `.partcfg`, `PTCCFG01` |
| Encrypted export | `.adcexp`, `ADCEXP01` | `.partexp`, `PTCEXP01` |
| Join URI | `adc://join/v1` | `particeps://join/v1` |
| Bundle format | `research-bundle-v1` | `particeps-research-bundle-v1` |
| Upload media type | `application/vnd.adc.research-bundle` | `application/vnd.particeps.research-bundle` |
| Upload headers | `X-ADC-*` | `X-Particeps-*` |
| Offline analysis | `adc-analysis` | `particeps-analysis` |

The retired spellings are rejected inputs rather than an older dialect. Every implementation fails
closed on them, and the shared conformance corpus carries a vector for each.

**Coming from `v1.0.0-rc.3` or earlier:** uninstall it first — it is a different application ID and
runs alongside. Its exports are `.adcexp` files that current tooling refuses, so anything worth
keeping has to be exported and analysed with the pre-rename tooling before you remove it.

## v1.0.0-rc.3 — 2026-08-05

Application ID `cool.linc.androiddatacollector`. The R2 ciphertext receiver, the offline
verification and Parquet pipeline, immutable signed join links, and the battery, temporal-context,
gyroscope, ambient-light and proximity collectors.

## v1.0.0-rc.2 — 2026-08-03

Application ID `cool.linc.androiddatacollector`.

## v1.0.0-rc.1 — 2026-08-02

First release candidate. Application ID `cool.linc.androiddatacollector`.
39 changes: 36 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ A collector is three pieces:

### Design constraints

Collectors observe a source and emit events. They do not write files, change study state, start activities, schedule interventions, render surveys, export, or request permissions. This is not a rule imposed on collector authors so much as a consequence of the module graph: a `collector:*` module depends only on `core:collector-api` and `core:study-definition`, so storage, the runtime, and the protocol layer are not on its classpath.
Collectors observe a source and emit events. They do not write files, change study state, start activities, schedule interventions, render surveys, export, or request permissions. This is not a rule imposed on collector authors so much as a consequence of the module graph. A `collector:*` module depends only on `core:collector-api` and `core:study-definition`, so storage, the runtime, and the protocol layer are not on its classpath.

That boundary is what keeps a new data source cheap to add and cheap to review. It also means the answer to "how do I persist this myself?" is that you do not — everything goes through the `EventSink` in your `CollectorContext`, which is what makes sequence numbers contiguous and monotone, quota accounting correct, and a bundle able to declare the exact window it carries.
That boundary is what keeps a new data source cheap to add and cheap to review. It also means the answer to "how do I persist this myself?" is that you do not. Everything goes through the `EventSink` in your `CollectorContext`. That is what makes sequence numbers contiguous and monotone, quota accounting correct, and a bundle able to declare the exact window it carries.

Note that the boundary is enforced by the dependency graph and by review, not by a sandbox. There is currently no architecture test asserting it.
The boundary is checked, not merely reviewed. `tools/collector_assurance.py` reads `assurance/collector-policy.json` and fails CI on a forbidden import, a forbidden Gradle dependency, or a forbidden symbol in a compiled class. What it does not read is the manifest, so a collector module can still declare a permission or a component that nothing stops — that gap is tracked in issue #11.

### What review will look at

Expand All @@ -46,12 +46,35 @@ Requirements: JDK 17, Android SDK platform and build tools for API 37.
./gradlew test testDebugUnitTest lintDebug assembleDebug assembleRelease
```

The debug APK lands at `app/build/outputs/apk/debug/app-debug.apk`. A clean checkout has no signing material, so `assembleRelease` produces an unsigned release APK.

With an emulator or device attached:

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

The app suite separates the Android signed-configuration regression
([`AndroidConfigurationImportTest`](app/src/androidTest/kotlin/cool/jacoblin/particeps/AndroidConfigurationImportTest.kt)),
the full participant UI flow ([`CoreFlowTest`](app/src/androidTest/kotlin/cool/jacoblin/particeps/CoreFlowTest.kt)),
and the five-collector Android integration
([`P2CollectorEmulatorTest`](app/src/androidTest/kotlin/cool/jacoblin/particeps/P2CollectorEmulatorTest.kt)).
The last test skips when gyro, light, or proximity hardware is absent. Its optional exact-value mode
expects a sensor-capable emulator that the host has already configured; the test does not fake
Android's sensor APIs:

```bash
adb -s emulator-5554 emu power ac on
adb -s emulator-5554 emu power status charging
adb -s emulator-5554 emu power capacity 73
adb -s emulator-5554 emu sensor set gyroscope 1.25:-2.5:0.5
adb -s emulator-5554 emu sensor set light 123
adb -s emulator-5554 emu sensor set proximity 1
./gradlew :app:connectedDebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=cool.jacoblin.particeps.P2CollectorEmulatorTest \
-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.

### Tests
Expand All @@ -73,6 +96,16 @@ Two conventions worth knowing:

Keep them focused — one collector, or one fix. Explain what changed and why, and say explicitly if the change affects what data can be collected or what a participant sees.

A change is finished when all five of these hold:

1. the targeted tests and the full build checks pass;
2. a review has checked the failure paths, not only the success path;
3. nothing was added that a simpler version would not need — no duplicate implementation, no legacy path, no unused code, no avoidable dependency;
4. the code you touched is clearer than you found it; and
5. a reader who has never seen the change can find its specification, its source, its tests, and its operational documentation starting from the root documentation.

The fifth is the one people skip. It is also the one that decides whether anyone can maintain this after you.

By contributing you agree that your contribution is licensed under the [MIT License](LICENSE).

## Security issues
Expand Down
Loading
Loading