Skip to content

improve open database #1480

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: feat/multi_account
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 51 additions & 12 deletions lib/app.dart
Original file line number Diff line number Diff line change
@@ -8,10 +8,12 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'
hide Consumer, FutureProvider, Provider;
import 'package:path/path.dart' as p;
import 'package:provider/provider.dart';

import 'account/notification_service.dart';
import 'constants/brightness_theme_data.dart';
import 'constants/constants.dart';
import 'constants/resources.dart';
import 'generated/l10n.dart';
import 'ui/home/bloc/conversation_list_bloc.dart';
@@ -28,6 +30,7 @@ import 'ui/provider/mention_cache_provider.dart';
import 'ui/provider/setting_provider.dart';
import 'ui/provider/slide_category_provider.dart';
import 'utils/extension/extension.dart';
import 'utils/file.dart';
import 'utils/hook.dart';
import 'utils/logger.dart';
import 'utils/platform.dart';
@@ -53,6 +56,38 @@ class App extends HookConsumerWidget {
precacheImage(
const AssetImage(Resources.assetsImagesChatBackgroundPng), context);

final appDatabaseInitError = ref.watch(appDatabaseInitErrorProvider);
if (appDatabaseInitError != null) {
var error = appDatabaseInitError;
if (error is DriftRemoteException) {
error = error.remoteCause;
}
if (error is SqliteException) {
return _App(
home: DatabaseOpenFailedPage(
error: error,
closeDatabaseCallback: () => ref.read(appDatabaseProvider).close(),
deleteDatabaseCallback: () => dropDatabaseFile(
mixinDocumentsDirectory.path,
kDbFileName,
),
openDatabaseCallback: () async {
final db = ref.refresh(appDatabaseProvider);
try {
await db.settingKeyValue.initialize;
ref.read(appDatabaseInitErrorProvider.notifier).state = null;
} catch (e) {
w('reOpenDatabaseCallback error: $e');
ref.read(appDatabaseInitErrorProvider.notifier).state = e;
}
},
),
);
} else {
return _App(home: OpenAppFailedPage(error: error));
}
}

final initialized = useMemoizedFuture(
() => ref.read(multiAuthStateNotifierProvider.notifier).initialized,
null,
@@ -93,20 +128,24 @@ class _LoginApp extends HookConsumerWidget {
}
if (error is SqliteException) {
return _App(
home: DatabaseOpenFailedPage(error: error),
home: DatabaseOpenFailedPage(
error: error,
openDatabaseCallback: () =>
ref.read(databaseProvider.notifier).open(),
closeDatabaseCallback: () =>
ref.read(databaseProvider.notifier).close(),
deleteDatabaseCallback: () async {
final identityNumber = context.account?.identityNumber;
if (identityNumber == null) return;
await dropDatabaseFile(
p.join(mixinDocumentsDirectory.path, identityNumber),
kDbFileName,
);
},
),
);
} else {
return _App(
home: LandingFailedPage(
title: context.l10n.unknowError,
message: error.toString(),
actions: [
ElevatedButton(
onPressed: () {},
child: Text(context.l10n.exit),
)
]),
);
return _App(home: OpenAppFailedPage(error: error));
}
}

14 changes: 8 additions & 6 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -21,7 +21,6 @@ import 'package:window_size/window_size.dart';

import 'app.dart';
import 'bloc/custom_bloc_observer.dart';
import 'db/app/app_database.dart';
import 'ui/home/home.dart';
import 'ui/provider/database_provider.dart';
import 'utils/app_lifecycle.dart';
@@ -104,12 +103,15 @@ Future<void> main(List<String> args) async {
Bloc.observer = CustomBlocObserver();
}

final appDatabase = AppDatabase.connect(fromMainIsolate: true);
await appDatabase.settingKeyValue.initialize;
final container = ProviderContainer();
try {
await container.read(appDatabaseProvider).settingKeyValue.initialize;
} catch (error, stacktrace) {
e('failed to initialize setting key value: $error\n$stacktrace');
container.read(appDatabaseInitErrorProvider.notifier).state = error;
}
runApp(ProviderScope(
overrides: [
appDatabaseProvider.overrideWithValue(appDatabase),
],
parent: container,
child: const OverlaySupport.global(child: App()),
));

105 changes: 79 additions & 26 deletions lib/ui/landing/landing_failed.dart
Original file line number Diff line number Diff line change
@@ -5,25 +5,34 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path/path.dart' as p;

import '../../constants/constants.dart';
import '../../utils/extension/extension.dart';
import '../../utils/file.dart';
import '../../widgets/dialog.dart';
import '../provider/database_provider.dart';
import 'landing.dart';

// https://sqlite.org/rescode.html
const _kSqliteCorrupt = 11;
const _kSqliteLocked = 6;
const _kSqliteNotADb = 26;
const _kSqliteBusy = 5;

typedef DeleteDatabaseCallback = Future<void> Function();
typedef OpenDatabaseCallback = Future<void> Function();
typedef CloseDatabaseCallback = Future<void> Function();

class DatabaseOpenFailedPage extends StatelessWidget {
const DatabaseOpenFailedPage({
required this.error,
required this.openDatabaseCallback,
required this.deleteDatabaseCallback,
required this.closeDatabaseCallback,
super.key,
});

final SqliteException error;
final OpenDatabaseCallback openDatabaseCallback;
final DeleteDatabaseCallback deleteDatabaseCallback;
final CloseDatabaseCallback closeDatabaseCallback;

@override
Widget build(BuildContext context) {
@@ -41,14 +50,31 @@ class DatabaseOpenFailedPage extends StatelessWidget {
final canDeleteDatabase =
const {_kSqliteCorrupt, _kSqliteNotADb}.contains(error.resultCode);

final canRetry = error.resultCode == _kSqliteBusy;

return LandingFailedPage(
title: context.l10n.failedToOpenDatabase,
message: message,
actions: [
if (canDeleteDatabase)
const Padding(
padding: EdgeInsets.only(bottom: 16),
child: _RecreateDatabaseButton(),
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: _RecreateDatabaseButton(
openDatabaseCallback: openDatabaseCallback,
deleteDatabaseCallback: deleteDatabaseCallback,
closeDatabaseCallback: closeDatabaseCallback,
),
),
if (canRetry)
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: _Button(
onTap: () async {
await closeDatabaseCallback();
await openDatabaseCallback();
},
text: context.l10n.retry,
),
),
_Button(
onTap: () {
@@ -62,14 +88,19 @@ class DatabaseOpenFailedPage extends StatelessWidget {
}

class _RecreateDatabaseButton extends HookConsumerWidget {
const _RecreateDatabaseButton();
const _RecreateDatabaseButton({
required this.openDatabaseCallback,
required this.deleteDatabaseCallback,
required this.closeDatabaseCallback,
});

final OpenDatabaseCallback openDatabaseCallback;
final DeleteDatabaseCallback deleteDatabaseCallback;
final CloseDatabaseCallback closeDatabaseCallback;

@override
Widget build(BuildContext context, WidgetRef ref) => TextButton(
onPressed: () async {
final identityNumber = context.account?.identityNumber;
if (identityNumber == null) return;

final result = await showConfirmMixinDialog(
context,
context.l10n.databaseRecreateTips,
@@ -78,23 +109,9 @@ class _RecreateDatabaseButton extends HookConsumerWidget {
if (result != DialogEvent.positive) {
return;
}
await ref.read(databaseProvider.notifier).close();
// Rename the old database file to a new name with timestamp.
final now = DateTime.now();
renameFileWithTime(
p.join(mixinDocumentsDirectory.path, identityNumber,
'$kDbFileName.db'),
now);
await Future.forEach(
[
File(p.join(mixinDocumentsDirectory.path, identityNumber,
'$kDbFileName.db-shm')),
File(p.join(mixinDocumentsDirectory.path, identityNumber,
'$kDbFileName.db-wal'))
].where((e) => e.existsSync()),
(element) => element.delete(),
);
await ref.read(databaseProvider.notifier).open();
await closeDatabaseCallback();
await deleteDatabaseCallback();
await openDatabaseCallback();
},
child: Text(
context.l10n.continueText,
@@ -105,6 +122,19 @@ class _RecreateDatabaseButton extends HookConsumerWidget {
);
}

Future<void> dropDatabaseFile(String dbDir, String dbName) async {
// Rename the old database file to a new name with timestamp.
final now = DateTime.now();
renameFileWithTime(p.join(dbDir, '$dbName.db'), now);
await Future.forEach(
[
File(p.join(dbDir, '$dbName.db-shm')),
File(p.join(dbDir, '$dbName.db-wal'))
].where((e) => e.existsSync()),
(element) => renameFileWithTime(element.path, now),
);
}

class _Button extends StatelessWidget {
const _Button({required this.text, required this.onTap});

@@ -169,3 +199,26 @@ class LandingFailedPage extends StatelessWidget {
),
);
}

/// Failed to open app with an unknown error.
class OpenAppFailedPage extends StatelessWidget {
const OpenAppFailedPage({
required this.error,
super.key,
});

final dynamic error;

@override
Widget build(BuildContext context) => LandingFailedPage(
title: context.l10n.unknowError,
message: error.toString(),
actions: [
ElevatedButton(
onPressed: () {
exit(1);
},
child: Text(context.l10n.exit),
)
]);
}
11 changes: 9 additions & 2 deletions lib/ui/provider/database_provider.dart
Original file line number Diff line number Diff line change
@@ -12,8 +12,15 @@ import 'account/multi_auth_provider.dart';
import 'hive_key_value_provider.dart';
import 'slide_category_provider.dart';

final appDatabaseProvider =
Provider<AppDatabase>((ref) => throw UnimplementedError());
final appDatabaseProvider = StateProvider<AppDatabase>(
(ref) {
final db = AppDatabase.connect(fromMainIsolate: true);
ref.onDispose(db.close);
return db;
},
);

final appDatabaseInitErrorProvider = StateProvider<dynamic>((ref) => null);

final databaseProvider =
StateNotifierProvider.autoDispose<DatabaseOpener, AsyncValue<Database>>(
2 changes: 1 addition & 1 deletion lib/utils/db/db_key_value.dart
Original file line number Diff line number Diff line change
@@ -56,7 +56,7 @@ typedef AppKeyValue = _BaseDbKeyValue<AppPropertyGroup>;
class _BaseDbKeyValue<G> extends ChangeNotifier {
_BaseDbKeyValue({required this.group, required KeyValueDao<G> dao})
: _dao = dao {
_loadProperties().whenComplete(_initCompleter.complete);
_initCompleter.complete(_loadProperties());
_subscription = dao.watchTableHasChanged(group).listen((event) {
_loadProperties();
});