Enable auto-export on update checks

This commit is contained in:
Imran Remtulla
2023-09-10 22:24:18 -04:00
parent 873a1a0683
commit 6e735b1763
19 changed files with 182 additions and 70 deletions

View File

@ -254,6 +254,8 @@
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnUpdateCheckKeepNum": "Auto-export on update check (keep last N auto-exports)",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Remover App?", "one": "Remover App?",
"other": "Remover Apps?" "other": "Remover Apps?"

View File

@ -111,7 +111,7 @@
"dark": "Tamna", "dark": "Tamna",
"light": "Svijetla", "light": "Svijetla",
"followSystem": "Pratite sistem", "followSystem": "Pratite sistem",
"obtainium": "Obtainium", "obtainium": "Obtainium",
"materialYou": "Material You", "materialYou": "Material You",
"useBlackTheme": "Koristite čisto crnu tamnu temu", "useBlackTheme": "Koristite čisto crnu tamnu temu",
"appSortBy": "Aplikacije sortirane po", "appSortBy": "Aplikacije sortirane po",
@ -251,7 +251,9 @@
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"removeAppQuestion": { "pickExportDir": "Pick Export Directory",
"autoExportOnUpdateCheckKeepNum": "Auto-export on update check (keep last N auto-exports)",
"removeAppQuestion": {
"one": "Želite li ukloniti aplikaciju?", "one": "Želite li ukloniti aplikaciju?",
"other": "Želite li ukloniti aplikacije?" "other": "Želite li ukloniti aplikacije?"
}, },

View File

