mirror of
https://github.com/BlueBubblesApp/bluebubbles-app.git
synced 2025-08-06 19:44:08 +08:00
213 lines
7.0 KiB
Dart
213 lines
7.0 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
|
import 'package:bluebubbles/helpers/helpers.dart';
|
|
import 'package:bluebubbles/database/database.dart';
|
|
import 'package:bluebubbles/services/services.dart';
|
|
import 'package:bluebubbles/utils/logger/logger.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:in_app_review/in_app_review.dart';
|
|
import 'package:on_exit/init.dart';
|
|
import 'package:app_install_date/app_install_date.dart';
|
|
import 'package:path/path.dart';
|
|
import 'package:tuple/tuple.dart';
|
|
import 'package:window_manager/window_manager.dart';
|
|
|
|
class StartupTasks {
|
|
|
|
static final Completer<void> uiReady = Completer<void>();
|
|
|
|
static Future<void> waitForUI() async {
|
|
await uiReady.future;
|
|
}
|
|
|
|
static Future<void> initStartupServices({bool isBubble = false}) async {
|
|
debugPrint("Initializing startup services...");
|
|
|
|
// First, initialize the filesystem service as it's used by other necessary services
|
|
await fs.init();
|
|
|
|
// Initialize the logger so we can start logging things immediately
|
|
await Logger.init();
|
|
Logger.debug("Initializing startup services...");
|
|
|
|
// Check if another instance is running (Linux Only).
|
|
// Automatically handled on Windows (I think)
|
|
await StartupTasks.checkInstanceLock();
|
|
|
|
// Setup the settings service
|
|
await ss.init();
|
|
|
|
// The next thing we need to do is initialize the database.
|
|
// If the database is not initialized, we cannot do anything.
|
|
await Database.init();
|
|
|
|
// Load FCM data into settings from the database
|
|
// We only need to do this for the main startup
|
|
ss.getFcmData();
|
|
|
|
// We then have to initialize all the services that the app will use.
|
|
// Order matters here as some services may rely on others. For instance,
|
|
// The MethodChannel service needs the database to be initialized to handle events.
|
|
// The Lifecycle service needs the MethodChannel service to be initialized to send events.
|
|
await mcs.init();
|
|
await ls.init(isBubble: isBubble);
|
|
await ts.init();
|
|
|
|
if (!kIsWeb) {
|
|
await cs.init();
|
|
GlobalChatService;
|
|
}
|
|
|
|
await notif.init();
|
|
await intents.init();
|
|
}
|
|
|
|
static Future<void> initIsolateServices() async {
|
|
debugPrint("Initializing isolate services...");
|
|
await fs.init(headless: true);
|
|
await Logger.init();
|
|
Logger.debug("Initializing isolate services...");
|
|
await ss.init(headless: true);
|
|
await Database.init();
|
|
await mcs.init(headless: true);
|
|
await ls.init(headless: true);
|
|
}
|
|
|
|
static Future<void> initIncrementalSyncServices() async {
|
|
debugPrint("Initializing incremental sync services...");
|
|
await fs.init();
|
|
await Logger.init();
|
|
Logger.debug("Initializing incremental sync services...");
|
|
await ss.init();
|
|
await Database.init();
|
|
}
|
|
|
|
static Future<void> onStartup() async {
|
|
if (!ss.settings.finishedSetup.value) return;
|
|
|
|
if (!kIsDesktop) {
|
|
chats.init();
|
|
socket;
|
|
}
|
|
|
|
// Fetch server details for the rest of the app.
|
|
// We only need to fetch it on startup since the metadata shouldn't change.
|
|
await ss.getServerDetails(refresh: true);
|
|
|
|
// Only register FCM device on startup
|
|
await fcm.registerDevice();
|
|
|
|
// We don't need to check for updates immediately, so delay it so other
|
|
// code has a chance to run and we don't block the UI thread.
|
|
Future.delayed(const Duration(seconds: 30), () {
|
|
try {
|
|
ss.checkServerUpdate();
|
|
} catch (ex, stack) {
|
|
Logger.warn("Failed to check for server update!", error: ex, trace: stack);
|
|
}
|
|
|
|
try {
|
|
ss.checkClientUpdate();
|
|
} catch (ex, stack) {
|
|
Logger.warn("Failed to check for client update!", error: ex, trace: stack);
|
|
}
|
|
});
|
|
|
|
// Check if we need to request a review
|
|
if (Platform.isAndroid) {
|
|
Future.delayed(const Duration(minutes: 1), () async {
|
|
await reviewFlow();
|
|
});
|
|
}
|
|
}
|
|
|
|
static Future<void> checkInstanceLock() async {
|
|
if (!kIsDesktop || !Platform.isLinux) return;
|
|
Logger.debug("Starting process with PID $pid");
|
|
|
|
final lockFile = File(join(fs.appDocDir.path, 'bluebubbles.lck'));
|
|
final instanceFile = File(join(fs.appDocDir.path, '.instance'));
|
|
onExit(() {
|
|
if (lockFile.existsSync()) lockFile.deleteSync();
|
|
});
|
|
|
|
if (!lockFile.existsSync()) {
|
|
lockFile.createSync();
|
|
}
|
|
if (!instanceFile.existsSync()) {
|
|
instanceFile.createSync();
|
|
}
|
|
|
|
Logger.debug("Lockfile at ${lockFile.path}");
|
|
String _pid = lockFile.readAsStringSync();
|
|
String ps = Process.runSync('ps', ['-p', _pid]).stdout;
|
|
if (kReleaseMode && "$pid" != _pid && ps.endsWith('bluebubbles\n')) {
|
|
Logger.debug("Another instance is running. Sending foreground signal");
|
|
instanceFile.openSync(mode: FileMode.write).closeSync();
|
|
exit(0);
|
|
}
|
|
|
|
lockFile.writeAsStringSync("$pid");
|
|
instanceFile.watch(events: FileSystemEvent.modify).listen((event) async {
|
|
Logger.debug("Got Signal to go to foreground");
|
|
doWhenWindowReady(() async {
|
|
await windowManager.show();
|
|
List<Tuple2<String, String>?> widAndNames = await (await Process.start('wmctrl', ['-pl']))
|
|
.stdout
|
|
.transform(utf8.decoder)
|
|
.transform(const LineSplitter())
|
|
.map((line) => line.replaceAll(RegExp(r"\s+"), " ").split(" "))
|
|
.map((split) => split[2] == "$pid" ? Tuple2(split.first, split.last) : null)
|
|
.where((tuple) => tuple != null)
|
|
.toList();
|
|
|
|
for (Tuple2<String, String>? window in widAndNames) {
|
|
if (window?.item2 == "BlueBubbles") {
|
|
Process.runSync('wmctrl', ['-iR', window!.item1]);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> reviewFlow() async {
|
|
if (!ls.isAlive) return;
|
|
Logger.info('Checking if we should request a review');
|
|
|
|
try {
|
|
DateTime sinceDate = await AppInstallDate().installDate;
|
|
int lastReviewRequest = ss.settings.lastReviewRequestTimestamp.value;
|
|
if (lastReviewRequest > 0) {
|
|
sinceDate = DateTime.fromMillisecondsSinceEpoch(lastReviewRequest);
|
|
}
|
|
|
|
final DateTime now = DateTime.now();
|
|
final int days = now.difference(sinceDate).inDays;
|
|
|
|
// If the app has been installed for 7 days, request a review
|
|
// And if the user has not been asked for a review ever.
|
|
// If the user has already been asked, ask again after 30 days
|
|
if ((lastReviewRequest == 0 && days >= 7) || (lastReviewRequest > 0 && days >= 30)) {
|
|
ss.settings.lastReviewRequestTimestamp.value = now.millisecondsSinceEpoch;
|
|
await ss.settings.saveOne("lastReviewRequestTimestamp");
|
|
await requestReview();
|
|
} else {
|
|
Logger.info('Not requesting review, days since install/last request: $days');
|
|
}
|
|
} catch (e, st) {
|
|
Logger.warn("Failed to request app review", error: e, trace: st);
|
|
}
|
|
}
|
|
|
|
Future<void> requestReview() async {
|
|
Logger.info('Requesting in app review!');
|
|
final InAppReview inAppReview = InAppReview.instance;
|
|
if (await inAppReview.isAvailable()) {
|
|
await inAppReview.requestReview();
|
|
}
|
|
} |