import 'dart:async'; import 'package:bluebubbles/app/layouts/settings/pages/scheduling/scheduled_messages_panel.dart'; import 'package:bluebubbles/app/layouts/settings/pages/server/server_management_panel.dart'; import 'package:bluebubbles/app/wrappers/theme_switcher.dart'; import 'package:bluebubbles/helpers/backend/startup_tasks.dart'; import 'package:bluebubbles/helpers/ui/facetime_helpers.dart'; import 'package:bluebubbles/utils/logger/logger.dart'; import 'package:bluebubbles/helpers/helpers.dart'; import 'package:bluebubbles/app/layouts/chat_creator/chat_creator.dart'; import 'package:bluebubbles/app/layouts/conversation_view/pages/conversation_view.dart'; import 'package:bluebubbles/database/models.dart'; import 'package:bluebubbles/services/services.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' hide Intent; import 'package:get/get.dart'; import 'package:path/path.dart'; import 'package:receive_intent/receive_intent.dart'; import 'package:tuple/tuple.dart'; import 'package:universal_io/io.dart'; import 'package:url_launcher/url_launcher.dart'; IntentsService intents = Get.isRegistered() ? Get.find() : Get.put(IntentsService()); class IntentsService extends GetxService { late final StreamSubscription sub; Future init() async { if (kIsWeb || kIsDesktop) return; final intent = await ReceiveIntent.getInitialIntent(); handleIntent(intent); sub = ReceiveIntent.receivedIntentStream.listen((Intent? intent) { handleIntent(intent); }, onError: (err) { Logger.error("Failed to get intent!", error: err); }); } @override void onClose() async { await sub.cancel(); super.onClose(); } void handleIntent(Intent? intent) async { if (intent == null) return; switch (intent.action) { case "android.intent.action.SEND": case "android.intent.action.SEND_MULTIPLE": final id = intent.extra?["android.intent.extra.shortcut.ID"]; final text = intent.extra?["android.intent.extra.TEXT"]; final files = []; if (intent.extra?["android.intent.extra.STREAM"] != null) { final data = intent.extra!["android.intent.extra.STREAM"]; if (data is List) { for (String? s in data) { if (s == null) continue; final path = await mcs.invokeMethod("get-content-uri-path", {"uri": s}); final bytes = await File(path).readAsBytes(); files.add(PlatformFile( path: path, name: basename(path), bytes: bytes, size: bytes.length, )); } } else if (data != null) { final path = await mcs.invokeMethod("get-content-uri-path", {"uri": data}); final bytes = await File(path).readAsBytes(); files.add(PlatformFile( path: path, name: basename(path), bytes: bytes, size: bytes.length, )); } } await openChat(id, text: text, attachments: files); return; default: if (intent.data?.startsWith("imessage://") ?? false) { final uri = Uri.tryParse(intent.data!.replaceFirst("imessage://", "imessage:").replaceFirst("&body=", "?body=")); if (uri != null) { final address = uri.path; final handle = Handle.findOne(addressAndService: Tuple2(address, "iMessage")); ns.pushAndRemoveUntil( Get.context!, ChatCreator( initialSelected: [SelectedContact(displayName: handle?.displayName ?? address, address: address)], initialText: uri.queryParameters['body'], ), (route) => route.isFirst, ); } } else if (intent.extra?["chatGuid"] != null) { final guid = intent.extra!["chatGuid"]!; final bubble = intent.extra!["bubble"] == true; ls.isBubble = bubble; await openChat(guid); } else if (intent.extra?["callUuid"] != null) { await StartupTasks.waitForUI(); if (intent.extra?["answer"] == true) { await answerFaceTime(intent.extra?["callUuid"]!); } else { await showFaceTimeOverlay(intent.extra?["callUuid"], intent.extra?["caller"], null, false); } } } } Future answerFaceTime(String callUuid) async { if (Get.context != null) { showDialog( context: Get.context!, builder: (BuildContext context) { return AlertDialog( backgroundColor: context.theme.colorScheme.properSurface, title: Text( "Generating link for call...", style: context.theme.textTheme.titleLarge, ), content: Container( height: 70, child: Center( child: CircularProgressIndicator( backgroundColor: context.theme.colorScheme.properSurface, valueColor: AlwaysStoppedAnimation(context.theme.colorScheme.primary), ), ), ), ); } ); hideFaceTimeOverlay(callUuid); } String? link; try { final call = await http.answerFaceTime(callUuid); link = call.data?["data"]?["link"]; } catch (_) {} if (Get.context != null) { Navigator.of(Get.context!).pop(); } if (link == null) { return showSnackbar("Failed to answer FaceTime", "Unable to generate FaceTime link!"); } if (!kIsWeb) { await launchUrl(Uri.parse(link), mode: LaunchMode.externalApplication); } else if (kIsWeb) { // TODO: Implement web FaceTime } } Future openChat(String? guid, {String? text, List attachments = const []}) async { Logger.info("Handling open chat intent with guid: $guid", tag: "IntentsService"); if (guid == null) { Logger.debug("Opening new chat creator..", tag: "IntentsService"); await StartupTasks.waitForUI(); ns.pushAndRemoveUntil( Get.context!, ChatCreator( initialAttachments: attachments, initialText: text, ), (route) => route.isFirst, ); } else if (guid == "-1") { Logger.debug("Popping all routes...", tag: "IntentsService"); if (cm.activeChat != null) { Navigator.of(Get.context!).popUntil((route) => route.isFirst); } } else if (guid == "-2") { Logger.debug("Opening server management panel...", tag: "IntentsService"); Navigator.of(Get.context!).push( ThemeSwitcher.buildPageRoute( builder: (BuildContext context) { return ServerManagementPanel(); }, ), ); } else if (guid.contains("scheduled")) { Logger.debug("Opening scheduled messages panel...", tag: "IntentsService"); Navigator.of(Get.context!).push( ThemeSwitcher.buildPageRoute( builder: (BuildContext context) { return ScheduledMessagesPanel(); }, ), ); } else { Logger.debug("Opening existing chat (Attachments: ${attachments.length}; Text: ${text?.shorten(10) ?? 'N/A'})", tag: "IntentsService"); final chat = Chat.findOne(guid: guid); if (chat == null) { Logger.debug("Chat not found with guid: $guid", tag: "IntentsService"); return; } bool chatIsOpen = cm.activeChat?.chat.guid == guid; Logger.debug("Chat is active: $chatIsOpen", tag: "IntentsService"); setPickedAttachments() { if (attachments.isNotEmpty) { cvc(chat).pickedAttachments.value = attachments; } if (text != null && text.isNotEmpty) { cvc(chat).textController.text = text; } } if (!chatIsOpen) { Logger.debug("Navigating to conversation view...", tag: "IntentsService"); await StartupTasks.waitForUI(); await Future.delayed(const Duration(seconds: 1)); await ns.pushAndRemoveUntil( Get.context!, ConversationView( chat: chat, onInit: () => setPickedAttachments(), ), (route) => route.isFirst, ); } else { Logger.debug("Chat is already open, not navigating", tag: "IntentsService"); setPickedAttachments(); } } } }