Skip to content
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

improve open database #1480

Draft
wants to merge 1 commit into
base: feat/multi_account
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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));
}
}

Expand Down
14 changes: 8 additions & 6 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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()),
));

Expand Down
105 changes: 79 additions & 26 deletions lib/ui/landing/landing_failed.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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: () {
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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});

Expand Down Expand Up @@ -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
Expand Up @@ -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>>(
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/db/db_key_value.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down