NeoNos Portfolio

Quick Capture Widget

Capture before the thought is gone.

Android
$ composer require all1web/nativephp-quick-capture-widget

A home-screen widget for quick note, photo and voice capture, plus a dynamic “Search” app shortcut. It piggybacks on the Share Target native inbox, so an app already using that pipeline needs zero changes.

  • Note, photo and voice capture straight from the home screen
  • Dynamic “Search” app shortcut
  • Reuses the Share Target inbox — no new capture pipeline

Documentation

Synced from GitHub 0 seconds ago

nativephp-quick-capture-widget

A home-screen widget for capturing a note, a photo, or a voice memo in one tap — before the app is even open. Plus a "Search" app shortcut reachable by long-pressing the launcher icon.

Three buttons, "Note", "Photo", and "Voice", live on the Android home screen. Tap one and you're either typing straight into the app's capture dialog or looking at the system camera / voice recorder; either way the result lands in the app's normal inbox with no extra plumbing.

Android only, v1. There is no iOS implementation — nativephp.json declares an empty "ios": {} block and no resources/ios/ directory exists, so the NativePHP iOS compiler skips this plugin entirely.


How the piggyback works

This plugin does not implement its own capture pipeline. It writes directly into the native inbox owned by all1web/nativephp-share-target — the same com.all1web.plugins.sharetarget.ShareTargetInbox object that plugin's own ShareTargetActivity (the OS share-sheet trampoline) writes into.

  • Tap NoteQuickCaptureActivity (no UI) enqueues an item with empty text and marker subject = "widget-quick-note", then brings MainActivity to the foreground exactly the way ShareTargetActivity does.
  • Tap PhotoQuickCapturePhotoActivity (transparent) fires MediaStore.ACTION_IMAGE_CAPTURE into a FileProvider URI, and on RESULT_OK copies the result into the same inbox via ShareTargetInbox.copyUri() — marker subject = "widget-quick-photo" — then brings the app to the foreground the same way. RESULT_CANCELED just finishes; nothing is enqueued.
  • Tap VoiceQuickCaptureVoiceActivity (transparent) fires MediaStore.Audio.Media.RECORD_SOUND_ACTION (no FileProvider output — the recorder app owns the file and hands back its own content Uri), and on RESULT_OK copies the result into the same inbox the same way — marker subject = "widget-quick-voice", mimeType read via contentResolver.getType() (falls back to audio/mp4 if a resolver ever declines to report one). RESULT_CANCELED (or no camera/recorder app installed at all) just finishes; nothing is enqueued.

Because all three paths write the exact same shape of item the share-target plugin already produces for an OS share, your app's existing loader fast-path → capture dialog → upload flow handles all three widget buttons with zero app-side code changes (the server-side capture pipeline already accepts audio/*, same as image/*). Drain them the normal way:

use All1web\ShareTarget\Facades\ShareTarget;

foreach (ShareTarget::pending() as $item) {
    match ($item->subject) {
        'widget-quick-note'  => $capture->openBlank(),
        'widget-quick-photo' => $capture->files($item->filePaths()),
        'widget-quick-voice' => $capture->files($item->filePaths()),
        default              => $capture->handle($item), // a real OS share
    };
}

If you don't care about distinguishing a widget tap from a real share, ignore subject entirely and just branch on $item->type() as usual.

OEM quirk worth knowing at test time: unlike the camera (ACTION_IMAGE_CAPTURE, which effectively every device answers), not every OEM ships an app that answers RECORD_SOUND_ACTION. Stock/AOSP-adjacent builds (Pixel, most Android-One devices) generally do; some other OEM skins don't ship a dedicated "sound recorder" app at all, or only expose recording from inside another app that doesn't register for this action. Per the "never resolveActivity()" rule, QuickCaptureVoiceActivity just fires the intent and catches ActivityNotFoundException — on an affected device, tapping Voice silently logs a warning and returns to the home screen with nothing enqueued (no crash, no dialog). Test the Voice button on a real stock-Android device, not just the emulator image you happen to have handy.


What you get

  • A compact 3-button AppWidgetProvider widget (res/layout/widget_quick_capture.xml), registered as a system receiver with APPWIDGET_UPDATE + its appwidget-provider metadata XML.
  • Three trampoline activities, Android-12-safe (FLAG_IMMUTABLE PendingIntents), none exported (they're only ever launched by this app's own widget).
  • A scoped FileProvider (cache-only, one subdirectory) for the camera output URI — no CAMERA permission requested; ACTION_IMAGE_CAPTURE is an implicit intent handled by whatever camera app is installed. The Voice path needs no FileProvider of its own — the recorder app owns its output file and hands back a content Uri we're granted a one-shot read on.
  • A dynamic "Search" app shortcut (long-press the launcher icon, pinnable) — see The Search app shortcut below.
  • All1web\QuickCaptureWidget\Facades\QuickCaptureWidget::isAdded() / ::count() — report whether the widget is on a home screen, for onboarding UX ("hey, did you know you can add our widget?").
  • All1web\QuickCaptureWidget\Facades\QuickCaptureWidget::publishShortcuts() / ::drainPendingRoute() — the two bridge calls behind the Search shortcut.

The Search app shortcut

Long-pressing the app's launcher icon shows a "Search" entry (a standard Android app shortcut, pinnable to the home screen by the user from there).