@ -251,6 +251,8 @@
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnUpdateCheckKeepNum": "Auto-export on update check (keep last N auto-exports)",
"removeAppQuestion": { "removeAppQuestion": {
"one": "App entfernen?", "one": "App entfernen?",
"other": "Apps entfernen?" "other": "Apps entfernen?"

View File

@ -254,6 +254,8 @@
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnUpdateCheckKeepNum": "Auto-export on update check (keep last N auto-exports)",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Remove App?", "one": "Remove App?",
"other": "Remove Apps?" "other": "Remove Apps?"

View File

@ -251,6 +251,8 @@
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnUpdateCheckKeepNum": "Auto-export on update check (keep last N auto-exports)",
"removeAppQuestion": { "removeAppQuestion": {
"one": "¿Eliminar Aplicación?", "one": "¿Eliminar Aplicación?",
"other": "¿Eliminar Aplicaciones?" "other": "¿Eliminar Aplicaciones?"

View File

@ -251,6 +251,8 @@
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnUpdateCheckKeepNum": "Auto-export on update check (keep last N auto-exports)",
"removeAppQuestion": { "removeAppQuestion": {
"one": "برنامه حذف شود؟", "one": "برنامه حذف شود؟",
"other": "برنامه ها حذف شوند؟" "other": "برنامه ها حذف شوند؟"

View File

@ -251,6 +251,8 @@
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnUpdateCheckKeepNum": "Auto-export on update check (keep last N auto-exports)",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Supprimer l'application ?", "one": "Supprimer l'application ?",
"other": "Supprimer les applications ?" "other": "Supprimer les applications ?"

View File

@ -250,6 +250,8 @@
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnUpdateCheckKeepNum": "Auto-export on update check (keep last N auto-exports)",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Eltávolítja az alkalmazást?", "one": "Eltávolítja az alkalmazást?",
"other": "Eltávolítja az alkalmazást?" "other": "Eltávolítja az alkalmazást?"

View File

@ -251,6 +251,8 @@
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnUpdateCheckKeepNum": "Auto-export on update check (keep last N auto-exports)",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Rimuovere l'app?", "one": "Rimuovere l'app?",
"other": "Rimuovere le app?" "other": "Rimuovere le app?"

View File

@ -252,6 +252,8 @@
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnUpdateCheckKeepNum": "Auto-export on update check (keep last N auto-exports)",
"removeAppQuestion": { "removeAppQuestion": {
"one": "アプリを削除しますか?", "one": "アプリを削除しますか?",
"other": "アプリを削除しますか?" "other": "アプリを削除しますか?"

View File

@ -257,6 +257,8 @@
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnUpdateCheckKeepNum": "Auto-export on update check (keep last N auto-exports)",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Usunąć aplikację?", "one": "Usunąć aplikację?",
"few": "Usunąć aplikacje?", "few": "Usunąć aplikacje?",

View File

@ -251,6 +251,8 @@
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnUpdateCheckKeepNum": "Auto-export on update check (keep last N auto-exports)",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Удалить приложение?", "one": "Удалить приложение?",
"other": "Удалить приложения?" "other": "Удалить приложения?"

View File

@ -252,6 +252,8 @@
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnUpdateCheckKeepNum": "Auto-export on update check (keep last N auto-exports)",
"removeAppQuestion": { "removeAppQuestion": {
"one": "是否删除应用?", "one": "是否删除应用?",
"other": "是否删除应用?" "other": "是否删除应用?"

View File

@ -124,10 +124,7 @@ class HTML extends AppSource {
additionalValidators: [ additionalValidators: [
(value) { (value) {
value ??= '1'; value ??= '1';
if (int.tryParse(value) == null) { return intValidator(value);
return tr('invalidInput');
}
return null;
} }
]) ])
] ]

View File

@ -74,6 +74,11 @@ class AppsPageState extends State<AppsPage> {
setState(() { setState(() {
refreshingSince = null; refreshingSince = null;
}); });
if (settingsProvider.autoExportOnUpdateCheckKeepNum > 0) {
appsProvider.exportApps(isAuto: true).then((value) {
appsProvider.trimAutoExports();
});
}
}); });
} }

View File

@ -104,8 +104,12 @@ class _ImportExportPageState extends State<ImportExportPage> {
runObtainiumExport() { runObtainiumExport() {
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
appsProvider.exportApps().then((String path) { appsProvider
showError(tr('exportedTo', args: [path]), context); .exportApps(pickOnly: settingsProvider.exportDir == null)
.then((String? result) {
if (result != null) {
showError(tr('exportedTo', args: [result]), context);
}
}).catchError((e) { }).catchError((e) {
showError(e, context); showError(e, context);
}); });
@ -310,7 +314,10 @@ class _ImportExportPageState extends State<ImportExportPage> {
importInProgress importInProgress
? null ? null
: runObtainiumExport, : runObtainiumExport,
child: Text(tr('obtainiumExport')))), child: Text(tr(
settingsProvider.exportDir != null
? 'obtainiumExport'
: 'pickExportDirKeepLastN')))),
const SizedBox( const SizedBox(
width: 16, width: 16,
), ),
@ -323,6 +330,48 @@ class _ImportExportPageState extends State<ImportExportPage> {
child: Text(tr('obtainiumImport')))) child: Text(tr('obtainiumImport'))))
], ],
), ),
if (settingsProvider.exportDir != null)
Column(
children: [
const SizedBox(height: 16),
GeneratedForm(
items: [
[
GeneratedFormTextField(
'autoExportOnUpdateCheckKeepNum',
label: tr(
'autoExportOnUpdateCheckKeepNum'),
required: false,
defaultValue: settingsProvider
.autoExportOnUpdateCheckKeepNum
.toString(),
textInputType: const TextInputType
.numberWithOptions(),
additionalValidators: [
(value) {
value ??= settingsProvider
.autoExportOnUpdateCheckKeepNum
.toString();
return intValidator(value,
positive: true);
}
])
]
],
onValueChanges: (value, valid, isBuilding) {
if (valid && !isBuilding) {
if (value[
'autoExportOnUpdateCheckKeepNum'] !=
null) {
settingsProvider
.autoExportOnUpdateCheckKeepNum =
int.parse(value[
'autoExportOnUpdateCheckKeepNum']);
}
}
}),
],
),
if (importInProgress) if (importInProgress)
const Column( const Column(
children: [ children: [
@ -399,7 +448,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
fontStyle: FontStyle.italic, fontSize: 12)), fontStyle: FontStyle.italic, fontSize: 12)),
const SizedBox( const SizedBox(
height: 8, height: 8,
) ),
], ],
))) )))
])); ]));

View File

