NeoNos Portfolio

Activity Trace

One tile. One bounded span.

Android
$ 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

Synced from GitHub 0 seconds ago

🧭 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.md with a real device. iOS is not implemented and is not claimed to work β€” see iOS. Read docs/DESIGN.md before 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

πŸ†˜ Support

[email protected]