Why this is a ShortcutManager dynamic shortcut, not a classic res/xml/shortcuts.xml + <meta-data> static one: a static shortcut is declared via a <meta-data android:name="android.app.shortcuts"> element on the app's own LAUNCHER-category main activity — MainActivity, which this plugin doesn't own and can't patch (a plugin's manifest compiler only knows how to emit its own new components, not attach children onto a component declared elsewhere). Publishing a dynamic shortcut at runtime via ShortcutManager needs no manifest ownership of MainActivity at all, and dynamic shortcuts already show up in the long-press menu and are pinnable from there — so this sidesteps the manifest-surgery problem entirely, at the cost of the shortcut only existing once the app has run at least once after install (a <meta-data> static shortcut would be visible immediately after install, before first launch; a dynamic one needs the plugin's service provider to have booted once, which in practice means the very first app launch on the device).

How it's wired:

  1. QuickCaptureWidget::publishShortcuts() — called automatically from QuickCaptureWidgetServiceProvider::boot() on every request when running on-device (it no-ops off-device). It calls the QuickCaptureWidget.PublishShortcuts bridge function, which ShortcutManager.setDynamicShortcuts()s a single "Search" shortcut pointing at the QuickCaptureSearchActivity trampoline. Idempotent — republishing the same shortcut id every boot is harmless.
  2. Tapping the shortcut launches QuickCaptureSearchActivity (no UI, exported="true" because the system launcher process — not this app — is what starts it). It writes pending_route = "/search?focus=1" into a small SharedPreferences mailbox and brings MainActivity to the foreground, exactly like the widget's own trampolines.
  3. QuickCaptureWidget::drainPendingRoute(): ?string — call this once during your app's normal boot / loader sequence (same place you'd drain ShareTarget::pending()). It reads-and-clears the pending route atomically, so a stale route is never re-applied on a later cold start. Returns null off-device or when nothing is waiting.
use All1web\QuickCaptureWidget\Facades\QuickCaptureWidget;

if ($route = QuickCaptureWidget::drainPendingRoute()) {
    // e.g. $route === '/search?focus=1'
    return redirect($route);
}

Install

This is a private, unpublished plugin (see License below) — there is no marketplace listing. Install from local path repositories.

Both this plugin and its dependency, all1web/nativephp-share-target, must be registered as path (or VCS) repositories in the consuming app's own composer.json — Composer does not inherit repositories entries declared inside a dependency's composer.json, so declaring the share-target repository here only helps when developing/testing this plugin standalone.

In the app's composer.json:

{
    "repositories": [
        { "type": "path", "url": "../nativephp-share-target" },
        { "type": "path", "url": "../nativephp-quick-capture-widget" }
    ]
}

(Adjust the relative paths to wherever the two checkouts actually sit next to the app.)

Then, from the app:

composer require all1web/nativephp-share-target:"*@dev" all1web/nativephp-quick-capture-widget:"*@dev"

php artisan native:plugin:register all1web/nativephp-share-target
php artisan native:plugin:register all1web/nativephp-quick-capture-widget

php artisan native:install android --force
php artisan native:run android

The rebuild matters: Android decides which app owns a widget class, its manifest receiver/activities, and its FileProvider authority at install time — the widget only becomes pickable from the home screen's widget picker after this rebuild, not just a re-run of an old build.

Both native:plugin:register calls matter and in either order is fine, but both plugins must be registered — this plugin's Kotlin trampolines import com.all1web.plugins.sharetarget.ShareTargetInbox directly, so if share-target isn't registered the Android build will fail to resolve that class.


Use it

Nothing to call to receive a capture — see How the piggyback works above; drain ShareTarget::pending() as you already do.

Check whether the widget has been added (e.g. to show a "add our widget!" onboarding tip only to users who haven't):

use All1web\QuickCaptureWidget\Facades\QuickCaptureWidget;

if (! QuickCaptureWidget::isAdded()) {
    // show the tip
}

Requirements

  • NativePHP Mobile ^4.0
  • all1web/nativephp-share-target registered in the same app
  • Android 8.0+ (minSdk 26)
  • No runtime permissions needed — camera capture and voice recording both use implicit intents (ACTION_IMAGE_CAPTURE, RECORD_SOUND_ACTION) handled by whatever camera/recorder app is installed, not the CAMERA or RECORD_AUDIO permission held by this app

Deviations from the nativephp-share-target reference conventions

Documented here because this plugin was built by mirroring that plugin's structure closely — these are the three places it intentionally diverges, and why:

  1. The FileProvider is declared in a legacy resources/android/AndroidManifest.xml override, not in nativephp.json's android.providers array. AndroidPluginCompiler::buildProviderEntry() (in nativephp/mobile) only emits android:name / android:authorities / android:exported / android:grantUriPermissions for JSON-declared providers — it does not support a nested <meta-data> child the way its receiver/service builders do. A FileProvider without its FILE_PROVIDER_PATHS meta-data throws at first use, so the whole <provider> block (meta-data included) lives in the legacy XML file instead, where the compiler's extractManifestEntries() captures full component blocks verbatim.
  2. res/layout, res/xml, and res/drawable files are declared via the manifest's "assets" key, not via a copy_assets hook command (no CopyAssetsCommand exists in this plugin). PluginHookRunner::copyManifestAssets() already copies any assets.android entry whose destination starts with res/ straight into app/src/main/res/... before hooks run, so a hook command would be redundant for plain, unconditional file copies.
  3. nativephp/mobile is pinned to ^4.0 only (share-target supports ^3.0 || ^4.0) — this plugin was commissioned specifically as a v4 plugin.

Everything else — composer.json shape, PSR-4 namespace under All1web\ matching the all1web/ package vendor, src/Facades/, bridge-function/manifest conventions, resources/boost/guidelines/core.blade.php — mirrors nativephp-share-target directly. (Prior to v0.2.0 the PHP namespace and Kotlin package predated that identity unification — see UPGRADING.md if you're on an older checkout.)


License

Private / commercial line. Not published to Packagist or the NativePHP Plugin Marketplace. Source access is for internal development on this app only — do not publish, redistribute, or make this repository public.