Skip to content

Repository files navigation

Particeps

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.

Android CI License: MIT Android 14–17

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.

The name

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.

How a study works

  1. Generate your keys. One Ed25519 pair to sign study configurations, one X25519 HPKE pair to decrypt bundles. researcher-tools writes raw 32-byte keys as unpadded base64url.
  2. 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.
  3. Sign it. researcher-tools sign produces a .partcfg file that any build of the app can verify, with no change to the app.
  4. Distribute. Participants install the app and import your .partcfg, or open a particeps://join/v1 link 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.
  5. Collect. Events are written to encrypted on-device storage. Participants can pause, resume, or withdraw.
  6. 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-analysis inventories, verifies, decrypts, reassembles, and writes typed Parquet offline.

The full procedure, including key handling and study design guidance, is in the researcher guide.

What you can collect

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.

Adding a collector

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.

Participant data protection

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 PAUSED rather 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_REBOOT commits 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 commits PAUSED → 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.

Who published the study

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.

Quick start

Requirements

JDK 17, and Android SDK platform and build tools for API 37. The app targets Android 14 through 17 (minSdk 34, compileSdk/targetSdk 37).

Build and test

The build and test command block, the emulator-attached suites, and the sensor setup the collector integration test expects are in CONTRIBUTING.md.

Try it without a real study

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.

Researcher CLI

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

Architecture

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
Loading
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.

Documentation

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

Contributing

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.

Coming from an earlier release candidate

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.

Status

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.

License and citation

MIT — see LICENSE. If you use this platform in published work, please cite it using the metadata in CITATION.cff.

About

Participant-first sensing for research: run a mobile data collection study from a signed configuration file, without building an app.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages