Add Tasker action to buzz the whoop, and ability to subscribe to double-tap from Tasker#89
Add Tasker action to buzz the whoop, and ability to subscribe to double-tap from Tasker#89MrAwesome wants to merge 4 commits into
Conversation
📝 WalkthroughWalkthroughTasker buzz broadcasts, double-tap broadcasts, configurable BLE buzz patterns, pending startup handling, and profile controls are added across Android and Flutter. The profile also documents automation actions and supports scrolling gesture options. ChangesTasker integration
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant Tasker
participant TaskerReceiver
participant TaskerBridge
participant AppState
participant BleEngine
participant NativeChannels
Tasker->>TaskerReceiver: Send BUZZ_STRAP with pattern
TaskerReceiver->>TaskerBridge: Invoke buzz_strap with pattern
TaskerBridge->>AppState: Forward buzz pattern
AppState->>BleEngine: Play haptic pattern
AppState->>NativeChannels: Perform broadcast_to_tasker
NativeChannels->>Tasker: Send DOUBLE_TAP broadcast
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
.gitignore (1)
2-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winKeep
.fvmrctracked if it is the project toolchain pin.Ignoring
.fvmrcprevents contributors and CI from receiving the same FVM configuration, allowing Flutter versions to drift. Remove this entry unless another checked-in configuration pins the intended Flutter 3.11.4 toolchain.Proposed fix
.fvm -.fvmrc🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.gitignore at line 2, Remove the .fvmrc entry from .gitignore so the project toolchain pin remains tracked; only retain the ignore rule if another checked-in configuration explicitly pins Flutter 3.11.4.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@android/app/src/main/AndroidManifest.xml`:
- Around line 149-156: Protect the exported TaskerReceiver buzz endpoint by
documenting and enforcing a user-configured capability/token and rate limit in
the AndroidManifest.xml declaration and TaskerReceiver handler. In
TaskerReceiver, validate the capability and pattern range before persisting
data, invoking the channel, or starting the service; reject unauthorized,
invalid, or rate-limited requests while preserving Tasker accessibility.
In `@android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/TaskerReceiver.kt`:
- Around line 35-42: The deferred Tasker request producer and consumer use
different preference stores. In TaskerReceiver.kt lines 35-42, align writes with
the store and key scheme consumed by consumePendingBuzz(), or implement a native
consume-and-clear bridge for openstrap_runtime; in
lib/platform/tasker_bridge.dart lines 31-38, replace the Flutter preferences
read with that matching native bridge or shared store access. Ensure deferred
requests are consumed and cleared consistently.
In `@lib/platform/tasker_bridge.dart`:
- Around line 31-38: In lib/platform/tasker_bridge.dart lines 31-38, update
consumePendingBuzz to retrieve the pending pattern without removing either
preference, and provide a separate acknowledgement/clear operation. In
lib/state/app_state.dart lines 1283-1291, defer delivery until the BLE engine is
connected, invoke engine.buzzPattern(), and acknowledge the pending request only
after the send succeeds.
In `@lib/ui/profile/profile_screen.dart`:
- Around line 353-399: Update both automation long-press handlers in the
GestureDetector widgets for “Tasker buzz strap” and “Double-tap broadcast” to be
async, await Clipboard.setData, and only call _snack with the success message
after it completes. Catch write failures and show an appropriate failure
snackbar instead.
---
Nitpick comments:
In @.gitignore:
- Line 2: Remove the .fvmrc entry from .gitignore so the project toolchain pin
remains tracked; only retain the ignore rule if another checked-in configuration
explicitly pins Flutter 3.11.4.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d1d8b418-faf0-44f4-8b98-58e4788686f5
⛔ Files ignored due to path filters (1)
pubspec.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
.gitignoreandroid/app/src/main/AndroidManifest.xmlandroid/app/src/main/kotlin/wtf/openstrap/openstrap_edge/NativeChannels.ktandroid/app/src/main/kotlin/wtf/openstrap/openstrap_edge/TaskerReceiver.ktguides/BUZZ_MEANINGS.mdlib/ble/ble_engine.dartlib/gestures/device_action.dartlib/platform/tasker_bridge.dartlib/state/app_state.dartlib/ui/profile/gesture_section.dartlib/ui/profile/profile_screen.dart
| <receiver | ||
| android:name=".TaskerReceiver" | ||
| android:exported="true" | ||
| android:enabled="true"> | ||
| <intent-filter> | ||
| <action android:name="wtf.openstrap.openstrap_edge.BUZZ_STRAP" /> | ||
| </intent-filter> | ||
| </receiver> |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Protect the exported buzz endpoint.
Any installed app can explicitly target this exported receiver with the documented action, repeatedly wake the app/service, and trigger strap haptics. Keep it exported for Tasker, but require a user-configured capability/token and apply a rate limit before persisting or invoking the channel.
android/app/src/main/AndroidManifest.xml#L149-L156: document and enforce the chosen external-caller protection model.android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/TaskerReceiver.kt#L12-L50: validate the capability and pattern range before executing or starting the service.
📍 Affects 2 files
android/app/src/main/AndroidManifest.xml#L149-L156(this comment)android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/TaskerReceiver.kt#L12-L50
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@android/app/src/main/AndroidManifest.xml` around lines 149 - 156, Protect the
exported TaskerReceiver buzz endpoint by documenting and enforcing a
user-configured capability/token and rate limit in the AndroidManifest.xml
declaration and TaskerReceiver handler. In TaskerReceiver, validate the
capability and pattern range before persisting data, invoking the channel, or
starting the service; reject unauthorized, invalid, or rate-limited requests
while preserving Tasker accessibility.
| val prefs = context.getSharedPreferences( | ||
| "openstrap_runtime", | ||
| Context.MODE_PRIVATE | ||
| ) | ||
| prefs.edit() | ||
| .putBoolean(PENDING_BUZZ_KEY, true) | ||
| .putInt(PENDING_PATTERN_KEY, pattern) | ||
| .apply() |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
For shared_preferences version 2.3.3 on Android, does SharedPreferences.getInstance() read an arbitrary native preference file named "openstrap_runtime", or only the plugin-managed default store? Include the key-prefix behavior.
💡 Result:
For the shared_preferences plugin (version 2.3.3), the SharedPreferences.getInstance method interacts with a default, plugin-managed SharedPreferences file on Android, not an arbitrary file named "openstrap_runtime" [1][2][3]. By default, the plugin uses a specific SharedPreferences file internally [4]. It does not automatically target or read from other arbitrary native preference files like "openstrap_runtime". Regarding key-prefix behavior: 1. Default Prefix: By default, the plugin only reads and writes preferences that begin with the prefix "flutter." [5][3]. This filtering is handled automatically by the plugin [5][3]. 2. Custom Prefixes: You can change this behavior by calling setPrefix before instantiating any SharedPreferences object [5][3]. If you set the prefix to an empty string (""), the plugin will read all keys within that SharedPreferences file, which is often used for migrating existing native Android preferences to Flutter [5][3]. 3. Interaction with Async APIs: While newer APIs like SharedPreferencesAsync (available in later versions) allow specifying custom file names via SharedPreferencesAsyncAndroidOptions (e.g., using AndroidSharedPreferencesStoreOptions(fileName: '...')), the standard SharedPreferences.getInstance API in version 2.3.3 remains tied to the plugin's default storage configuration [6][1][2].
Citations:
- 1: https://pub.dev/packages/shared_preferences
- 2: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences
- 3: https://pub.dev/documentation/shared_preferences/latest/
- 4: https://chromium.googlesource.com/external/github.com/flutter/packages/+/refs/tags/shared_preferences_windows-v2.3.0/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java
- 5: https://raw.githubusercontent.com/flutter/packages/main/packages/shared_preferences/shared_preferences/README.md
- 6: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_android
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the relevant files first
ast-grep outline android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/TaskerReceiver.kt --view expanded || true
ast-grep outline lib/platform/tasker_bridge.dart --view expanded || true
# Read the relevant slices with line numbers
echo '--- TaskerReceiver.kt ---'
cat -n android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/TaskerReceiver.kt | sed -n '1,220p'
echo '--- tasker_bridge.dart ---'
cat -n lib/platform/tasker_bridge.dart | sed -n '1,220p'Repository: OpenStrap/edge
Length of output: 4691
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate any shared_preferences usage and any native bridge for pending buzz
rg -n "SharedPreferences\.getInstance|openstrap_runtime|PENDING_BUZZ_KEY|PENDING_PATTERN_KEY|consumePendingBuzz|setPrefix|flutter\." android libRepository: OpenStrap/edge
Length of output: 8442
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find where shared_preferences plugin behavior is configured, if at all
rg -n "SharedPreferences\.setPrefix|SharedPreferencesAsync|AndroidSharedPreferencesStoreOptions|SharedPreferencesWithCache|shared_preferences" lib androidRepository: OpenStrap/edge
Length of output: 2622
Use the same store for deferred Tasker requests. SharedPreferences.getInstance() reads the plugin-managed FlutterSharedPreferences store (flutter. keys), so the openstrap_runtime entries written here are invisible to consumePendingBuzz().
android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/TaskerReceiver.kt#L35-L42: write to the same store/key scheme, or expose a native consume-and-clear bridge foropenstrap_runtime.lib/platform/tasker_bridge.dart#L31-L38: call that bridge instead of the Flutter preferences plugin.
📍 Affects 2 files
android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/TaskerReceiver.kt#L35-L42(this comment)lib/platform/tasker_bridge.dart#L31-L38
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/TaskerReceiver.kt`
around lines 35 - 42, The deferred Tasker request producer and consumer use
different preference stores. In TaskerReceiver.kt lines 35-42, align writes with
the store and key scheme consumed by consumePendingBuzz(), or implement a native
consume-and-clear bridge for openstrap_runtime; in
lib/platform/tasker_bridge.dart lines 31-38, replace the Flutter preferences
read with that matching native bridge or shared store access. Ensure deferred
requests are consumed and cleared consistently.
| static Future<int?> consumePendingBuzz() async { | ||
| final prefs = await SharedPreferences.getInstance(); | ||
| final pending = prefs.getBool(_pendingKey) ?? false; | ||
| if (!pending) return null; | ||
| final pattern = prefs.getInt(_pendingPatternKey) ?? 2; | ||
| await prefs.remove(_pendingKey); | ||
| await prefs.remove(_pendingPatternKey); | ||
| return pattern; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not acknowledge a pending buzz before BLE is ready.
_init() starts openSession() without awaiting it, then immediately consumes and deletes the pending request. engine.buzzPattern() can therefore run before a connection exists, where its write is skipped and the request is lost.
lib/platform/tasker_bridge.dart#L31-L38: split read from acknowledgement; do not clear the request during retrieval.lib/state/app_state.dart#L1283-L1291: deliver only after the engine is connected, then acknowledge/clear after a successful send.
📍 Affects 2 files
lib/platform/tasker_bridge.dart#L31-L38(this comment)lib/state/app_state.dart#L1283-L1291
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/platform/tasker_bridge.dart` around lines 31 - 38, In
lib/platform/tasker_bridge.dart lines 31-38, update consumePendingBuzz to
retrieve the pending pattern without removing either preference, and provide a
separate acknowledgement/clear operation. In lib/state/app_state.dart lines
1283-1291, defer delivery until the BLE engine is connected, invoke
engine.buzzPattern(), and acknowledge the pending request only after the send
succeeds.
| // ── Automation (Tasker) ────────────────────────────────────── | ||
| const SectionHeader('Automation'), | ||
| _SettingsCard(rows: [ | ||
| GestureDetector( | ||
| onLongPress: () { | ||
| Clipboard.setData( | ||
| const ClipboardData(text: 'wtf.openstrap.openstrap_edge.BUZZ_STRAP'), | ||
| ); | ||
| _snack(context, 'Copied BUZZ_STRAP action.'); | ||
| }, | ||
| child: ListRow( | ||
| icon: OsIcon.activity, | ||
| title: 'Tasker buzz strap', | ||
| subtitle: 'wtf.openstrap.openstrap_edge.BUZZ_STRAP', | ||
| onTap: null, | ||
| ), | ||
| ), | ||
| ]), | ||
| const _CardNote( | ||
| 'Send a Broadcast intent from Tasker or any automation app to ' | ||
| 'vibrate your WHOOP strap. On Android 12+, set Package to ' | ||
| 'wtf.openstrap.openstrap_edge. Optional int extra "pattern" (default 2).', | ||
| ), | ||
| const SizedBox(height: Sp.x3), | ||
| _SettingsCard(rows: [ | ||
| GestureDetector( | ||
| onLongPress: () { | ||
| Clipboard.setData( | ||
| const ClipboardData(text: 'wtf.openstrap.openstrap_edge.DOUBLE_TAP'), | ||
| ); | ||
| _snack(context, 'Copied DOUBLE_TAP action.'); | ||
| }, | ||
| child: ListRow( | ||
| icon: OsIcon.activity, | ||
| title: 'Double-tap broadcast', | ||
| subtitle: 'wtf.openstrap.openstrap_edge.DOUBLE_TAP', | ||
| onTap: null, | ||
| ), | ||
| ), | ||
| ]), | ||
| const _CardNote( | ||
| 'Set double-tap gesture to "Broadcast to Tasker", then create ' | ||
| 'a Tasker profile: Event → Intent Received → action above.', | ||
| ), | ||
|
|
||
| const SizedBox(height: Sp.x6), | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the target file around the reported lines
nl -ba lib/ui/profile/profile_screen.dart | sed -n '320,430p'
# Find the helper and any existing async patterns in the file
rg -n "Clipboard\.setData|_snack\(|mounted|onLongPress:" lib/ui/profile/profile_screen.dartRepository: OpenStrap/edge
Length of output: 192
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the target section with line numbers using tools more likely to be available
sed -n '320,430p' lib/ui/profile/profile_screen.dart | cat -n
# Inspect clipboard usage and mounted checks elsewhere in the file
rg -n "Clipboard\.setData|_snack\(|mounted|onLongPress:" lib/ui/profile/profile_screen.dartRepository: OpenStrap/edge
Length of output: 6863
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check whether the file or project already uses async clipboard handling patterns
rg -n "await Clipboard\.setData|try \{|catch \(|context\.mounted|if \(mounted\)" lib/ui/profile/profile_screen.dart lib/ui -g '*.dart'Repository: OpenStrap/edge
Length of output: 18290
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the reported region with line numbers
sed -n '340,405p' lib/ui/profile/profile_screen.dart | cat -n
# Locate exact clipboard calls and any async error handling nearby
rg -n "Clipboard\.setData|_snack\(|context\.mounted|mounted|onLongPress:" lib/ui/profile/profile_screen.dartRepository: OpenStrap/edge
Length of output: 4719
Await the clipboard write before showing success.
Clipboard.setData is async here, so both long-press handlers can show a success snackbar before the copy completes. Make them async, await the call, and show a failure message if the write throws.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/ui/profile/profile_screen.dart` around lines 353 - 399, Update both
automation long-press handlers in the GestureDetector widgets for “Tasker buzz
strap” and “Double-tap broadcast” to be async, await Clipboard.setData, and only
call _snack with the success message after it completes. Catch write failures
and show an appropriate failure snackbar instead.
NOTE: I have zero experience with Android development so this is heavily vibe-coded. Happy to do any testing or UI changes you see fit! Just wanted to put something up while the fire is hot.
So far, I've tested that the Tasker actions work in both directions. A double-tap can trigger profiles, and with Send Intent you can vibrate the whoop from any Tasker task.
Long-press on the action names in the menu to copy to clipboard.
As a bonus, this fixes scrolling on the double-tap menu, which previously ran off the bottom of the screen and couldn't be scrolled.
Summary by CodeRabbit