Participant-first sensing for research. Run a mobile data collection study without building an app. A study is a signed configuration file: choose which collectors to run, set their parameters and the study duration, sign it, and hand it to participants. Data is collected on the device and encrypted as it is written. It reaches you as an encrypted export the participant sends, or on a schedule if the study names an upload endpoint.
Standing up a mobile sensing study normally means writing an Android app, getting permissions right, handling background execution, and building a data pipeline — before collecting a single sample. This platform does that part once. Designing a new study means writing a JSON file and signing it.
Particeps is Latin for one who takes part or shares in something, and it is the root of participant.
The name describes where the design puts the participant, and it is worth being exact about what that does and does not mean. Events are written and encrypted on the participant's own phone. Every collector a study enables is shown to them before they are asked to consent, with what it records and what it cannot establish. Nothing is collected until they press Start. Those are defaults the implementation actually provides.
What the name does not grant is authorship of the study. The collector set, the duration, and whether the study uploads on a schedule are fixed in the signed configuration. A participant cannot change them, add to them, or recall a bundle once it has been delivered. Their leverage over a running study is bounded and real — decline it outright, withhold the Android access an optional collector needs so that collector stays off, pause, withdraw, and delete the local data. A collector the configuration marks required is not optional in that sense: withholding its access stops the study rather than trimming it.
- Generate your keys. One Ed25519 pair to sign study configurations, one X25519 HPKE pair to decrypt bundles.
researcher-toolswrites raw 32-byte keys as unpadded base64url. - Write the study. A strict Protocol v1 RFC 8785 JSON file naming collectors, reusable surveys, scheduled interventions, anonymous or assigned-code identity mode, duration, storage quota, consent text, and signing/export public keys.
- Sign it.
researcher-tools signproduces a.partcfgfile that any build of the app can verify, with no change to the app. - Distribute. Participants install the app and import your
.partcfg, or open aparticeps://join/v1link that names where those exact bytes are served and pins their SHA-256. Setup is five steps, one screen each: the study details, what each enabled collector records and does not record, the consent text with the signer's key fingerprint, the Android access the study and its collectors need, and the start button. Collection begins only when they press the start button. - Collect. Events are written to encrypted on-device storage. Participants can pause, resume, or withdraw.
- Export and analyse. The participant exports an encrypted bundle and sends it to you. If the study declares an upload endpoint, the app also delivers immutable ciphertext bundles to an R2 receiver on a schedule.
particeps-analysisinventories, verifies, decrypts, reassembles, and writes typed Parquet offline.
The full procedure, including key handling and study design guidance, is in the researcher guide.
Twelve selectable collectors ship in v1. A study enables the ones it names and configures each one's parameters within validated ranges.
| Collector | Records |
|---|---|
app_lifecycle.v1 |
Lifecycle of this app's own activities |
accelerometer.v1 |
Raw x/y/z acceleration, sensor time, accuracy |
battery_state.v1 |
Battery percentage, charging state/source, power-save state |
temporal_context.v1 |
Time-zone ID, UTC offset, DST state, clock-change reason |
gyroscope.v1 |
Raw x/y/z angular velocity, sensor time, accuracy |
ambient_light.v1 |
Raw illuminance, sensor time, accuracy |
proximity.v1 |
Raw distance, sensor range, near/far interpretation |
network_state.v1 |
Default network transport, validated/metered/roaming/VPN flags, bandwidth estimates |
network_usage.v1 |
Device-total Wi-Fi and mobile rx/tx bytes and packets per interval |
usage_events.v1 |
Raw app, screen, keyguard, and boot events |
location.v1 |
Fused Location fixes with accuracy, speed, altitude, bearing |
keyboard_touch.v1 |
Within-key touch position, timing, pressure, size, key category |
What each collector cannot establish is set out per collector in the researcher guide, which is where a study is designed; field-level definitions of every event, including units and timestamp semantics, are in the data dictionary.
Some practical notes: package names, location, fine-grained timing, acceleration, and keyboard dynamics can all be identifying, and ethics review will ask about that. Choosing the fewest collectors, the lowest usable rate, and the shortest duration that answers your question makes both the review and the analysis easier.
The collector set is meant to grow. A collector is a Gradle module implementing three things: a typed configuration that appears in the signed study file, a plugin descriptor declaring what access it needs, and a runtime instance that observes its source and emits events.
Collector modules depend only on core:collector-api, core:study-definition, and, for Android
hardware listeners, the narrow collector:sensor-common lifecycle helper. A new data
source does not touch storage, the runtime, or protocol/export code. The
implementation guide walks through the contract and every registration step.
Studies collect from people's personal phones, so the platform is built to support a defensible ethics submission and honest commitments to participants.
- Encrypted on the device. Each study's events and metadata are encrypted with a per-study AES-256-GCM key from the Android Keystore, marked non-exportable. They are written in 4 MiB event segments under the app's no-backup storage, up to the quota the configuration set. An event is appended once and never rewritten. The only thing that removes a segment is confirmed delivery to the study's endpoint, and then only under storage pressure.
- Signed, tamper-evident studies. A configuration is Ed25519-signed and strictly validated: RFC 8785 bytes, exact schema, known collectors, Android platform, validity window, and minimum client build. Verification failures are fail-closed — an unverifiable configuration collects nothing. A signature proves the configuration is unchanged since it was signed; it does not prove who wrote it unless the build pins that signer, and the consent screen states which of the two applies.
- Durable interventions and native surveys. Notification actions can use one-time, recurring, daily-local, or signed random-local-window triggers. Random instants are selected with a CSPRNG and persisted before scheduling, so retries and reboot do not redraw them. Clock and time-zone changes do not rewrite already materialized occurrences. Native surveys support short text, integer scales, single choice, and multiple choice; only a confirmed, complete submission enters the encrypted event stream.
- Separated participant identities. Every import gets a fresh random instance UUID. A configuration may additionally carry an opaque researcher-assigned code; both appear in the encrypted document. Upload URLs and headers contain no participant, assigned, experiment, or configuration ID. Their bundle UUID, configuration digest, researcher key ID, exact range/count, size, and digest are untrusted routing claims, not participant authentication.
- Encrypted, participant-directed export. Getting data to the research team is an export the participant performs and directs, encrypted with a fresh key per export and wrapped to your HPKE public key. The app never holds your private key.
- Scheduled upload, when the study asks for it. A configuration may name an HTTPS endpoint, interval, and metered-network policy. The endpoint host, cadence, and network condition are shown before consent. Before HTTP starts, the app durably stages one immutable ciphertext bundle in no-backup storage: about 16 MiB of plaintext and at most 32 MiB on the wire. Retries send those exact bytes with fixed length and digest. A bundle counts as delivered only when the receiver returns a receipt that matches the staged bundle exactly, so any other response leaves those events undelivered. Protocol v1 defines the receipt and which responses may advance the watermark. Duration completion or withdrawal leaves delivery running until the tail arrives. Undelivered events are never reclaimed to make room.
- Participant control over the lifecycle. Collection starts only on an explicit action and can be paused or withdrawn. The runtime takes a monotonic boundary, asks sources to release their callbacks, closes admission, waits for writes already admitted before that boundary, and only then commits the participant transition. A failed or cancelled teardown leaves a typed durable cleanup witness instead of reporting a clean boundary.
- Storage failures stop collection. Quota exhaustion or a write failure closes every event gate and persists a typed safety witness before the failing mutation returns. The study moves to
PAUSEDrather than silently dropping events, so a dataset is complete over the window it declares or absent. - The duration is an admission ceiling. Metadata v2 carries one durable clock checkpoint derived from the participant's only Start. Within a boot, every collector, occurrence, deadline, and intervention decision uses
elapsedRealtimeNanos. Across a reboot,RUNNING → PAUSED / DEVICE_REBOOTcommits before any source reopens; the calendar clock advances only from Android's network time or wall time while system automatic time is enabled, and the unconfirmed reboot gap never advances the active-collection clock. After deadline, access, WorkManager, and foreground-service checks succeed, the app commitsPAUSED → RUNNING / AUTOMATIC_RECOVERY. Without trusted UTC it remains paused and posts a repair notification. No recovery can grant a fresh duration or admit an event at or beyond the checkpoint-derived deadline.
One published app can verify and run any researcher's study. The cost is that a signature alone says nothing about origin: the researcher name and contact shown on the consent screen are text the signer chose. The mitigation is the signing key fingerprint, which the consent step shows under the heading Configuration signature. Publish your fingerprint in the material that recruits participants so they can compare the two. Note also that a participant reaches a study through your recruitment channel rather than an anonymous download. How the signing key travels inside the signed bytes, and how its fingerprint is derived, is in the threat model.
The shipped build pins no signer, so it accepts any correctly signed configuration and tells the participant that the publisher is unverified. An institution that wants one build to run only its own studies adds its key to TRUSTED_SIGNING_KEYS in CollectorApplication and ships that build; every other signer is then refused outright.
For ethics reviewers, the threat model documents the trust assumptions and, more usefully, what the design does not protect against.
JDK 17, and Android SDK platform and build tools for API 37. The app targets Android 14 through 17 (minSdk 34, compileSdk/targetSdk 37).
The build and test command block, the emulator-attached suites, and the sensor setup the collector integration test expects are in CONTRIBUTING.md.
researcher-tools/examples contains a demonstration study and its key pair. Those keys are public fixtures committed to this repository, fine for development and emulator testing but never for real participants, and a release build ships no demonstration study at all — see researcher-tools/examples/README.md. Build the debug variant if you want to try the participant flow without a configuration of your own.
signing-keygen generate an Ed25519 signing pair
hpke-keygen generate a raw X25519 HPKE key pair
canonicalize strictly parse and emit a canonical configuration
sign sign a canonical configuration into .partcfg
personalize sign one canonical configuration and .partcfg per row of an assigned-code mapping
check-config verify envelope, signature, platform, validity window, and client build; optionally pin the signer
decrypt decrypt a .partexp into particeps-research-bundle-v1 JSON
flowchart LR
UI[":app Compose UI"] --> VM["StudyViewModel"]
VM --> Session[":core:study-application"]
Session --> Runtime[":core:experiment-runtime"]
Runtime --> API[":core:collector-api"]
Collectors[":collector:*"] --> API
Session --> StorePort["StudyStore port"]
Storage[":core:storage"] --> StorePort
Session --> Export[":core:export"]
Export --> Crypto[":core:crypto"]
Android[":app Android adapters"] --> Session
Android --> Receiver["receiver/ Cloudflare Worker"]
Receiver --> R2["private R2 ciphertext"]
Access[":core:access"] --> Session
Protocol[":core:protocol"] --> Definition[":core:study-definition"]
Tools[":researcher-tools"] --> Definition
Tools --> Protocol
Tools --> Export
| Module | Responsibility |
|---|---|
:app |
Compose UI, finite UI state, SAF, and Android foreground/work/recovery/upload adapters |
:core:model |
Bounded study metadata, state and event models, StudyStore port and its retained window |
:core:study-definition |
Strict canonical JSON, closed-world typed study and collector configuration |
:core:protocol |
Signed envelope, immutable join URI, signature verification, optional signer pinning, validity and version checks |
:core:collector-api |
Collector lifecycle, health, registry, access contract, shared callback dispatcher |
:core:crypto |
Protocol v1 raw-key Ed25519 verification and fixed-suite RFC 9180 HPKE over raw X25519 keys; Tink is internal only, never a wire keyset |
:core:access |
Runtime permission, Usage Access, input method, and hardware preflight |
:core:experiment-runtime |
Command serialisation, state machine, collector supervision, event admission gate |
:core:study-application |
The single active-study session, recovery, port coordination, and the upload watermark |
:core:storage |
Keystore-backed encrypted metadata, appended event segments, and reclaiming delivered ones |
:core:export |
Streaming JSON to AES-GCM over a sequence window under an optional size budget, HPKE key wrapping, receipts |
:collector:* |
One isolated module per data source |
:researcher-tools |
Ed25519 and HPKE keys, canonicalise, sign, verify, decrypt CLI |
receiver/ |
One bounded Protocol v1 upload POST, immutable ciphertext writes, and canonical receipts |
Platform-independent modules contain no android.* imports, which keeps the domain logic testable on the JVM. System design documents the module contracts.
New contributors should treat protocol/v1 as the normative wire contract, the collector catalog as the schema source, and docs/system-design.md for how the modules fit together. Trace one path through the configuration codec, signed envelope, bundle exporter, bundle verifier, single-entry outbox, HTTP adapter, receiver handler, and the offline particeps-analysis pipeline. The join path is similarly short: Web authoring, shared parser, Android staging, intent entry, then the existing session import. The outbox, uploader, and receiver tests make crash/replay and receipt semantics executable. Receiver deployment and R2 operations start at receiver/README.md, and the Collector capability policy lives under assurance.
For random_window, trace the signed model and bounds in
StudyConfiguration.kt,
its codec and Web editor, then the CSPRNG
materialization in
InterventionSchedulePlanner.kt.
The session
persists the occurrence before scheduling. The Android delivery and expiry workers in
AndroidStudyPlatform.kt
and ScheduledWorkRecoveryReceiver
reconcile the same ID after retries, reboot, clock, or time-zone changes. The adjacent planner,
runtime, session, and app policy tests make each boundary executable.
| Document | For |
|---|---|
| Changelog | What changed between releases, and what it asks of an existing install |
| Researcher guide | Designing, signing, deploying, and analysing a study |
| Data dictionary | Every field on every event, per collector |
| Participant guide | People taking part in a study |
| Collector implementation guide | Writing a new collector |
| System design | The implemented v1 architecture in full |
| Threat model | Trust assumptions and limitations, for ethics review |
| Normative Protocol v1 | JCS, keys, join URI, binary framing, bundle document, upload, receipt, and conformance corpora |
| Collector capability policy | Static source, bytecode, and dependency boundaries for collectors |
| Ciphertext receiver | R2-only Worker contract, verification commands, deployment, and operations |
| Offline analysis | Ciphertext inventory, verification, reassembly, and typed Parquet materialization |
| Release process | Maintainers |
New collectors are the main contribution path — see CONTRIBUTING.md and the implementation guide. To report a security or privacy issue, see SECURITY.md rather than opening a public issue.
v1.0.0-rc.5 established the current application ID and the production signing certificate recorded
in the repository's auditable identity anchor,
so rc.7 updates rc.5 and rc.6 in place. Rc.4 and earlier use another application ID, signing
certificate, or file identity and cannot update directly to the current build. Those older apps keep
running under their own identity until removed. Uninstalling destroys their Keystore key and
everything encrypted under it, so export whatever is still wanted first. Artifacts from before the
rename are unsupported input to every current implementation, and there is no converter.
CHANGELOG.md says which release carries which identity, which spellings it retired,
and what each release asks of an existing install.
This repository implements and tests the full local participant flow on Android 14–17.
The app's own screens ship in English and Traditional Chinese. The interface follows the phone's system language, and a picker in the app's header changes it for this app alone. That picker writes through Android's LocaleManager, so it is the same setting as the system's per-app language screen rather than a second one beside it. Adding a language is a values-* directory and one line in res/xml/locales_config.xml.
Researcher-supplied text is a separate matter. The study title, purpose, researcher name, contact, and consent summary are rendered exactly as they were signed, in whatever language they were written, whatever language the app is in. Recruiting across languages therefore means one signed configuration per language.
Running a real study also needs work this repository cannot do for you: ethics and legal approval, your own study signing key and a published fingerprint for it, a data governance plan, and validation on the physical devices and OEM builds you intend to support. Emulator tests passing is not ethics approval, Play policy compliance, or scientific validity.
MIT — see LICENSE. If you use this platform in published work, please cite it using the metadata in CITATION.cff.