From 3407f4c705f754add792c9156463a26b93eeadb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 05:52:39 -0700 Subject: [PATCH] Add wake lock and device keepalive for reliable mobile calls Prevents screen sleep during calls and auto-reconnects the Twilio device after app backgrounding. JS handles Screen Wake Lock API and 30s keepalive polling; Flutter coordinates via WakelockPlus and synthetic visibilitychange events on app resume. Co-Authored-By: Claude Opus 4.6 (1M context) --- assets/mobile/phone.js | 75 ++++++++++- .../plugins/GeneratedPluginRegistrant.java | 15 +++ mobile/lib/screens/phone_screen.dart | 18 +++ mobile/pubspec.lock | 120 +++++++++++++----- mobile/pubspec.yaml | 3 +- 5 files changed, 197 insertions(+), 34 deletions(-) diff --git a/assets/mobile/phone.js b/assets/mobile/phone.js index 1213c44..1285181 100644 --- a/assets/mobile/phone.js +++ b/assets/mobile/phone.js @@ -110,6 +110,8 @@ var twpTwilioEdge = window.twpConfig.twilioEdge; var serviceWorkerRegistration = null; var currentCallDirection = null; var callHistory = []; + var wakeLock = null; + var deviceKeepaliveTimer = null; // ============================================================ // AudioContext & Ringtone @@ -193,13 +195,79 @@ var twpTwilioEdge = window.twpConfig.twilioEdge; } } + // ============================================================ + // Screen Wake Lock + // ============================================================ + async function requestWakeLock() { + if (!('wakeLock' in navigator)) return; + try { + wakeLock = await navigator.wakeLock.request('screen'); + console.log('TWP: Wake lock acquired'); + wakeLock.addEventListener('release', function() { + console.log('TWP: Wake lock released'); + wakeLock = null; + }); + } catch (e) { + console.warn('TWP: Wake lock request failed:', e.message); + } + } + + function releaseWakeLock() { + if (wakeLock) { + wakeLock.release().catch(function() {}); + wakeLock = null; + } + } + + // ============================================================ + // Device Keepalive Monitor + // ============================================================ + function startDeviceKeepalive() { + stopDeviceKeepalive(); + deviceKeepaliveTimer = setInterval(function() { + if (device && deviceConnectionState !== 'connected' && !currentCall) { + console.log('TWP: Keepalive — device disconnected, re-registering'); + device.register().catch(function(e) { + console.warn('TWP: Keepalive re-register failed:', e.message); + }); + } + }, 30000); + } + + function stopDeviceKeepalive() { + if (deviceKeepaliveTimer) { + clearInterval(deviceKeepaliveTimer); + deviceKeepaliveTimer = null; + } + } + // ============================================================ // Page Visibility // ============================================================ function setupPageVisibility() { document.addEventListener('visibilitychange', function() { isPageVisible = !document.hidden; - if (isPageVisible && audioContext) initializeAudioContext(); + if (isPageVisible) { + console.log('TWP: Page visible — checking connections'); + // Resume audio + if (audioContext) initializeAudioContext(); + // Re-request wake lock (auto-released when tab hidden) + requestWakeLock(); + // Check token expiry — refresh if expired or within 2 minutes + if (tokenExpiry && (Date.now() > tokenExpiry - 2 * 60 * 1000)) { + console.log('TWP: Token expired or expiring soon, refreshing'); + refreshToken(); + } + // Check device registration + if (device && deviceConnectionState !== 'connected' && !currentCall) { + console.log('TWP: Device disconnected, re-registering'); + device.register().catch(function(e) { + console.warn('TWP: Visibility re-register failed:', e.message); + }); + } + } else { + console.log('TWP: Page hidden — wake lock auto-released by browser'); + } }); } @@ -352,6 +420,8 @@ var twpTwilioEdge = window.twpConfig.twilioEdge; device.on('tokenWillExpire', function() { refreshToken(); }); await device.register(); + requestWakeLock(); + startDeviceKeepalive(); } catch (error) { showError('Failed to setup device: ' + error.message); } @@ -363,6 +433,7 @@ var twpTwilioEdge = window.twpConfig.twilioEdge; function setupCallHandlers(call) { call.on('accept', function() { stopRingtone(); + requestWakeLock(); $('#phone-status').text('Connected').css('color', 'var(--accent)'); $('#call-btn').hide(); $('#answer-btn').hide(); @@ -1041,6 +1112,8 @@ var twpTwilioEdge = window.twpConfig.twilioEdge; // ============================================================ $(window).on('beforeunload', function() { if (tokenRefreshTimer) clearTimeout(tokenRefreshTimer); + releaseWakeLock(); + stopDeviceKeepalive(); if (device) device.destroy(); }); diff --git a/mobile/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/mobile/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java index 9ea951b..bb35ca1 100644 --- a/mobile/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +++ b/mobile/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -35,11 +35,26 @@ public final class GeneratedPluginRegistrant { } catch (Exception e) { Log.e(TAG, "Error registering plugin flutter_secure_storage, com.it_nomads.fluttersecurestorage.FlutterSecureStoragePlugin", e); } + try { + flutterEngine.getPlugins().add(new dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin package_info_plus, dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin", e); + } try { flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin()); } catch (Exception e) { Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e); } + try { + flutterEngine.getPlugins().add(new com.baseflow.permissionhandler.PermissionHandlerPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin permission_handler_android, com.baseflow.permissionhandler.PermissionHandlerPlugin", e); + } + try { + flutterEngine.getPlugins().add(new dev.fluttercommunity.plus.wakelock.WakelockPlusPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin wakelock_plus, dev.fluttercommunity.plus.wakelock.WakelockPlusPlugin", e); + } try { flutterEngine.getPlugins().add(new io.flutter.plugins.webviewflutter.WebViewFlutterPlugin()); } catch (Exception e) { diff --git a/mobile/lib/screens/phone_screen.dart b/mobile/lib/screens/phone_screen.dart index cfcf4a0..acd3e96 100644 --- a/mobile/lib/screens/phone_screen.dart +++ b/mobile/lib/screens/phone_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; import '../services/push_notification_service.dart'; @@ -52,10 +53,27 @@ class _PhoneScreenState extends State with WidgetsBindingObserver { } _initWebView(); _initPush(); + WakelockPlus.enable(); + debugPrint('TWP: Wake lock enabled'); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + debugPrint('TWP: App resumed — re-enabling wake lock'); + WakelockPlus.enable(); + // Dispatch synthetic visibilitychange to trigger JS-side recovery + _controller.runJavaScript( + 'document.dispatchEvent(new Event("visibilitychange"));' + 'Object.defineProperty(document, "hidden", {value: false, configurable: true});', + ); + } } @override void dispose() { + WakelockPlus.disable(); + debugPrint('TWP: Wake lock disabled'); WidgetsBinding.instance.removeObserver(this); super.dispose(); } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index c09405e..639426e 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -37,26 +37,26 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" dbus: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: @@ -224,30 +224,46 @@ packages: description: flutter source: sdk version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -260,34 +276,50 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d + url: "https://pub.dev" + source: hosted + version: "9.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: transitive description: @@ -425,18 +457,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: @@ -457,10 +489,10 @@ packages: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.10" timezone: dependency: transitive description: @@ -469,14 +501,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -485,6 +525,22 @@ packages: url: "https://pub.dev" source: hosted version: "14.3.0" + wakelock_plus: + dependency: "direct main" + description: + name: wakelock_plus + sha256: "8b12256f616346910c519a35606fb69b1fe0737c06b6a447c6df43888b097f39" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "24b84143787220a403491c2e5de0877fbbb87baf3f0b18a2a988973863db4b03" + url: "https://pub.dev" + source: hosted + version: "1.4.0" web: dependency: transitive description: @@ -550,5 +606,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.6.0 <4.0.0" - flutter: ">=3.27.0" + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index ab1a65a..51960bd 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -1,7 +1,7 @@ name: twp_softphone description: TWP Softphone - WebView client for Twilio WordPress Plugin publish_to: 'none' -version: 2.0.1+7 +version: 2.0.2+8 environment: sdk: ^3.5.0 @@ -16,6 +16,7 @@ dependencies: webview_flutter: ^4.10.0 webview_flutter_android: ^4.3.0 permission_handler: ^11.3.0 + wakelock_plus: ^1.2.8 dev_dependencies: flutter_test: