feat: Add debug output, run app in guarded zone

This commit is contained in:
Anton Stubenbord
2023-06-14 11:53:37 +02:00
parent 2eca84cb30
commit 8b7bbae00b
9 changed files with 315 additions and 111 deletions

View File

@@ -19,11 +19,15 @@ class DioHttpErrorInterceptor extends Interceptor {
} else if (err.response?.statusCode == 403) {
var data = err.response!.data;
if (data is Map && data.containsKey("detail")) {
handler.reject(DioError(
requestOptions: err.requestOptions,
error: ServerMessageException(data['detail']),
response: err.response,
));
handler.reject(
DioError(
message: data['detail'],
requestOptions: err.requestOptions,
error: ServerMessageException(data['detail']),
response: err.response,
type: DioErrorType.unknown,
),
);
return;
}
} else if (err.error is SocketException) {
@@ -31,6 +35,7 @@ class DioHttpErrorInterceptor extends Interceptor {
if (ex.osError?.errorCode == _OsErrorCodes.serverUnreachable.code) {
return handler.reject(
DioError(
message: "The server could not be reached. Is the device offline?",
error: const PaperlessServerException(ErrorCode.deviceOffline),
requestOptions: err.requestOptions,
type: DioErrorType.connectionTimeout,

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
@@ -22,13 +23,14 @@ class SessionManager extends ValueNotifier<Dio> {
BaseOptions(contentType: Headers.jsonContentType),
);
dio.options
..receiveTimeout = const Duration(seconds: 20)
..receiveTimeout = const Duration(seconds: 30)
..sendTimeout = const Duration(seconds: 60)
..responseType = ResponseType.json;
(dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate =
(client) => client..badCertificateCallback = (cert, host, port) => true;
dio.interceptors.addAll([
...interceptors,
DioHttpErrorInterceptor(),
PrettyDioLogger(
compact: true,
responseBody: false,

View File

@@ -12,6 +12,7 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/exception/server_message_exception.dart';
import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';

View File

@@ -130,10 +130,17 @@ class HomeRoute extends StatelessWidget {
.get(currentLocalUserId)!,
)..initialize(),
),
Provider(create: (context) => DocumentScannerCubit(context.read())),
ProxyProvider4<PaperlessDocumentsApi, PaperlessServerStatsApi, LabelRepository,
DocumentChangedNotifier, InboxCubit>(
update: (context, docApi, statsApi, labelRepo, notifier, previous) =>
Provider(
create: (context) =>
DocumentScannerCubit(context.read())),
ProxyProvider4<
PaperlessDocumentsApi,
PaperlessServerStatsApi,
LabelRepository,
DocumentChangedNotifier,
InboxCubit>(
update: (context, docApi, statsApi, labelRepo, notifier,
previous) =>
InboxCubit(
docApi,
statsApi,
@@ -143,9 +150,7 @@ class HomeRoute extends StatelessWidget {
),
ProxyProvider<SavedViewRepository, SavedViewCubit>(
update: (context, savedViewRepo, previous) =>
SavedViewCubit(
savedViewRepo,
),
SavedViewCubit(savedViewRepo),
),
ProxyProvider<LabelRepository, LabelCubit>(
update: (context, value, previous) => LabelCubit(value),

View File

@@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
@@ -37,7 +38,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
}) async {
assert(credentials.username != null && credentials.password != null);
final localUserId = "${credentials.username}@$serverUrl";
_debugPrintMessage(
"login",
"Trying to login $localUserId...",
);
await _addUser(
localUserId,
serverUrl,
@@ -60,6 +64,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
localUserId: localUserId,
),
);
_debugPrintMessage(
"login",
"User successfully logged in.",
);
}
/// Switches to another account if it exists.
@@ -161,26 +169,57 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
/// Performs a conditional hydration based on the local authentication success.
///
Future<void> restoreSessionState() async {
_debugPrintMessage(
"restoreSessionState",
"Trying to restore previous session...",
);
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final localUserId = globalSettings.currentLoggedInUser;
if (localUserId == null) {
_debugPrintMessage(
"restoreSessionState",
"There is nothing to restore.",
);
// If there is nothing to restore, we can quit here.
return;
}
final localUserAccountBox =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final localUserAccount = localUserAccountBox.get(localUserId)!;
_debugPrintMessage(
"restoreSessionState",
"Checking if biometric authentication is required...",
);
if (localUserAccount.settings.isBiometricAuthenticationEnabled) {
_debugPrintMessage(
"restoreSessionState",
"Biometric authentication required, waiting for user to authenticate...",
);
final localAuthSuccess = await _localAuthService
.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
if (!localAuthSuccess) {
emit(const AuthenticationState.requriresLocalAuthentication());
_debugPrintMessage(
"restoreSessionState",
"User could not be authenticated.",
);
return;
}
_debugPrintMessage(
"restoreSessionState",
"User successfully autheticated.",
);
} else {
_debugPrintMessage(
"restoreSessionState",
"Biometric authentication not configured, skipping.",
);
}
_debugPrintMessage(
"restoreSessionState",
"Trying to retrieve authentication credentials...",
);
final authentication =
await withEncryptedBox<UserCredentials, UserCredentials>(
HiveBoxes.localUserCredentials, (box) {
@@ -188,14 +227,32 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
});
if (authentication == null) {
_debugPrintMessage(
"restoreSessionState",
"Could not retrieve existing authentication credentials.",
);
throw Exception(
"User should be authenticated but no authentication information was found."); //TODO: INTL
"User should be authenticated but no authentication information was found.",
); //TODO: INTL
}
_debugPrintMessage(
"restoreSessionState",
"Authentication credentials successfully retrieved.",
);
_debugPrintMessage(
"restoreSessionState",
"Updating current session state...",
);
_sessionManager.updateSettings(
clientCertificate: authentication.clientCertificate,
authToken: authentication.token,
baseUrl: localUserAccount.serverUrl,
);
_debugPrintMessage(
"restoreSessionState",
"Current session state successfully updated.",
);
final apiVersion = await _getApiVersion(_sessionManager.client);
await _updateRemoteUser(
_sessionManager,
@@ -208,20 +265,41 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
localUserId: localUserId,
),
);
_debugPrintMessage(
"restoreSessionState",
"Session was successfully restored.",
);
}
Future<void> logout() async {
_debugPrintMessage(
"logout",
"Trying to log out current user...",
);
await _resetExternalState();
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
globalSettings.currentLoggedInUser = null;
await globalSettings.save();
emit(const AuthenticationState.unauthenticated());
_debugPrintMessage(
"logout",
"User successfully logged out.",
);
}
Future<void> _resetExternalState() async {
_debugPrintMessage(
"_resetExternalState",
"Resetting session manager and clearing storage...",
);
_sessionManager.resetSettings();
await HydratedBloc.storage.clear();
_debugPrintMessage(
"_resetExternalState",
"Session manager successfully reset and storage cleared.",
);
}
Future<int> _addUser(
@@ -232,6 +310,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
SessionManager sessionManager,
) async {
assert(credentials.username != null && credentials.password != null);
_debugPrintMessage("_addUser", "Adding new user $localUserId...");
sessionManager.updateSettings(
baseUrl: serverUrl,
@@ -240,11 +319,21 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final authApi = _apiFactory.createAuthenticationApi(sessionManager.client);
_debugPrintMessage(
"_addUser",
"Trying to login user ${credentials.username} on $serverUrl...",
);
final token = await authApi.login(
username: credentials.username!,
password: credentials.password!,
);
_debugPrintMessage(
"_addUser",
"Successfully acquired token.",
);
sessionManager.updateSettings(
baseUrl: serverUrl,
clientCertificate: clientCert,
@@ -257,17 +346,41 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
if (userAccountBox.containsKey(localUserId)) {
_debugPrintMessage(
"_addUser",
"An error occurred! The user $localUserId already exists.",
);
throw Exception("User already exists!");
}
final apiVersion = await _getApiVersion(sessionManager.client);
final serverUser = await _apiFactory
.createUserApi(
sessionManager.client,
apiVersion: apiVersion,
)
.findCurrentUser();
_debugPrintMessage(
"_addUser",
"Trying to fetch user object for $localUserId...",
);
late UserModel serverUser;
try {
serverUser = await _apiFactory
.createUserApi(
sessionManager.client,
apiVersion: apiVersion,
)
.findCurrentUser();
} on DioError catch (error, stackTrace) {
_debugPrintMessage(
"_addUser",
"An error occurred: ${error.message}",
stackTrace: stackTrace,
);
rethrow;
}
_debugPrintMessage(
"_addUser",
"User object successfully fetched.",
);
_debugPrintMessage(
"_addUser",
"Persisting local user account...",
);
// Create user account
await userAccountBox.put(
localUserId,
@@ -278,15 +391,29 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
paperlessUser: serverUser,
),
);
_debugPrintMessage(
"_addUser",
"Local user account successfully persisted.",
);
_debugPrintMessage(
"_addUser",
"Persisting user state...",
);
// Create user state
await userStateBox.put(
localUserId,
LocalUserAppState(userId: localUserId),
);
_debugPrintMessage(
"_addUser",
"User state successfully persisted.",
);
// Save credentials in encrypted box
await withEncryptedBox(HiveBoxes.localUserCredentials, (box) async {
_debugPrintMessage(
"_addUser",
"Saving user credentials inside encrypted storage...",
);
await box.put(
localUserId,
UserCredentials(
@@ -294,17 +421,32 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
clientCertificate: clientCert,
),
);
_debugPrintMessage(
"_addUser",
"User credentials successfully saved.",
);
});
final hostsBox = Hive.box<String>(HiveBoxes.hosts);
if (!hostsBox.values.contains(serverUrl)) {
await hostsBox.add(serverUrl);
}
return serverUser.id;
}
Future<int> _getApiVersion(Dio dio) async {
_debugPrintMessage(
"_getApiVersion",
"Trying to fetch API version...",
);
final response = await dio.get("/api/");
return int.parse(response.headers.value('x-api-version') ?? "3");
final apiVersion =
int.parse(response.headers.value('x-api-version') ?? "3");
_debugPrintMessage(
"_getApiVersion",
"API version ($apiVersion) successfully retrieved.",
);
return apiVersion;
}
/// Fetches possibly updated (permissions, name, updated server version and thus new user model, ...) remote user data.
@@ -313,13 +455,37 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
LocalUserAccount localUserAccount,
int apiVersion,
) async {
_debugPrintMessage(
"_updateRemoteUser",
"Updating paperless user object...",
);
final updatedPaperlessUser = await _apiFactory
.createUserApi(
sessionManager.client,
apiVersion: apiVersion,
)
.findCurrentUser();
localUserAccount.paperlessUser = updatedPaperlessUser;
await localUserAccount.save();
_debugPrintMessage(
"_updateRemoteUser",
"Paperless user object successfully updated.",
);
}
void _debugPrintMessage(
String methodName,
String message, {
Object? error,
StackTrace? stackTrace,
}) {
debugPrint("AuthenticationCubit#$methodName: $message");
if (error != null) {
debugPrint(error.toString());
}
if (stackTrace != null) {
debugPrintStack(stackTrace: stackTrace);
}
}
}

View File

@@ -4,8 +4,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/exception/server_message_exception.dart';
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
@@ -147,7 +149,11 @@ class _LoginPageState extends State<LoginPage> {
form[ServerAddressFormField.fkServerAddress],
clientCert,
);
} on Exception catch (error) {
} on PaperlessServerException catch (error) {
showErrorMessage(context, error);
} on ServerMessageException catch (error) {
showLocalizedError(context, error.message);
} catch (error) {
showGenericError(context, error);
}
}

View File

@@ -24,11 +24,13 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
final newFilter = state.filter.copyWith(page: state.filter.page + 1);
try {
final result = await api.findAll(newFilter);
emit(state.copyWithPaged(
hasLoaded: true,
filter: newFilter,
value: [...state.value, result],
));
emit(
state.copyWithPaged(
hasLoaded: true,
filter: newFilter,
value: [...state.value, result],
),
);
} finally {
await onFilterUpdated(newFilter);
emit(state.copyWithPaged(isLoading: false));

View File

@@ -14,6 +14,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl_standalone.dart';
import 'package:local_auth/local_auth.dart';
import 'package:mock_server/mock_server.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/constants.dart';
@@ -22,6 +23,7 @@ import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
import 'package:paperless_mobile/core/exception/server_message_exception.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart';
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
@@ -41,14 +43,11 @@ import 'package:paperless_mobile/features/login/view/login_page.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/features/settings/view/pages/switching_accounts_page.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/theme.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:mock_server/mock_server.dart';
String get defaultPreferredLocaleSubtag {
String preferredLocale = Platform.localeName.split("_").first;
@@ -77,95 +76,112 @@ Future<void> _initHive() async {
}
void main() async {
Paint.enableDithering = true;
if (kDebugMode) {
// URL: http://localhost:3131
// Login: admin:test
await LocalMockApiServer(
// RandomDelayGenerator(
// const Duration(milliseconds: 100),
// const Duration(milliseconds: 800),
// ),
)
.start();
}
await _initHive();
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
final globalSettingsBox = Hive.box<GlobalSettings>(HiveBoxes.globalSettings);
final globalSettings = globalSettingsBox.getValue()!;
runZonedGuarded(() async {
Paint.enableDithering = true;
if (kDebugMode) {
// URL: http://localhost:3131
// Login: admin:test
await LocalMockApiServer(
// RandomDelayGenerator(
// const Duration(milliseconds: 100),
// const Duration(milliseconds: 800),
// ),
)
.start();
}
await _initHive();
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
final globalSettingsBox =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings);
final globalSettings = globalSettingsBox.getValue()!;
await findSystemLocale();
packageInfo = await PackageInfo.fromPlatform();
if (Platform.isAndroid) {
androidInfo = await DeviceInfoPlugin().androidInfo;
}
if (Platform.isIOS) {
iosInfo = await DeviceInfoPlugin().iosInfo;
}
await findSystemLocale();
packageInfo = await PackageInfo.fromPlatform();
if (Platform.isAndroid) {
androidInfo = await DeviceInfoPlugin().androidInfo;
}
if (Platform.isIOS) {
iosInfo = await DeviceInfoPlugin().iosInfo;
}
final connectivity = Connectivity();
final localAuthentication = LocalAuthentication();
final connectivityStatusService = ConnectivityStatusServiceImpl(connectivity);
final localAuthService = LocalAuthenticationService(localAuthentication);
final connectivity = Connectivity();
final localAuthentication = LocalAuthentication();
final connectivityStatusService =
ConnectivityStatusServiceImpl(connectivity);
final localAuthService = LocalAuthenticationService(localAuthentication);
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: await getApplicationDocumentsDirectory(),
);
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: await getApplicationDocumentsDirectory(),
);
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
final languageHeaderInterceptor = LanguageHeaderInterceptor(
globalSettings.preferredLocaleSubtag,
);
// Manages security context, required for self signed client certificates
final sessionManager = SessionManager([
DioHttpErrorInterceptor(),
languageHeaderInterceptor,
]);
final languageHeaderInterceptor = LanguageHeaderInterceptor(
globalSettings.preferredLocaleSubtag,
);
// Manages security context, required for self signed client certificates
final sessionManager = SessionManager([
languageHeaderInterceptor,
]);
// Initialize Blocs/Cubits
final connectivityCubit = ConnectivityCubit(connectivityStatusService);
// Initialize Blocs/Cubits
final connectivityCubit = ConnectivityCubit(connectivityStatusService);
// Load application settings and stored authentication data
await connectivityCubit.initialize();
// Load application settings and stored authentication data
await connectivityCubit.initialize();
final localNotificationService = LocalNotificationService();
await localNotificationService.initialize();
final localNotificationService = LocalNotificationService();
await localNotificationService.initialize();
//Update language header in interceptor on language change.
globalSettingsBox.listenable().addListener(() {
languageHeaderInterceptor.preferredLocaleSubtag =
globalSettings.preferredLocaleSubtag;
});
//Update language header in interceptor on language change.
globalSettingsBox.listenable().addListener(() {
languageHeaderInterceptor.preferredLocaleSubtag =
globalSettings.preferredLocaleSubtag;
});
final apiFactory = PaperlessApiFactoryImpl(sessionManager);
final apiFactory = PaperlessApiFactoryImpl(sessionManager);
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider.value(value: sessionManager),
Provider<LocalAuthenticationService>.value(value: localAuthService),
Provider<Connectivity>.value(value: connectivity),
Provider<ConnectivityStatusService>.value(
value: connectivityStatusService),
Provider<LocalNotificationService>.value(
value: localNotificationService),
Provider.value(value: DocumentChangedNotifier()),
],
child: MultiBlocProvider(
runApp(
MultiProvider(
providers: [
BlocProvider<ConnectivityCubit>.value(value: connectivityCubit),
BlocProvider(
create: (context) => AuthenticationCubit(
localAuthService, apiFactory, sessionManager),
)
ChangeNotifierProvider.value(value: sessionManager),
Provider<LocalAuthenticationService>.value(value: localAuthService),
Provider<Connectivity>.value(value: connectivity),
Provider<ConnectivityStatusService>.value(
value: connectivityStatusService),
Provider<LocalNotificationService>.value(
value: localNotificationService),
Provider.value(value: DocumentChangedNotifier()),
],
child: PaperlessMobileEntrypoint(
paperlessProviderFactory: apiFactory,
child: MultiBlocProvider(
providers: [
BlocProvider<ConnectivityCubit>.value(value: connectivityCubit),
BlocProvider(
create: (context) => AuthenticationCubit(
localAuthService, apiFactory, sessionManager),
),
],
child: PaperlessMobileEntrypoint(
paperlessProviderFactory: apiFactory,
),
),
),
),
);
);
}, (error, stack) {
String message = switch (error) {
PaperlessServerException e => e.details ?? error.toString(),
ServerMessageException e => e.message,
_ => error.toString()
};
debugPrint("An unepxected exception has occured!");
debugPrint(message);
debugPrintStack(stackTrace: stack);
// if (_rootScaffoldKey.currentContext != null) {
// ScaffoldMessenger.maybeOf(_rootScaffoldKey.currentContext!)
// ?..hideCurrentSnackBar()
// ..showSnackBar(SnackBar(content: Text(message)));
// }
});
}
class PaperlessMobileEntrypoint extends StatefulWidget {

View File

@@ -1,4 +1,5 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/modules/authentication_api/authentication_api.dart';