mirror of
https://github.com/ImranR98/Obtainium.git
synced 2026-07-31 17:36:48 -04:00
Critical fixes: - Remove setTrustedCertificatesBytes which replaced the entire TLS trust store - Fix late final async init race causing LateInitializationError crashes - Fix infinite recursion in ObtainiumError.message for MULTI_ERROR code High fixes: - Fix identical() always returning false on badge icon (update count never shown) - Clone form items before mutation to prevent cross-contamination between sources - Log transient errors before removing apps on load failure - Await uninstallApp calls instead of fire-and-forget - Revert optimistic install on background workaround failure - Log SAF errors before silently disabling export directory Medium fixes: - Remove dead GeneratedForm.fromDefinitions constructor and unused fields - Replace Shizuku-specific param with generic installOptions map in Installer interface - Add importable-keys allowlist to prevent arbitrary SharedPreferences writes - Guard against stale Shizuku permission check callbacks with sequence counter - Prevent ReceivePort leak in listenForDownloadCancelFromMain - Add 500MB tarball size guard to prevent OOM on large archives - Simplify ReDoS-vulnerable URL regex in html.dart - Fix TOCTOU race between existsSync and length calls - Log rename failures instead of empty catch blocks - Use safe null-aware defaults in setFormValuesFromMap
264 lines
7.5 KiB
Dart
264 lines
7.5 KiB
Dart
import 'dart:async';
|
|
import 'dart:ui' show Locale;
|
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
import 'package:android_package_installer/android_package_installer.dart';
|
|
import 'package:obtainium/providers/logs_provider.dart';
|
|
import 'package:obtainium/providers/source_provider.dart';
|
|
|
|
class ObtainiumError {
|
|
final String code;
|
|
final String _message;
|
|
final StackTrace? stack;
|
|
final Map<String, dynamic> data;
|
|
final bool unexpected;
|
|
|
|
ObtainiumError(
|
|
this._message, {
|
|
this.code = 'UNKNOWN',
|
|
this.unexpected = false,
|
|
this.stack,
|
|
this.data = const {},
|
|
});
|
|
|
|
ObtainiumError.withCode(
|
|
this.code, {
|
|
String message = '',
|
|
this.unexpected = false,
|
|
this.stack,
|
|
this.data = const {},
|
|
}) : _message = message;
|
|
|
|
String get message =>
|
|
code == 'UNKNOWN' ||
|
|
code == 'UNEXPECTED' ||
|
|
code == 'CHECK_UPDATES_FAILED' ||
|
|
code == 'HTTP_ERROR'
|
|
? _message
|
|
: code == 'MULTI_ERROR'
|
|
? localizeErrorCode(code, data)
|
|
: localizeErrorCode(code, data);
|
|
|
|
@override
|
|
String toString() => message;
|
|
}
|
|
|
|
Never rethrowOrWrapError(Object error, {String? sourceName, StackTrace? stack}) {
|
|
if (error is ObtainiumError) {
|
|
if (error.unexpected) {
|
|
final resolvedStack = error.stack ?? StackTrace.current;
|
|
unawaited(
|
|
LogsProvider().add(
|
|
'Unexpected ObtainiumError: ${error.message}\n$resolvedStack',
|
|
level: LogLevel.error,
|
|
),
|
|
);
|
|
throw ObtainiumError(
|
|
error.message,
|
|
code: 'UNEXPECTED',
|
|
unexpected: true,
|
|
stack: resolvedStack,
|
|
data: error.data,
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
final capturedStack = stack ?? StackTrace.current;
|
|
unawaited(
|
|
LogsProvider().add(
|
|
'Wrapping unexpected error: $error\n$capturedStack',
|
|
level: LogLevel.error,
|
|
),
|
|
);
|
|
throw ObtainiumError(
|
|
sourceName != null ? '$sourceName: $error' : error.toString(),
|
|
code: 'UNEXPECTED',
|
|
unexpected: true,
|
|
stack: capturedStack,
|
|
);
|
|
}
|
|
|
|
class RateLimitError extends ObtainiumError {
|
|
final int remainingMinutes;
|
|
RateLimitError(this.remainingMinutes)
|
|
: super.withCode(
|
|
'RATE_LIMIT',
|
|
data: {'remainingMinutes': remainingMinutes},
|
|
);
|
|
}
|
|
|
|
class InvalidURLError extends ObtainiumError {
|
|
InvalidURLError(String sourceName)
|
|
: super.withCode('INVALID_URL', data: {'sourceName': sourceName});
|
|
}
|
|
|
|
class CredsNeededError extends ObtainiumError {
|
|
CredsNeededError(String sourceName)
|
|
: super.withCode('CREDS_NEEDED', data: {'sourceName': sourceName});
|
|
}
|
|
|
|
class NoReleasesError extends ObtainiumError {
|
|
NoReleasesError({String? note})
|
|
: super.withCode('NO_RELEASES', data: {'note': note ?? ''});
|
|
}
|
|
|
|
class NoAPKError extends ObtainiumError {
|
|
NoAPKError() : super.withCode('NO_APK');
|
|
}
|
|
|
|
class NoVersionError extends ObtainiumError {
|
|
NoVersionError() : super.withCode('NO_VERSION');
|
|
}
|
|
|
|
class UnsupportedURLError extends ObtainiumError {
|
|
UnsupportedURLError() : super.withCode('UNSUPPORTED_URL');
|
|
}
|
|
|
|
class DowngradeError extends ObtainiumError {
|
|
DowngradeError(int currentVersionCode, int newVersionCode)
|
|
: super.withCode(
|
|
'DOWNGRADE',
|
|
data: {
|
|
'currentVersionCode': currentVersionCode,
|
|
'newVersionCode': newVersionCode,
|
|
},
|
|
);
|
|
}
|
|
|
|
class InstallError extends ObtainiumError {
|
|
InstallError(int code)
|
|
: super.withCode(
|
|
'INSTALL_FAILED',
|
|
data: {
|
|
'errorCode': code,
|
|
'message': PackageInstallerStatus.byCode(code).name,
|
|
},
|
|
);
|
|
}
|
|
|
|
class IDChangedError extends ObtainiumError {
|
|
IDChangedError(String newId)
|
|
: super.withCode('ID_CHANGED', data: {'newId': newId});
|
|
}
|
|
|
|
class RepositoryRenamedError extends ObtainiumError {
|
|
final String oldUrl;
|
|
final String newUrl;
|
|
RepositoryRenamedError(this.oldUrl, this.newUrl)
|
|
: super.withCode(
|
|
'REPO_RENAMED',
|
|
data: {'oldUrl': oldUrl, 'newUrl': newUrl},
|
|
);
|
|
}
|
|
|
|
class CheckUpdatesException extends ObtainiumError {
|
|
final List<App> updates;
|
|
final MultiAppMultiError errors;
|
|
CheckUpdatesException(this.updates, this.errors)
|
|
: super.withCode('CHECK_UPDATES_FAILED', unexpected: true);
|
|
@override
|
|
String toString() => errors.toString();
|
|
}
|
|
|
|
class NotImplementedError extends ObtainiumError {
|
|
NotImplementedError() : super.withCode('NOT_IMPLEMENTED');
|
|
}
|
|
|
|
class MultiAppMultiError extends ObtainiumError {
|
|
Map<String, dynamic> rawErrors = {};
|
|
Map<String, List<String>> idsByErrorString = {};
|
|
Map<String, String> appIdNames = {};
|
|
|
|
MultiAppMultiError() : super.withCode('MULTI_ERROR', unexpected: true);
|
|
|
|
void add(String appId, dynamic error, {String? appName}) {
|
|
rawErrors[appId] = error;
|
|
final string = error.toString();
|
|
var tempIds = idsByErrorString.remove(string);
|
|
if (tempIds == null) {
|
|
tempIds = [];
|
|
idsByErrorString[string] = tempIds;
|
|
}
|
|
tempIds.add(appId);
|
|
if (appName != null) {
|
|
appIdNames[appId] = appName;
|
|
}
|
|
}
|
|
|
|
String errorString(String appId, {bool includeIdsWithNames = false}) =>
|
|
'${appIdNames.containsKey(appId) ? '${appIdNames[appId]}${includeIdsWithNames ? ' ($appId)' : ''}' : appId}: ${rawErrors[appId].toString()}';
|
|
|
|
String errorsAppsString(
|
|
String errString,
|
|
List<String> appIds, {
|
|
bool includeIdsWithNames = false,
|
|
}) =>
|
|
'$errString [${list2FriendlyString(appIds.map((id) => appIdNames.containsKey(id) == true ? '${appIdNames[id]}${includeIdsWithNames ? ' ($id)' : ''}' : id).toList())}]';
|
|
|
|
@override
|
|
String toString() => idsByErrorString.entries
|
|
.map((e) => errorsAppsString(e.key, e.value))
|
|
.join('\n\n');
|
|
}
|
|
|
|
String localizeErrorCode(String code, Map<String, dynamic>? data) {
|
|
return switch (code) {
|
|
'NO_RELEASES' =>
|
|
data?['note'] != null && (data!['note'] as String).isNotEmpty
|
|
? '${tr('noReleaseFound')}\n\n${data['note']}'
|
|
: tr('noReleaseFound'),
|
|
'RATE_LIMIT' => plural(
|
|
'tooManyRequestsTryAgainInMinutes',
|
|
data?['remainingMinutes'] ?? 0,
|
|
),
|
|
'INVALID_URL' => tr(
|
|
'invalidURLForSource',
|
|
args: [data?['sourceName'] ?? ''],
|
|
),
|
|
'CREDS_NEEDED' => tr(
|
|
'requiresCredentialsInSettings',
|
|
args: [data?['sourceName'] ?? ''],
|
|
),
|
|
'NO_APK' => tr('noAPKFound'),
|
|
'NO_VERSION' => tr('noVersionFound'),
|
|
'UNSUPPORTED_URL' => tr('urlMatchesNoSource'),
|
|
'DOWNGRADE' =>
|
|
'${tr('cantInstallOlderVersion')} (versionCode ${data?['currentVersionCode'] ?? '?'} → ${data?['newVersionCode'] ?? '?'})',
|
|
'INSTALL_FAILED' => data?['message']?.toString() ?? tr('installFailed'),
|
|
'ID_CHANGED' => '${tr('appIdMismatch')} - ${data?['newId'] ?? ''}',
|
|
'REPO_RENAMED' => tr('repoRenamed'),
|
|
'NOT_IMPLEMENTED' => tr('functionNotImplemented'),
|
|
_ => data?['message']?.toString() ?? tr('unexpectedError'),
|
|
};
|
|
}
|
|
|
|
Locale? _appCurrentLocale;
|
|
|
|
void setAppLocale(Locale? locale) => _appCurrentLocale = locale;
|
|
|
|
bool isEnglish() {
|
|
if (_appCurrentLocale != null) return _appCurrentLocale!.languageCode == 'en';
|
|
return false;
|
|
}
|
|
|
|
String lowerCaseIfEnglish(String str) => isEnglish() ? str.toLowerCase() : str;
|
|
|
|
String list2FriendlyString(List<String> list) {
|
|
final isUsingEnglish = isEnglish();
|
|
return list.length == 2
|
|
? '${list[0]} ${tr('and')} ${list[1]}'
|
|
: list
|
|
.asMap()
|
|
.entries
|
|
.map(
|
|
(e) =>
|
|
e.value +
|
|
(e.key == list.length - 1
|
|
? ''
|
|
: e.key == list.length - 2
|
|
? '${isUsingEnglish ? ',' : ''} ${tr('and')} '
|
|
: ', '),
|
|
)
|
|
.join('');
|
|
}
|