fix(android): sanitize restored installation state

This commit is contained in:
zarzet
2026-07-23 10:39:31 +07:00
parent 480a96bd0e
commit c9114fef01
12 changed files with 673 additions and 27 deletions
+10
View File
@@ -251,6 +251,16 @@ class AppStateDatabase {
});
}
/// A pending queue is tied to the installation's output grants and worker
/// lifecycle. It must not resume after Android restores data into a newly
/// installed package.
Future<void> clearPendingQueueAfterInstallationRestore() async {
await replacePendingDownloadQueueRows(const []);
final prefs = await _prefs;
await prefs.remove(_legacyQueueKey);
await prefs.setBool(_queueMigrationKey, true);
}
Future<void> applyPendingDownloadQueueChanges({
required List<Map<String, dynamic>> upserts,
required List<String> deletedIds,
+52 -4
View File
@@ -31,6 +31,41 @@ class IosPickedDirectory {
const IosPickedDirectory({required this.path, required this.bookmark});
}
class InstallationState {
final bool markerExisted;
final bool markerCreated;
final bool freshPackageInstall;
const InstallationState({
required this.markerExisted,
required this.markerCreated,
required this.freshPackageInstall,
});
const InstallationState.unsupported()
: markerExisted = true,
markerCreated = true,
freshPackageInstall = false;
factory InstallationState.fromMap(Map<String, dynamic> map) {
return InstallationState(
markerExisted: map['marker_existed'] == true,
markerCreated: map['marker_created'] == true,
freshPackageInstall: map['fresh_package_install'] == true,
);
}
}
bool shouldResetRestoredInstallation({
required bool hasPersistedSettings,
required InstallationState installState,
}) {
return hasPersistedSettings &&
!installState.markerExisted &&
installState.markerCreated &&
installState.freshPackageInstall;
}
class _BridgeCacheEntry {
final Map<String, dynamic> value;
final DateTime expiresAt;
@@ -137,6 +172,12 @@ class PlatformBridge {
return _decodeRequiredMapResult(result, method);
}
static Future<InstallationState> ensureInstallMarker() async {
if (!Platform.isAndroid) return const InstallationState.unsupported();
final result = await _invokeMap('ensureInstallMarker');
return InstallationState.fromMap(result);
}
static Future<Map<String, dynamic>> _cachedInvoke(
String cacheKey,
Map<String, _BridgeCacheEntry> cache,
@@ -578,16 +619,23 @@ class PlatformBridge {
/// so a bridge problem never raises a false "access lost" alarm.
static Future<bool> isSafTreeAccessible(String treeUri) async {
try {
final result = await _channel.invokeMethod('isSafTreeAccessible', {
'tree_uri': treeUri,
});
return result as bool? ?? true;
return await validateSafTreeAccess(treeUri);
} catch (e) {
_log.w('Failed to check SAF tree accessibility: $e');
return true;
}
}
/// Strict SAF validation for startup and download preflight. Unlike
/// [isSafTreeAccessible], channel failures are surfaced to the caller so a
/// write cannot proceed on an unverified destination.
static Future<bool> validateSafTreeAccess(String treeUri) async {
final result = await _channel.invokeMethod('isSafTreeAccessible', {
'tree_uri': treeUri,
});
return result as bool? ?? false;
}
static Future<bool> safDelete(String uri) async {
final result = await _channel.invokeMethod('safDelete', {'uri': uri});
return result as bool;