@ -16,6 +16,7 @@ import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:obtainium/app_sources/html.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/components/generated_form_modal.dart'; import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
@ -31,6 +32,7 @@ import 'package:obtainium/providers/source_provider.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:android_intent_plus/android_intent.dart'; import 'package:android_intent_plus/android_intent.dart';
import 'package:flutter_archive/flutter_archive.dart'; import 'package:flutter_archive/flutter_archive.dart';
import 'package:shared_storage/shared_storage.dart' as saf;
final pm = AndroidPackageManager(); final pm = AndroidPackageManager();
@ -167,7 +169,8 @@ class AppsProvider with ChangeNotifier {
if (cacheDirs?.isNotEmpty ?? false) { if (cacheDirs?.isNotEmpty ?? false) {
APKDir = cacheDirs!.first; APKDir = cacheDirs!.first;
} else { } else {
APKDir = Directory('${await settingsProvider.getAppDir()}/apks'); APKDir =
Directory('${(await getExternalStorageDirectory())!.path}/apks');
if (!APKDir.existsSync()) { if (!APKDir.existsSync()) {
APKDir.createSync(); APKDir.createSync();
} }
@ -676,7 +679,7 @@ class AppsProvider with ChangeNotifier {
Future<Directory> getAppsDir() async { Future<Directory> getAppsDir() async {
Directory appsDir = Directory appsDir =
Directory('${await settingsProvider.getAppDir()}/app_data'); Directory('${(await getExternalStorageDirectory())!.path}/app_data');
if (!appsDir.existsSync()) { if (!appsDir.existsSync()) {
appsDir.createSync(); appsDir.createSync();
} }
@ -1091,32 +1094,58 @@ class AppsProvider with ChangeNotifier {
return updateAppIds; return updateAppIds;
} }
Future<String> exportApps() async { Future<String?> exportApps({bool pickOnly = false, isAuto = false}) async {
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) { if (isAuto) {
if (await Permission.storage.isDenied) { logs.add('Started auto-export.');
await Permission.storage.request(); }
var exportDir = settingsProvider.exportDir;
if (exportDir == null || pickOnly) {
await settingsProvider.pickExportDirKeepLastN();
exportDir = settingsProvider.exportDir;
}
if (exportDir == null) {
throw ObtainiumError(tr('unexpectedError'));
}
String? returnPath;
if (!pickOnly) {
var result = await saf.createFile(exportDir,
displayName:
'${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json',
mimeType: 'application/json',
content: jsonEncode(apps.values.map((e) => e.app.toJson()).toList()));
if (result == null) {
throw ObtainiumError(tr('unexpectedError'));
} }
if (await Permission.storage.isDenied) { returnPath =
throw ObtainiumError(tr('storagePermissionDenied')); exportDir.pathSegments.join('/').replaceFirst('tree/primary:', '');
}
return returnPath;
}
Future<void> trimAutoExports() async {
var exportDir = settingsProvider.exportDir;
if (exportDir != null) {
var files = await saf
.listFiles(exportDir, columns: [saf.DocumentFileColumn.id]).toList();
var maxCount = settingsProvider.autoExportOnUpdateCheckKeepNum;
if (files.length > maxCount) {
files.sort((a, b) {
if (a.name == null) {
return -1;
} else if (b.name == null) {
return 1;
} else {
return compareAlphaNumeric(a.name!, b.name!);
}
});
files = files.reversed.toList();
logs.add(
'Deleting auto-exports older than ${files[maxCount - 1].uri.pathSegments.last}.');
files.sublist(maxCount).forEach((f) {
saf.delete(f.uri);
});
} }
} }
Directory? exportDir = Directory('/storage/emulated/0/Download');
String path = 'Downloads'; // TODO: See if hardcoding this can be avoided
var downloadsAccessible = false;
try {
downloadsAccessible = exportDir.existsSync();
} catch (e) {
logs.add('Error accessing Downloads (will use fallback): $e');
}
if (!downloadsAccessible) {
exportDir = Directory(await settingsProvider.getAppDir());
path = exportDir.path;
}
File export = File(
'${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json');
export.writeAsStringSync(
jsonEncode(apps.values.map((e) => e.app.toJson()).toList()));
return path;
} }
Future<int> importApps(String appsJSON) async { Future<int> importApps(String appsJSON) async {
@ -1402,6 +1431,10 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
if (toNotify.isNotEmpty) { if (toNotify.isNotEmpty) {
notificationsProvider.notify(UpdateNotification(toNotify)); notificationsProvider.notify(UpdateNotification(toNotify));
} }
if (appsProvider.settingsProvider.autoExportOnUpdateCheckKeepNum > 0) {
await appsProvider.exportApps(isAuto: true);
await appsProvider.trimAutoExports();
}
} }
// If you're done checking and found some silently installable updates, schedule another task which will run in install mode // If you're done checking and found some silently installable updates, schedule another task which will run in install mode
if (didCompleteChecking && toInstall.isNotEmpty) { if (didCompleteChecking && toInstall.isNotEmpty) {

View File

@ -1,7 +1,6 @@
// Exposes functions used to save/load app settings // Exposes functions used to save/load app settings
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -363,52 +362,41 @@ class SettingsProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<String> getAppDir() async { Uri? get exportDir {
return prefs?.getString('appDir') ?? defaultAppDir!; var uriString = prefs?.getString('exportDir');
if (uriString != null) {
return Uri.parse(uriString);
} else {
return null;
}
} }
pickAppDir({bool useDefault = false}) async { Future<void> pickExportDirKeepLastN({bool remove = false}) async {
var existingSAFPerms = (await saf.persistedUriPermissions()) ?? []; var existingSAFPerms = (await saf.persistedUriPermissions()) ?? [];
var currentAppDir = await getAppDir(); var currentOneWayDataSyncDir = exportDir;
if (currentAppDir != defaultAppDir) { Uri? newOneWayDataSyncDir;
currentAppDir = currentAppDir.replaceFirst( if (!remove) {
'/storage/emulated/0/', '/tree/primary%3A'); newOneWayDataSyncDir = (await saf.openDocumentTree());
} }
String? newAppDir; if (currentOneWayDataSyncDir?.path != newOneWayDataSyncDir?.path) {
if (!useDefault) { if (newOneWayDataSyncDir == null) {
var target = (await saf.openDocumentTree()); prefs?.remove('exportDir');
if (target != null) { } else {
newAppDir = target.path prefs?.setString('exportDir', newOneWayDataSyncDir.toString());
.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(); notifyListeners();
} }
for (var e in existingSAFPerms) { for (var e in existingSAFPerms) {
await saf.releasePersistableUriPermission(e.uri); await saf.releasePersistableUriPermission(e.uri);
} }
} }
}
void moveDirectoryContents(Directory sourceDir, Directory destinationDir) { int get autoExportOnUpdateCheckKeepNum {
if (!destinationDir.existsSync()) { return prefs?.getInt('autoExportOnUpdateCheckKeepNum') ?? 0;
destinationDir.createSync(recursive: true);
} }
List<FileSystemEntity> contents = sourceDir.listSync();
for (FileSystemEntity entity in contents) { set autoExportOnUpdateCheckKeepNum(int val) {
String newPath = '${destinationDir.path}/${entity.uri.pathSegments.last}'; prefs?.setInt('autoExportOnUpdateCheckKeepNum', val);
if (entity is File) { notifyListeners();
entity.renameSync(newPath);
} else if (entity is Directory) {
Directory newDestinationDir = Directory(newPath);
moveDirectoryContents(entity, newDestinationDir);
entity.deleteSync(recursive: true);
}
} }
} }

View File

@ -521,6 +521,20 @@ regExValidator(String? value) {
return null; return null;
} }
intValidator(String? value, {bool positive = false}) {
if (value == null) {
return tr('invalidInput');
}
var num = int.tryParse(value);
if (num == null) {
return tr('invalidInput');
}
if (positive && num <= 0) {
return tr('invalidInput');
}
return null;
}
class SourceProvider { class SourceProvider {
// Add more source classes here so they are available via the service // Add more source classes here so they are available via the service
List<AppSource> get sources => [ List<AppSource> get sources => [