Activity Trace
One tile. One bounded span.
$ composer require all1web/nativephp-activity-trace
A stateful Quick Settings tile starts and stops a foreground recording that samples motion and location, and hands PHP one buffered span it can turn into calendar history.
- Stateful Quick Settings tile starts and stops the recording
- Foreground service samples motion and location
- PHP receives one buffered span, not a firehose
Documentation
π§ nativephp-activity-trace
One tap before, one tap after. Your app gets the afternoon back as history.
The user flips a Quick Settings tile on the way into the shop and flips it off when they get home. Your Laravel app receives one recorded span β motion and location, sampled adaptively β and turns it into calendar history: "Shopping, 3:00β3:50", then "Driving, 3:50β4:15". From one recording, with the app closed the whole time.
This is a retrospective recorder, not a live tracker. Nothing is streamed, nothing is uploaded, nothing runs unless the user opened a bracket.
Status β Android complete, and NOT YET DEVICE-VALIDATED. The PHP API, the bridge contract, the configuration and the whole Android layer (tile, foreground service, activity-recognition governor, sample buffer) are written. None of it has ever run on a phone: Kotlin cannot be compiled in the environment that produced it, so nothing here is described as working until someone has walked through
docs/DEVICE-VALIDATION.mdwith a real device. iOS is not implemented and is not claimed to work β see iOS. Readdocs/DESIGN.mdbefore shipping this to production.
β¨ What you get
- ποΈ A stateful Quick Settings tile. ON while recording, OFF while not β and honest about it. The tile and the recorder are the same component, so a killed service, a reboot, or a revoked permission flips the tile back to OFF instead of lying to your user.
- π‘οΈ A foreground service with a real indicator. Android requires it for background location; this plugin makes it useful. Elapsed time, the detected activity, and a Stop button, without opening the app.
- π Adaptive sampling, not fixed-rate GPS. Activity recognition decides the cadence: standing still backs location off to one cheap fix every two minutes with the GPS chip switched off entirely; driving goes to one every 15 seconds. Fixed-rate GPS for an hour is what makes people disable a feature.
- π¦ Buffered natively, drained once. Samples accumulate on the device and cross into PHP in a single call after the bracket closes. No round trip per fix β the round trip would cost more than the fix.
- π Nothing leaves the device on its own. No endpoint, no vendor service, no background upload. Your app reads the buffer and decides what to do with it.
- π Plain Laravel. A facade, a config file, three events, and a JS wrapper for web-view front-ends.
π¦ Install
After purchasing, connect Composer to the NativePHP plugin marketplace (your credentials are on your Purchased Plugins dashboard), then:
composer config repositories.nativephp-plugins composer https://plugins.nativephp.com
composer config http-basic.plugins.nativephp.com [email protected] your-license-key
composer require all1web/nativephp-activity-trace
NativePHP plugins are opt-in for security, so requiring the package is not enough β register the provider, then compile it into the native project:
php artisan native:plugin:register all1web/nativephp-activity-trace
php artisan native:install android --force
php artisan native:run android
The rebuild matters: the tile service, the foreground service, and the permissions are written into the Android manifest at install time. They exist only after this rebuild, not after re-running an old build.
php artisan vendor:publish --tag=activitytrace-config
# Local checkout (plugin development):
composer config repositories.activity-trace path ../nativephp-activity-trace
composer require "all1web/nativephp-activity-trace:*@dev"
π§βπ» Use it
use All1web\ActivityTrace\Facades\ActivityTrace;
// Offer the tile once, during onboarding (Android 13+; false elsewhere).
ActivityTrace::requestTile();
// Open a bracket from a foregrounded screen.
$trace = ActivityTrace::start(); // ['traceId' => 'atr_β¦', 'startedAt' => 1755300000000]
// β¦later, from your UI, the tile, or the notification's Stop button.
ActivityTrace::stop(); // ['traceId' => 'atr_β¦', 'endedAt' => β¦, 'sampleCount' => 96]
The important part is the resume path. The tile can start and stop a trace while your app isn't running, so ask the recorder what happened instead of remembering:
$state = ActivityTrace::status();
// ['recording' => false, 'traceId' => null, 'startedAt' => null, 'samples' => 0,
// 'activity' => null, 'label' => null, 'pendingTraces' => 1, 'tileAdded' => true]
if ($state['pendingTraces'] > 0) {
foreach (ActivityTrace::drainAll() as $trace) {
// $trace['startedAt'], $trace['endedAt']
// $trace['samples'] => [
// ['t' => 1755300000000, 'lat' => 26.1224, 'lng' => -80.1373,
// 'acc' => 12.5, 'speed' => 1.3, 'activity' => 'walking', 'conf' => 82],
// β¦
// ]
StoreTrace::dispatch($trace);
}
}
Draining is destructive β once a trace crosses into PHP the device no longer has it. Persist first, segment into stops and journeys afterwards.
Prefer objects to arrays?
use All1web\ActivityTrace\Support\TraceSample;
$samples = TraceSample::collection($trace['samples']);
$samples[0]->activity; // 'walking'
$samples[0]->isMoving(); // true
Events (best-effort)
use All1web\ActivityTrace\Events\TraceStarted;
use All1web\ActivityTrace\Events\TraceStopped;
use All1web\ActivityTrace\Events\TraceInterrupted;
These fire only when your app is foregrounded at that moment β which is
not the common case here, since the point of the plugin is recording while it
isn't. Use them as a convenience; use status() + drainAll() on resume as
the delivery path. TraceInterrupted tells you the system ended a trace the
user didn't (service killed, location switched off), so you can label that
history as partial rather than pretend it's complete.
π Use it from JavaScript
For Livewire / Inertia / plain web-view front-ends:
import { activityTrace } from '@all1web/nativephp-activity-trace';
await activityTrace.requestTile();
const { traceId, startedAt } = await activityTrace.start();
await activityTrace.stop();
// On resume:
const state = await activityTrace.status();
if (state.pendingTraces > 0) {
const trace = await activityTrace.drain();
// trace.samples -> [{ t, lat, lng, acc, speed, activity, conf }, β¦]
}
One exported function per bridge function: start, stop, status, drain,
requestTile.
ποΈ Tune it
Everything lives in config/activitytrace.php and is forwarded to native code
on every start β change the cadence without rebuilding the app.
'sampling' => [
'still' => ['interval_ms' => 120_000, 'min_distance_m' => 25, 'priority' => 'low_power'],
'walking' => ['interval_ms' => 20_000, 'min_distance_m' => 10, 'priority' => 'high'],
'in_vehicle' => ['interval_ms' => 15_000, 'min_distance_m' => 50, 'priority' => 'high'],
// β¦
],
Also configurable: recognition interval and confidence floor, accuracy filter,
sample and duration caps, notification copy ({elapsed}, {activity}), the
tile label, and whether the tile is allowed to start a trace or only stop one.
π Permissions and the Play Store
The plugin declares ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION,
ACCESS_BACKGROUND_LOCATION, ACTIVITY_RECOGNITION, FOREGROUND_SERVICE,
FOREGROUND_SERVICE_LOCATION and POST_NOTIFICATIONS, and registers the
foreground service with foregroundServiceType="location".
ACCESS_BACKGROUND_LOCATION carries a Play Store declaration requirement.
Before your app can ship with it you must file a declaration in Play Console:
what the feature is, why it cannot work in the foreground only, a prominent
in-app disclosure shown before the first request, and usually a demonstration
video. Expect review time.
This plugin's shape is the straightforward case to justify β recording is
user-initiated, bounded, visible in both a persistent notification and a
Quick Settings tile, and the samples never leave the device on their own β but
the declaration is yours to file, and no package can do it for you. Ask
for ACCESS_FINE_LOCATION first and escalate to background in a separate,
later prompt with a clear explanation; Android and Play both expect that
order.
π iOS
Not implemented. iOS has no Quick Settings analogue at all, so the plugin's
primary control does not exist there; requestTile() returns false. The
sampling half has a real counterpart β Core Motion's
CMMotionActivityManager plus Core Location with background updates β and the
bridge contract is already shaped for it, but no iOS implementation has been
written or tested. Nothing about iOS is claimed to work. Build for Android.
π Docs
docs/DESIGN.mdβ why the tile and the service are one component, the full sampling table, the buffer-and-drain contract, the battery reasoning, and an explicit list of what is unverified.CHANGELOG.md