Compare commits

...

25 Commits

Author SHA1 Message Date
4951f62d4a Merge pull request #890 from ImranR98/dev
- Fix export encoding (#887)
- Fix repeating background install for some apps (#886)
- More version filtering options for F-Droid (#885)
- Trim user URL input
2023-09-13 22:20:49 -04:00
4c0c4b7010 Trim user URL input 2023-09-13 22:19:09 -04:00
96051e614c Merge remote-tracking branch 'origin/main' into dev 2023-09-13 22:17:11 -04:00
a2e494b2ba More version filtering options for F-Droid (#885) 2023-09-13 22:15:10 -04:00
2d5a9bec84 Increment version, update packages 2023-09-13 21:27:18 -04:00
7d9571cfdd Fix repeating background install for some apps (#886) 2023-09-13 21:24:25 -04:00
acc6a780fa Fix export encoding (#887) 2023-09-13 21:01:39 -04:00
0e36d42a06 Merge pull request #863 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-09-13 19:22:57 -04:00
5cfddd807a locale(pl): Update Polish translations 2023-09-11 22:05:32 +02:00
17b5604f2a Merge pull request #882 from ImranR98/dev
Bugfix
2023-09-10 23:29:38 -04:00
fdb6eed6d0 Slight tweak 2023-09-10 23:28:37 -04:00
13de0437b8 Bugfix 2023-09-10 23:23:35 -04:00
a43c45f310 Merge pull request #881 from ImranR98/dev
Pick export dir + auto-export (#283, #600) + UI fix
2023-09-10 23:15:36 -04:00
9c56a4d1fc Bugs 2023-09-10 23:15:16 -04:00
2aea1d2631 Bugfixes 2023-09-10 23:11:06 -04:00
118e05a0fa Bugfix 2023-09-10 22:47:04 -04:00
05f497787e Build script tweak 2023-09-10 22:37:32 -04:00
53cf4d0234 Pick export dir + auto-export (#283, #600) 2023-09-10 22:35:28 -04:00
6e735b1763 Enable auto-export on update checks 2023-09-10 22:24:18 -04:00
873a1a0683 Allow for alternative app dirs (unfinished) 2023-09-10 15:55:34 -04:00
27b1149d1c Fix padding when touch targets not highlighted 2023-09-10 14:16:19 -04:00
c1e64f111e Merge pull request #878 from ImranR98/dev
Fix Codeberg not finding APKs (#877), Make touch highlight visible in dark theme (#860)
2023-09-10 14:03:48 -04:00
b2af8448fd Increment version 2023-09-10 14:02:26 -04:00
8f44338e76 Make touch highlight visible in dark theme (#860) 2023-09-10 13:56:39 -04:00
e4a55abcb3 Fix Codeberg not finding APKs (#877) 2023-09-10 13:49:04 -04:00
28 changed files with 401 additions and 137 deletions

View File

@ -254,6 +254,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Remover App?",
"other": "Remover Apps?"

View File

@ -111,7 +111,7 @@
"dark": "Tamna",
"light": "Svijetla",
"followSystem": "Pratite sistem",
"obtainium": "Obtainium",
"obtainium": "Obtainium",
"materialYou": "Material You",
"useBlackTheme": "Koristite čisto crnu tamnu temu",
"appSortBy": "Aplikacije sortirane po",
@ -251,7 +251,11 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"removeAppQuestion": {
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Želite li ukloniti aplikaciju?",
"other": "Želite li ukloniti aplikacije?"
},

View File

@ -251,6 +251,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "App entfernen?",
"other": "Apps entfernen?"

View File

@ -254,6 +254,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Remove App?",
"other": "Remove Apps?"

View File

@ -251,6 +251,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "¿Eliminar Aplicación?",
"other": "¿Eliminar Aplicaciones?"

View File

@ -251,6 +251,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "برنامه حذف شود؟",
"other": "برنامه ها حذف شوند؟"

View File

@ -251,6 +251,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Supprimer l'application ?",
"other": "Supprimer les applications ?"

View File

@ -250,6 +250,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Eltávolítja az alkalmazást?",
"other": "Eltávolítja az alkalmazást?"

View File

@ -251,6 +251,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Rimuovere l'app?",
"other": "Rimuovere le app?"

View File

@ -252,6 +252,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "アプリを削除しますか?",
"other": "アプリを削除しますか?"

View File

@ -253,10 +253,14 @@
"verifyLatestTag": "Zweryfikuj najnowszy tag",
"exemptFromBackgroundUpdates": "Wyklucz z uaktualnień w tle (jeśli są włączone)",
"bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"autoSelectHighestVersionCode": "Automatycznie wybierz najwyższy kod wersji APK",
"versionExtractionRegEx": "Wyrażenie regularne wyodrębniające wersję",
"matchGroupToUse": "Dopasuj grupę do użycia",
"highlightTouchTargets": "Wyróżnij mniej oczywiste elementy dotykowe",
"pickExportDir": "Wybierz katalog eksportu",
"autoExportOnChanges": "Automatyczny eksport po wprowadzeniu zmian",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Usunąć aplikację?",
"few": "Usunąć aplikacje?",

View File

@ -251,6 +251,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Удалить приложение?",
"other": "Удалить приложения?"

View File

@ -252,6 +252,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "是否删除应用?",
"other": "是否删除应用?"

View File

@ -4,7 +4,9 @@
CURR_DIR="$(pwd)"
trap "cd "$CURR_DIR"" EXIT
git fetch && git merge origin/main && git push # Typically run after a PR to main, so bring dev up to date
if [ -z "$1" ]; then
git fetch && git merge origin/main && git push # Typically run after a PR to main, so bring dev up to date
fi
rm ./build/app/outputs/flutter-apk/* 2>/dev/null # Get rid of older builds if any
flutter build apk && flutter build apk --split-per-abi # Build (both split and combined APKs)
for file in ./build/app/outputs/flutter-apk/*.sha1; do gpg --sign --detach-sig "$file"; done # Generate PGP signatures

View File

@ -13,10 +13,24 @@ class FDroid extends AppSource {
name = tr('fdroid');
canSearch = true;
additionalSourceAppSpecificSettingFormItems = [
[
GeneratedFormTextField('filterVersionsByRegEx',
label: tr('filterVersionsByRegEx'),
required: false,
additionalValidators: [
(value) {
return regExValidator(value);
}
])
],
[
GeneratedFormSwitch('trySelectingSuggestedVersionCode',
label: tr('trySelectingSuggestedVersionCode'), defaultValue: true)
],
[
GeneratedFormSwitch('autoSelectHighestVersionCode',
label: tr('autoSelectHighestVersionCode'))
]
],
];
}
@ -45,25 +59,73 @@ class FDroid extends AppSource {
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
Response res, String apkUrlPrefix, String standardUrl,
{bool autoSelectHighestVersionCode = false}) {
{bool autoSelectHighestVersionCode = false,
bool trySelectingSuggestedVersionCode = false,
String? filterVersionsByRegEx}) {
if (res.statusCode == 200) {
List<dynamic> releases = jsonDecode(res.body)['packages'] ?? [];
var response = jsonDecode(res.body);
List<dynamic> releases = response['packages'] ?? [];
if (releases.isEmpty) {
throw NoReleasesError();
}
String? latestVersion = releases[0]['versionName'];
if (latestVersion == null) {
String? version;
Iterable<dynamic> releaseChoices = [];
// Grab the versionCode suggested if the user chose to do that
// Only do so at this stage if the user has no release filter
if (trySelectingSuggestedVersionCode &&
response['suggestedVersionCode'] != null &&
filterVersionsByRegEx == null) {
var suggestedReleases = releases.where((element) =>
element['versionCode'] == response['suggestedVersionCode']);
if (suggestedReleases.isNotEmpty) {
releaseChoices = suggestedReleases;
version = suggestedReleases.first['versionName'];
}
}
// Apply the release filter if any
if (filterVersionsByRegEx != null) {
version = null;
releaseChoices = [];
for (var i = 0; i < releases.length; i++) {
if (RegExp(filterVersionsByRegEx)
.hasMatch(releases[i]['versionName'])) {
version = releases[i]['versionName'];
}
}
if (version == null) {
throw NoVersionError();
}
}
// Default to the highest version
version ??= releases[0]['versionName'];
if (version == null) {
throw NoVersionError();
}
Iterable<dynamic> latestReleases =
releases.where((element) => element['versionName'] == latestVersion);
if (latestReleases.length > 1 && autoSelectHighestVersionCode) {
latestReleases = [latestReleases.first];
// If a suggested release was not already picked, pick all those with the selected version
if (releaseChoices.isEmpty) {
releaseChoices =
releases.where((element) => element['versionName'] == version);
}
List<String> apkUrls = latestReleases
// For the remaining releases, use the toggles to auto-select one if possible
if (releaseChoices.length > 1) {
if (autoSelectHighestVersionCode) {
releaseChoices = [releaseChoices.first];
} else if (trySelectingSuggestedVersionCode &&
response['suggestedVersionCode'] != null) {
var suggestedReleases = releaseChoices.where((element) =>
element['versionCode'] == response['suggestedVersionCode']);
if (suggestedReleases.isNotEmpty) {
releaseChoices = suggestedReleases;
}
}
}
if (releaseChoices.isEmpty) {
throw NoReleasesError();
}
List<String> apkUrls = releaseChoices
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
.toList();
return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
return APKDetails(version, getApkUrlsFromUrls(apkUrls),
AppNames(name, Uri.parse(standardUrl).pathSegments.last));
} else {
throw getObtainiumHttpError(res);
@ -82,7 +144,15 @@ class FDroid extends AppSource {
'https://$host/repo/$appId',
standardUrl,
autoSelectHighestVersionCode:
additionalSettings['autoSelectHighestVersionCode'] == true);
additionalSettings['autoSelectHighestVersionCode'] == true,
trySelectingSuggestedVersionCode:
additionalSettings['trySelectingSuggestedVersionCode'] == true,
filterVersionsByRegEx:
(additionalSettings['filterVersionsByRegEx'] as String?)
?.isNotEmpty ==
true
? additionalSettings['filterVersionsByRegEx']
: null);
}
@override

View File

@ -252,8 +252,10 @@ class GitHub extends AppSource {
List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
(release['assets'] as List<dynamic>?)
?.map((e) {
return e['name'] != null && e['url'] != null
? MapEntry(e['name'] as String, e['url'] as String)
return (e['name'] != null) &&
((e['url'] ?? e['browser_download_url']) != null)
? MapEntry(e['name'] as String,
(e['url'] ?? e['browser_download_url']) as String)
: const MapEntry('', '');
})
.where((element) => element.key.toLowerCase().endsWith('.apk'))

View File

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

View File

@ -40,6 +40,9 @@ class IzzyOnDroid extends AppSource {
'https://android.izzysoft.de/frepo/$appId',
standardUrl,
autoSelectHighestVersionCode:
additionalSettings['autoSelectHighestVersionCode'] == true);
additionalSettings['autoSelectHighestVersionCode'] == true,
trySelectingSuggestedVersionCode:
additionalSettings['trySelectingSuggestedVersionCode'] == true,
filterVersionsByRegEx: additionalSettings['filterVersionsByRegEx']);
}
}

View File

@ -19,7 +19,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports
import 'package:easy_localization/src/localization.dart';
const String currentVersion = '0.14.11';
const String currentVersion = '0.14.14';
const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES

View File

@ -148,7 +148,7 @@ class _AddAppPageState extends State<AddAppPage> {
userPickedTrackOnly))) {
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
app = await sourceProvider.getApp(
pickedSource!, userInput, additionalSettings,
pickedSource!, userInput.trim(), additionalSettings,
trackOnlyOverride: trackOnly,
overrideSource: pickedSourceOverride,
inferAppIdIfOptional: inferAppIdIfOptional);

View File

@ -338,9 +338,9 @@ class _AppPageState extends State<AppPage> {
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);

View File

@ -381,8 +381,7 @@ class AppsPageState extends State<AppsPage> {
: () {
appsProvider.downloadAndInstallLatestApps(
[listedApps[appIndex].app.id],
globalNavigatorKey.currentContext,
settingsProvider).catchError((e) {
globalNavigatorKey.currentContext).catchError((e) {
showError(e, context);
return <String>[];
});
@ -452,12 +451,16 @@ class AppsPageState extends State<AppsPage> {
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).primaryColor.withAlpha(
(settingsProvider.highlightTouchTargets &&
showChangesFn != null)
? 20
: 0)),
padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
color: settingsProvider.highlightTouchTargets &&
showChangesFn != null
? (Theme.of(context).brightness == Brightness.light
? Theme.of(context).primaryColor
: Theme.of(context).primaryColorLight)
.withAlpha(20)
: null),
padding: settingsProvider.highlightTouchTargets
? const EdgeInsetsDirectional.fromSTEB(12, 0, 12, 0)
: const EdgeInsetsDirectional.fromSTEB(24, 0, 0, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
@ -695,8 +698,8 @@ class AppsPageState extends State<AppsPage> {
toInstall.addAll(trackOnlyUpdateIdsAllOrSelected);
}
appsProvider
.downloadAndInstallLatestApps(toInstall,
globalNavigatorKey.currentContext, settingsProvider)
.downloadAndInstallLatestApps(
toInstall, globalNavigatorKey.currentContext)
.catchError((e) {
showError(e, context);
return <String>[];

View File

@ -28,8 +28,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
@override
Widget build(BuildContext context) {
SourceProvider sourceProvider = SourceProvider();
var appsProvider = context.read<AppsProvider>();
var settingsProvider = context.read<SettingsProvider>();
var appsProvider = context.watch<AppsProvider>();
var settingsProvider = context.watch<SettingsProvider>();
var outlineButtonStyle = ButtonStyle(
shape: MaterialStateProperty.all(
@ -102,10 +102,16 @@ class _ImportExportPageState extends State<ImportExportPage> {
});
}
runObtainiumExport() {
runObtainiumExport() async {
HapticFeedback.selectionClick();
appsProvider.exportApps().then((String path) {
showError(tr('exportedTo', args: [path]), context);
appsProvider
.exportApps(
pickOnly: (await settingsProvider.getExportDir()) == null,
sp: settingsProvider)
.then((String? result) {
if (result != null) {
showError(tr('exportedTo', args: [result]), context);
}
}).catchError((e) {
showError(e, context);
});
@ -301,27 +307,68 @@ class _ImportExportPageState extends State<ImportExportPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Expanded(
child: TextButton(
style: outlineButtonStyle,
onPressed: appsProvider.apps.isEmpty ||
importInProgress
? null
: runObtainiumExport,
child: Text(tr('obtainiumExport')))),
const SizedBox(
width: 16,
),
Expanded(
child: TextButton(
style: outlineButtonStyle,
onPressed: importInProgress
? null
: runObtainiumImport,
child: Text(tr('obtainiumImport'))))
],
FutureBuilder(
future: settingsProvider.getExportDir(),
builder: (context, snapshot) {
return Column(
children: [
Row(
children: [
Expanded(
child: TextButton(
style: outlineButtonStyle,
onPressed: appsProvider.apps.isEmpty ||
importInProgress
? null
: runObtainiumExport,
child: Text(tr(snapshot.data != null
? 'obtainiumExport'
: 'pickExportDir')),
)),
const SizedBox(
width: 16,
),
Expanded(
child: TextButton(
style: outlineButtonStyle,
onPressed: importInProgress
? null
: runObtainiumImport,
child: Text(tr('obtainiumImport'))))
],
),
if (snapshot.data != null)
Column(
children: [
const SizedBox(height: 16),
GeneratedForm(
items: [
[
GeneratedFormSwitch(
'autoExportOnChanges',
label: tr('autoExportOnChanges'),
defaultValue: settingsProvider
.autoExportOnChanges,
)
]
],
onValueChanges:
(value, valid, isBuilding) {
if (valid && !isBuilding) {
if (value['autoExportOnChanges'] !=
null) {
settingsProvider
.autoExportOnChanges = value[
'autoExportOnChanges'] ==
true;
}
}
}),
],
),
],
);
},
),
if (importInProgress)
const Column(
@ -399,7 +446,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
fontStyle: FontStyle.italic, fontSize: 12)),
const SizedBox(
height: 8,
)
),
],
)))
]));

View File

@ -31,6 +31,7 @@ import 'package:obtainium/providers/source_provider.dart';
import 'package:http/http.dart';
import 'package:android_intent_plus/android_intent.dart';
import 'package:flutter_archive/flutter_archive.dart';
import 'package:shared_storage/shared_storage.dart' as saf;
final pm = AndroidPackageManager();
@ -150,6 +151,7 @@ class AppsProvider with ChangeNotifier {
late Stream<FGBGType>? foregroundStream;
late StreamSubscription<FGBGType>? foregroundSubscription;
late Directory APKDir;
late SettingsProvider settingsProvider = SettingsProvider();
Iterable<AppInMemory> getAppValues() => apps.values.map((a) => a.deepCopy());
@ -161,6 +163,7 @@ class AppsProvider with ChangeNotifier {
if (isForeground) await loadApps();
});
() async {
await settingsProvider.initializeSettings();
var cacheDirs = await getExternalCacheDirectories();
if (cacheDirs?.isNotEmpty ?? false) {
APKDir = cacheDirs!.first;
@ -369,8 +372,7 @@ class AppsProvider with ChangeNotifier {
.where((element) => element.downloadProgress != null)
.isNotEmpty;
Future<bool> canInstallSilently(
App app, SettingsProvider settingsProvider) async {
Future<bool> canInstallSilently(App app) async {
if (app.id == obtainiumId) {
return false;
}
@ -428,7 +430,8 @@ class AppsProvider with ChangeNotifier {
zipFile: File(filePath), destinationDir: Directory(destinationPath));
}
Future<void> installXApkDir(DownloadedXApkDir dir) async {
Future<void> installXApkDir(
DownloadedXApkDir dir, BuildContext? context) async {
// We don't know which APKs in an XAPK are supported by the user's device
// So we try installing all of them and assume success if at least one installed
// If 0 APKs installed, throw the first install error encountered
@ -441,7 +444,7 @@ class AppsProvider with ChangeNotifier {
if (file.path.toLowerCase().endsWith('.apk')) {
try {
somethingInstalled = somethingInstalled ||
await installApk(DownloadedApk(dir.appId, file));
await installApk(DownloadedApk(dir.appId, file), context);
} catch (e) {
logs.add(
'Could not install APK from XAPK \'${file.path}\': ${e.toString()}');
@ -461,7 +464,7 @@ class AppsProvider with ChangeNotifier {
}
}
Future<bool> installApk(DownloadedApk file) async {
Future<bool> installApk(DownloadedApk file, BuildContext? context) async {
var newInfo =
await pm.getPackageArchiveInfo(archiveFilePath: file.file.path);
PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id);
@ -470,8 +473,16 @@ class AppsProvider with ChangeNotifier {
!(await canDowngradeApps())) {
throw DowngradeError();
}
int? code =
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
int? code;
if (context == null) {
// In background installs, 'installApk' never returns so don't wait for it
// TODO: Find a fix to make this work synchronously without context
AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
code = 0; // Be optimistic (ver. det. will get most wrong ones anyways)
} else {
code =
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
}
bool installed = false;
if (code != null && code != 0 && code != 3) {
throw InstallError(code);
@ -539,7 +550,6 @@ class AppsProvider with ChangeNotifier {
getHost(apkUrl.value) != getHost(app.url) &&
context != null) {
// ignore: use_build_context_synchronously
var settingsProvider = context.read<SettingsProvider>();
if (!(settingsProvider.hideAPKOriginWarning) &&
// ignore: use_build_context_synchronously
await showDialog(
@ -560,8 +570,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<List<String>> downloadAndInstallLatestApps(List<String> appIds,
BuildContext? context, SettingsProvider settingsProvider,
Future<List<String>> downloadAndInstallLatestApps(
List<String> appIds, BuildContext? context,
{NotificationsProvider? notificationsProvider}) async {
notificationsProvider =
notificationsProvider ?? context?.read<NotificationsProvider>();
@ -590,8 +600,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 +637,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'));
}
@ -641,19 +649,11 @@ class AppsProvider with ChangeNotifier {
notifyListeners();
try {
if (downloadedFile != null) {
if (willBeSilent && context == null) {
// Would await forever - workaround - TODO
installApk(downloadedFile);
} else {
await installApk(downloadedFile);
}
// ignore: use_build_context_synchronously
await installApk(downloadedFile, context);
} else {
if (willBeSilent && context == null) {
// Would await forever - workaround - TODO
installXApkDir(downloadedDir!);
} else {
await installXApkDir(downloadedDir!);
}
// ignore: use_build_context_synchronously
await installXApkDir(downloadedDir!, context);
}
if (willBeSilent && context == null) {
notificationsProvider?.notify(SilentUpdateAttemptNotification(
@ -678,8 +678,8 @@ class AppsProvider with ChangeNotifier {
}
Future<Directory> getAppsDir() async {
Directory appsDir = Directory(
'${(await getExternalStorageDirectory())?.path as String}/app_data');
Directory appsDir =
Directory('${(await getExternalStorageDirectory())!.path}/app_data');
if (!appsDir.existsSync()) {
appsDir.createSync();
}
@ -879,8 +879,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);
}
@ -919,6 +917,7 @@ class AppsProvider with ChangeNotifier {
}
}
notifyListeners();
exportApps(isAuto: true);
}
Future<void> removeApps(List<String> appIds) async {
@ -940,6 +939,7 @@ class AppsProvider with ChangeNotifier {
}
if (appIds.isNotEmpty) {
notifyListeners();
exportApps(isAuto: true);
}
}
@ -1096,32 +1096,52 @@ class AppsProvider with ChangeNotifier {
return updateAppIds;
}
Future<String> exportApps() async {
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) {
if (await Permission.storage.isDenied) {
await Permission.storage.request();
Future<String?> exportApps(
{bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async {
SettingsProvider settingsProvider = sp ?? this.settingsProvider;
var exportDir = await settingsProvider.getExportDir();
if (isAuto) {
if (settingsProvider.autoExportOnChanges != true) {
return null;
}
if (await Permission.storage.isDenied) {
throw ObtainiumError(tr('storagePermissionDenied'));
if (exportDir == null) {
logs.add('Skipping auto-export as dir is not set.');
return null;
}
logs.add('Started auto-export.');
var files = await saf
.listFiles(exportDir, columns: [saf.DocumentFileColumn.id])
.where((f) => f.uri.pathSegments.last.endsWith('-auto.json'))
.toList();
if (files.isNotEmpty) {
for (var f in files) {
saf.delete(f.uri);
}
logs.add('Previous auto-export deleted.');
}
}
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 (exportDir == null || pickOnly) {
await settingsProvider.pickExportDir();
exportDir = await settingsProvider.getExportDir();
}
if (!downloadsAccessible) {
exportDir = await getExternalStorageDirectory();
path = exportDir!.path;
if (exportDir == null) {
return null;
}
File export = File(
'${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json');
export.writeAsStringSync(
jsonEncode(apps.values.map((e) => e.app.toJson()).toList()));
return path;
String? returnPath;
if (!pickOnly) {
var result = await saf.createFile(exportDir,
displayName:
'${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json',
mimeType: 'application/json',
bytes: Uint8List.fromList(utf8.encode(
jsonEncode(apps.values.map((e) => e.app.toJson()).toList()))));
if (result == null) {
throw ObtainiumError(tr('unexpectedError'));
}
returnPath =
exportDir.pathSegments.join('/').replaceFirst('tree/primary:', '/');
}
return returnPath;
}
Future<int> importApps(String appsJSON) async {
@ -1298,14 +1318,12 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? 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<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[
...(params['toCheck']
@ -1335,7 +1353,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? 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 +1373,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? 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 +1459,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? 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:

View File

@ -9,8 +9,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 +37,7 @@ List<int> 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 +45,7 @@ class SettingsProvider with ChangeNotifier {
// Not done in constructor as we want to be able to await it
Future<void> initializeSettings() async {
prefs = await SharedPreferences.getInstance();
defaultAppDir = (await getExternalStorageDirectory())!.path;
notifyListeners();
}
@ -357,4 +361,49 @@ class SettingsProvider with ChangeNotifier {
prefs?.setBool('highlightTouchTargets', val);
notifyListeners();
}
Future<Uri?> getExportDir() async {
var uriString = prefs?.getString('exportDir');
if (uriString != null) {
Uri? uri = Uri.parse(uriString);
if (!(await saf.canRead(uri) ?? false) ||
!(await saf.canWrite(uri) ?? false)) {
uri = null;
prefs?.remove('exportDir');
notifyListeners();
}
return uri;
} else {
return null;
}
}
Future<void> pickExportDir({bool remove = false}) async {
var existingSAFPerms = (await saf.persistedUriPermissions()) ?? [];
var currentOneWayDataSyncDir = await getExportDir();
Uri? newOneWayDataSyncDir;
if (!remove) {
newOneWayDataSyncDir = (await saf.openDocumentTree());
}
if (currentOneWayDataSyncDir?.path != newOneWayDataSyncDir?.path) {
if (newOneWayDataSyncDir == null) {
prefs?.remove('exportDir');
} else {
prefs?.setString('exportDir', newOneWayDataSyncDir.toString());
}
notifyListeners();
}
for (var e in existingSAFPerms) {
await saf.releasePersistableUriPermission(e.uri);
}
}
bool get autoExportOnChanges {
return prefs?.getBool('autoExportOnChanges') ?? false;
}
set autoExportOnChanges(bool val) {
prefs?.setBool('autoExportOnChanges', val);
notifyListeners();
}
}

View File

@ -521,6 +521,20 @@ regExValidator(String? value) {
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 {
// Add more source classes here so they are available via the service
List<AppSource> get sources => [

View File

@ -46,10 +46,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e"
sha256: e0902a06f0e00414e4e3438a084580161279f137aeb862274710f29ec10cf01e
url: "https://pub.dev"
source: hosted
version: "3.3.8"
version: "3.3.9"
args:
dependency: transitive
description:
@ -320,10 +320,10 @@ packages:
dependency: "direct main"
description:
name: flutter_markdown
sha256: d4a1cb250c4e059586af0235f32e02882860a508e189b61f2b31b8810c1e1330
sha256: a10979814c5f4ddbe2b6143fba25d927599e21e3ba65b3862995960606fae78f
url: "https://pub.dev"
source: hosted
version: "0.6.17+2"
version: "0.6.17+3"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -538,18 +538,18 @@ packages:
dependency: "direct main"
description:
name: permission_handler
sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
sha256: ad65ba9af42a3d067203641de3fd9f547ded1410bad3b84400c2b4899faede70
url: "https://pub.dev"
source: hosted
version: "10.4.5"
version: "11.0.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
sha256: f23cfe9af0d49c6b9fd8a8b09f7b3301ca7e346204939b5afef4404d36d2608f
url: "https://pub.dev"
source: hosted
version: "10.3.6"
version: "11.0.1"
permission_handler_apple:
dependency: transitive
description:
@ -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

View File

@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.14.11+203 # When changing this, update the tag in main() accordingly
version: 0.14.14+206 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=3.0.0 <4.0.0'
@ -46,7 +46,7 @@ dependencies:
html: ^0.15.0
shared_preferences: ^2.0.15
url_launcher: ^6.1.5
permission_handler: ^10.0.0
permission_handler: ^11.0.0
fluttertoast: ^8.0.9
device_info_plus: ^9.0.0
file_picker: ^5.2.10
@ -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: