native_bridge.dart 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import 'package:flutter/services.dart';
  2. import '../models/app_entry.dart';
  3. import '../models/media_playback.dart';
  4. import '../models/system_status.dart';
  5. class LauncherPlatformException implements Exception {
  6. LauncherPlatformException(this.message);
  7. final String message;
  8. @override
  9. String toString() => message;
  10. }
  11. /// Thin, testable boundary around Android's package manager, volume and
  12. /// connectivity APIs.
  13. abstract class LauncherPlatform {
  14. Future<List<AppEntry>> listApps();
  15. Future<void> launchApp({
  16. required String packageName,
  17. required String activityName,
  18. });
  19. Future<bool> isDefaultHome();
  20. Future<bool> requestDefaultHome();
  21. Future<SystemStatus> getStatus();
  22. Future<VolumeInfo> getVolume();
  23. Future<void> setVolume(int index);
  24. Future<bool> isMusicPlaying();
  25. /// Sends a standard media key to the current media session.
  26. Future<bool> sendMediaKey(String action);
  27. Future<MediaPlayback> getMediaState();
  28. Future<bool> openNotificationAccessSettings();
  29. Future<void> setKeepScreenOn(bool enabled);
  30. Future<bool> openUrl(String url);
  31. }
  32. class NativeBridge implements LauncherPlatform {
  33. static const _appsChannel = MethodChannel('com.wasp.cardash_launcher/apps');
  34. static const _systemChannel = MethodChannel(
  35. 'com.wasp.cardash_launcher/system',
  36. );
  37. static const _mediaChannel = MethodChannel('com.wasp.cardash_launcher/media');
  38. @override
  39. Future<List<AppEntry>> listApps() async {
  40. try {
  41. final raw = await _appsChannel.invokeMethod<dynamic>('listApps');
  42. if (raw is Map && raw['error'] != null) {
  43. throw LauncherPlatformException(raw['error'].toString());
  44. }
  45. if (raw is! List) return const [];
  46. return raw
  47. .whereType<Map>()
  48. .map(AppEntry.fromMap)
  49. .where(
  50. (app) => app.packageName.isNotEmpty && app.activityName.isNotEmpty,
  51. )
  52. .toList(growable: false);
  53. } on PlatformException catch (error) {
  54. throw LauncherPlatformException(
  55. error.message ?? 'Could not load installed apps.',
  56. );
  57. } on MissingPluginException {
  58. throw LauncherPlatformException('The Android app bridge is unavailable.');
  59. }
  60. }
  61. @override
  62. Future<void> launchApp({
  63. required String packageName,
  64. required String activityName,
  65. }) async {
  66. try {
  67. await _appsChannel.invokeMethod<dynamic>('launchApp', {
  68. 'package': packageName,
  69. 'activity': activityName,
  70. });
  71. } on PlatformException catch (error) {
  72. throw LauncherPlatformException(
  73. error.message ?? 'Could not open the app.',
  74. );
  75. } on MissingPluginException {
  76. throw LauncherPlatformException('The Android app bridge is unavailable.');
  77. }
  78. }
  79. @override
  80. Future<bool> isDefaultHome() async {
  81. try {
  82. return await _appsChannel.invokeMethod<bool>('isDefaultHome') ?? false;
  83. } on PlatformException {
  84. return false;
  85. } on MissingPluginException {
  86. return false;
  87. }
  88. }
  89. @override
  90. Future<bool> requestDefaultHome() async {
  91. try {
  92. return await _appsChannel.invokeMethod<bool>('requestDefaultHome') ??
  93. false;
  94. } on PlatformException {
  95. return false;
  96. } on MissingPluginException {
  97. return false;
  98. }
  99. }
  100. @override
  101. Future<SystemStatus> getStatus() async {
  102. try {
  103. final raw = await _systemChannel.invokeMethod<dynamic>('getStatus');
  104. return raw is Map ? SystemStatus.fromMap(raw) : const SystemStatus();
  105. } on PlatformException {
  106. return const SystemStatus();
  107. } on MissingPluginException {
  108. return const SystemStatus();
  109. }
  110. }
  111. @override
  112. Future<VolumeInfo> getVolume() async {
  113. try {
  114. final raw = await _systemChannel.invokeMethod<dynamic>('getVolume');
  115. return raw is Map ? VolumeInfo.fromMap(raw) : const VolumeInfo();
  116. } on PlatformException {
  117. return const VolumeInfo();
  118. } on MissingPluginException {
  119. return const VolumeInfo();
  120. }
  121. }
  122. @override
  123. Future<bool> isMusicPlaying() async {
  124. try {
  125. return await _systemChannel.invokeMethod<bool>('isMusicPlaying') ?? false;
  126. } on PlatformException {
  127. return false;
  128. } on MissingPluginException {
  129. return false;
  130. }
  131. }
  132. @override
  133. Future<MediaPlayback> getMediaState() async {
  134. try {
  135. final raw = await _mediaChannel.invokeMethod<dynamic>('getMediaState');
  136. return raw is Map ? MediaPlayback.fromMap(raw) : const MediaPlayback();
  137. } on PlatformException {
  138. return const MediaPlayback();
  139. } on MissingPluginException {
  140. return const MediaPlayback();
  141. }
  142. }
  143. @override
  144. Future<bool> openNotificationAccessSettings() async {
  145. try {
  146. return await _mediaChannel.invokeMethod<bool>(
  147. 'openNotificationAccessSettings',
  148. ) ??
  149. false;
  150. } on PlatformException {
  151. return false;
  152. } on MissingPluginException {
  153. return false;
  154. }
  155. }
  156. @override
  157. Future<bool> sendMediaKey(String action) async {
  158. try {
  159. return await _systemChannel.invokeMethod<bool>('mediaKey', {
  160. 'action': action,
  161. }) ??
  162. false;
  163. } on PlatformException {
  164. return false;
  165. } on MissingPluginException {
  166. return false;
  167. }
  168. }
  169. @override
  170. Future<bool> openUrl(String url) async {
  171. try {
  172. return await _appsChannel.invokeMethod<bool>('openUrl', {'url': url}) ??
  173. false;
  174. } on PlatformException {
  175. return false;
  176. } on MissingPluginException {
  177. return false;
  178. }
  179. }
  180. @override
  181. Future<void> setKeepScreenOn(bool enabled) async {
  182. try {
  183. await _systemChannel.invokeMethod<dynamic>('setKeepScreenOn', {
  184. 'enabled': enabled,
  185. });
  186. } on PlatformException {
  187. // Older builds simply keep the window behaviour unchanged.
  188. } on MissingPluginException {
  189. // Nothing to apply outside Android.
  190. }
  191. }
  192. @override
  193. Future<void> setVolume(int index) async {
  194. try {
  195. await _systemChannel.invokeMethod<dynamic>('setVolume', {'index': index});
  196. } on PlatformException {
  197. // Volume can be unavailable on some emulators; the UI keeps its local
  198. // value and will reconcile on the next poll.
  199. } on MissingPluginException {
  200. // Nothing to synchronise when running outside Android.
  201. }
  202. }
  203. }