native_bridge.dart 7.1 KB

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