From 873a1a0683cf4c7df43b4094633df41374f41970 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 10 Sep 2023 15:55:34 -0400 Subject: [PATCH] Allow for alternative app dirs (unfinished) --- lib/pages/app.dart | 6 ++-- lib/pages/apps.dart | 7 ++-- lib/providers/apps_provider.dart | 41 +++++++++------------ lib/providers/settings_provider.dart | 54 ++++++++++++++++++++++++++++ pubspec.lock | 8 +++++ pubspec.yaml | 1 + 6 files changed, 85 insertions(+), 32 deletions(-) diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 45812d0..dcd52bd 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -338,9 +338,9 @@ class _AppPageState extends State { try { HapticFeedback.heavyImpact(); var res = await appsProvider.downloadAndInstallLatestApps( - app?.app.id != null ? [app!.app.id] : [], - globalNavigatorKey.currentContext, - settingsProvider); + app?.app.id != null ? [app!.app.id] : [], + globalNavigatorKey.currentContext, + ); if (app?.app.installedVersion != null && !trackOnly) { // ignore: use_build_context_synchronously showError(tr('appsUpdated'), context); diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 32eb8c3..3f4cc30 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -381,8 +381,7 @@ class AppsPageState extends State { : () { appsProvider.downloadAndInstallLatestApps( [listedApps[appIndex].app.id], - globalNavigatorKey.currentContext, - settingsProvider).catchError((e) { + globalNavigatorKey.currentContext).catchError((e) { showError(e, context); return []; }); @@ -699,8 +698,8 @@ class AppsPageState extends State { toInstall.addAll(trackOnlyUpdateIdsAllOrSelected); } appsProvider - .downloadAndInstallLatestApps(toInstall, - globalNavigatorKey.currentContext, settingsProvider) + .downloadAndInstallLatestApps( + toInstall, globalNavigatorKey.currentContext) .catchError((e) { showError(e, context); return []; diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 208b222..63a061e 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -150,6 +150,7 @@ class AppsProvider with ChangeNotifier { late Stream? foregroundStream; late StreamSubscription? foregroundSubscription; late Directory APKDir; + late SettingsProvider settingsProvider = SettingsProvider(); Iterable getAppValues() => apps.values.map((a) => a.deepCopy()); @@ -161,12 +162,12 @@ class AppsProvider with ChangeNotifier { if (isForeground) await loadApps(); }); () async { + await settingsProvider.initializeSettings(); var cacheDirs = await getExternalCacheDirectories(); if (cacheDirs?.isNotEmpty ?? false) { APKDir = cacheDirs!.first; } else { - APKDir = - Directory('${(await getExternalStorageDirectory())!.path}/apks'); + APKDir = Directory('${await settingsProvider.getAppDir()}/apks'); if (!APKDir.existsSync()) { APKDir.createSync(); } @@ -369,8 +370,7 @@ class AppsProvider with ChangeNotifier { .where((element) => element.downloadProgress != null) .isNotEmpty; - Future canInstallSilently( - App app, SettingsProvider settingsProvider) async { + Future canInstallSilently(App app) async { if (app.id == obtainiumId) { return false; } @@ -539,7 +539,6 @@ class AppsProvider with ChangeNotifier { getHost(apkUrl.value) != getHost(app.url) && context != null) { // ignore: use_build_context_synchronously - var settingsProvider = context.read(); if (!(settingsProvider.hideAPKOriginWarning) && // ignore: use_build_context_synchronously await showDialog( @@ -560,8 +559,8 @@ class AppsProvider with ChangeNotifier { // If no BuildContext is provided, apps that require user interaction are ignored // If user input is needed and the App is in the background, a notification is sent to get the user's attention // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result - Future> downloadAndInstallLatestApps(List appIds, - BuildContext? context, SettingsProvider settingsProvider, + Future> downloadAndInstallLatestApps( + List appIds, BuildContext? context, {NotificationsProvider? notificationsProvider}) async { notificationsProvider = notificationsProvider ?? context?.read(); @@ -590,8 +589,7 @@ class AppsProvider with ChangeNotifier { apps[id]!.app.preferredApkIndex = urlInd; await saveApps([apps[id]!.app]); } - if (context != null || - await canInstallSilently(apps[id]!.app, settingsProvider)) { + if (context != null || await canInstallSilently(apps[id]!.app)) { appsToInstall.add(id); } } @@ -628,8 +626,7 @@ class AppsProvider with ChangeNotifier { downloadedDir = downloadedArtifact as DownloadedXApkDir; } var appId = downloadedFile?.appId ?? downloadedDir!.appId; - bool willBeSilent = - await canInstallSilently(apps[appId]!.app, settingsProvider); + bool willBeSilent = await canInstallSilently(apps[appId]!.app); if (!(await settingsProvider.getInstallPermission(enforce: false))) { throw ObtainiumError(tr('cancelled')); } @@ -678,8 +675,8 @@ class AppsProvider with ChangeNotifier { } Future getAppsDir() async { - Directory appsDir = Directory( - '${(await getExternalStorageDirectory())?.path as String}/app_data'); + Directory appsDir = + Directory('${await settingsProvider.getAppDir()}/app_data'); if (!appsDir.existsSync()) { appsDir.createSync(); } @@ -879,8 +876,6 @@ class AppsProvider with ChangeNotifier { .toList(); // After reconciliation, delete externally uninstalled Apps if needed if (removedAppIds.isNotEmpty) { - var settingsProvider = SettingsProvider(); - await settingsProvider.initializeSettings(); if (settingsProvider.removeOnExternalUninstall) { await removeApps(removedAppIds); } @@ -1114,8 +1109,8 @@ class AppsProvider with ChangeNotifier { logs.add('Error accessing Downloads (will use fallback): $e'); } if (!downloadsAccessible) { - exportDir = await getExternalStorageDirectory(); - path = exportDir!.path; + exportDir = Directory(await settingsProvider.getAppDir()); + path = exportDir.path; } File export = File( '${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json'); @@ -1298,14 +1293,12 @@ Future bgUpdateCheck(int taskId, Map? params) async { NotificationsProvider notificationsProvider = NotificationsProvider(); AppsProvider appsProvider = AppsProvider(isBg: true); await appsProvider.loadApps(); - var settingsProvider = SettingsProvider(); - await settingsProvider.initializeSettings(); int maxAttempts = 4; params ??= {}; if (params['toCheck'] == null) { - settingsProvider.lastBGCheckTime = DateTime.now(); + appsProvider.settingsProvider.lastBGCheckTime = DateTime.now(); } List> toCheck = >[ ...(params['toCheck'] @@ -1335,7 +1328,7 @@ Future bgUpdateCheck(int taskId, Map? params) async { var didCompleteChecking = false; CheckingUpdatesNotification? notif; var networkRestricted = false; - if (settingsProvider.bgUpdatesOnWiFiOnly) { + if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) { var netResult = await (Connectivity().checkConnectivity()); networkRestricted = (netResult != ConnectivityResult.wifi) && (netResult != ConnectivityResult.ethernet); @@ -1355,8 +1348,7 @@ Future bgUpdateCheck(int taskId, Map? params) async { App? newApp = await appsProvider.checkUpdate(appId); if (newApp != null) { if (networkRestricted || - !(await appsProvider.canInstallSilently( - app!.app, settingsProvider))) { + !(await appsProvider.canInstallSilently(app!.app))) { toNotify.add(newApp); } else { toInstall.add(MapEntry(appId, 0)); @@ -1442,8 +1434,7 @@ Future bgUpdateCheck(int taskId, Map? params) async { try { logs.add( 'BG install task $taskId: Attempting to update $appId in the background.'); - await appsProvider.downloadAndInstallLatestApps( - [appId], null, settingsProvider, + await appsProvider.downloadAndInstallLatestApps([appId], null, notificationsProvider: notificationsProvider); await Future.delayed(const Duration( seconds: diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index a95e2b3..e96b5a7 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -1,6 +1,7 @@ // Exposes functions used to save/load app settings import 'dart:convert'; +import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -9,8 +10,10 @@ import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/main.dart'; import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/source_provider.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_storage/shared_storage.dart' as saf; String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}'; String obtainiumId = 'dev.imranr.obtainium'; @@ -35,6 +38,7 @@ List updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0] class SettingsProvider with ChangeNotifier { SharedPreferences? prefs; + String? defaultAppDir; bool justStarted = true; String sourceUrl = 'https://github.com/ImranR98/Obtainium'; @@ -42,6 +46,7 @@ class SettingsProvider with ChangeNotifier { // Not done in constructor as we want to be able to await it Future initializeSettings() async { prefs = await SharedPreferences.getInstance(); + defaultAppDir = (await getExternalStorageDirectory())!.path; notifyListeners(); } @@ -357,4 +362,53 @@ class SettingsProvider with ChangeNotifier { prefs?.setBool('highlightTouchTargets', val); notifyListeners(); } + + Future getAppDir() async { + return prefs?.getString('appDir') ?? defaultAppDir!; + } + + pickAppDir({bool useDefault = false}) async { + var existingSAFPerms = (await saf.persistedUriPermissions()) ?? []; + var currentAppDir = await getAppDir(); + if (currentAppDir != defaultAppDir) { + currentAppDir = currentAppDir.replaceFirst( + '/storage/emulated/0/', '/tree/primary%3A'); + } + String? newAppDir; + if (!useDefault) { + var target = (await saf.openDocumentTree()); + if (target != null) { + newAppDir = target.path + .replaceFirst('/tree/primary%3A', '/storage/emulated/0/'); + } + } else { + newAppDir = defaultAppDir; + } + newAppDir ??= defaultAppDir; + if (currentAppDir != newAppDir) { + moveDirectoryContents(Directory(currentAppDir), Directory(newAppDir!)); + prefs?.setString('appDir', newAppDir); + notifyListeners(); + } + for (var e in existingSAFPerms) { + await saf.releasePersistableUriPermission(e.uri); + } + } +} + +void moveDirectoryContents(Directory sourceDir, Directory destinationDir) { + if (!destinationDir.existsSync()) { + destinationDir.createSync(recursive: true); + } + List contents = sourceDir.listSync(); + for (FileSystemEntity entity in contents) { + String newPath = '${destinationDir.path}/${entity.uri.pathSegments.last}'; + if (entity is File) { + entity.renameSync(newPath); + } else if (entity is Directory) { + Directory newDestinationDir = Directory(newPath); + moveDirectoryContents(entity, newDestinationDir); + entity.deleteSync(recursive: true); + } + } } diff --git a/pubspec.lock b/pubspec.lock index 04ca07a..cdacfb5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -686,6 +686,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + shared_storage: + dependency: "direct main" + description: + name: shared_storage + sha256: "7c65a9d64f0f5521256be974cfd74010af12196657cec9f9fb7b03b2f11bcaf6" + url: "https://pub.dev" + source: hosted + version: "0.8.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 4f48012..355bbd4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,6 +65,7 @@ dependencies: flutter_archive: ^5.0.0 hsluv: ^1.1.3 connectivity_plus: ^4.0.2 + shared_storage: ^0.8.0 dev_dependencies: flutter_test: