Skip to content

Commit

Permalink
Enhanced Version Detection (Again) (#144)
Browse files Browse the repository at this point in the history
* Simpler approach to EVD

* Download notifs now have progress bars

* Removed unused import, changed some comments

* Re-added "Please Wait" on Apps list (accidentally removed)

* Updated README.md
  • Loading branch information
ImranR98 authored Dec 11, 2022
1 parent da86958 commit f81f637
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 81 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Currently supported App sources:
- [APKMirror](https://apkmirror.com/) (Track-Only)

## Limitations
- App installs are assumed to have succeeded; failures and cancelled installs cannot be detected.
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.
- Auto (unattended) updates are unsupported due to a lack of any capable Flutter plugin.
- For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods may be unavailable.

Expand Down
5 changes: 3 additions & 2 deletions assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"trackOnlyAppDescription": "The App will be tracked for updates, but Obtainium will not be able to download or install it.",
"cancelled": "Cancelled",
"appAlreadyAdded": "App already added",
"alreadyUpToDateQuestion": "App Already up to Date?",
"addApp": "Add App",
"appSourceURL": "App Source URL",
"error": "Error",
Expand All @@ -57,7 +58,7 @@
"noAppsForFilter": "No Apps for Filter",
"byX": "By {}",
"percentProgress": "Progress: {}%",
"pleaseWait": "Please Wait...",
"pleaseWait": "Please Wait",
"updateAvailable": "Update Available",
"estimateInBracketsShort": "(Est.)",
"notInstalled": "Not Installed",
Expand All @@ -73,7 +74,7 @@
"changeX": "Change {}",
"installUpdateApps": "Install/Update Apps",
"installUpdateSelectedApps": "Install/Update Selected Apps",
"onlyAppliesToInstalledAndOutdatedApps": "Only applies to installed but out of date Apps whose install status cannot be automatically detected.",
"onlyWorksWithNonEVDApps": "Only works for Apps whose install status cannot be automatically detected (uncommon).",
"markXSelectedAppsAsUpdated": "Mark {} Selected Apps as Updated?",
"no": "No",
"yes": "Yes",
Expand Down
4 changes: 2 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports
import 'package:easy_localization/src/localization.dart';

const String currentVersion = '0.8.7';
const String currentVersion = '0.8.8';
const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES

Expand Down Expand Up @@ -70,7 +70,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
var notificationsProvider = NotificationsProvider();
await notificationsProvider.notify(checkingUpdatesNotification);
try {
var appsProvider = AppsProvider(forBGTask: true);
var appsProvider = AppsProvider();
await notificationsProvider.cancel(ErrorCheckingUpdatesNotification('').id);
await appsProvider.loadApps();
List<String> existingUpdateIds =
Expand Down
12 changes: 10 additions & 2 deletions lib/pages/app.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:obtainium/components/generated_form_modal.dart';
Expand Down Expand Up @@ -150,8 +151,15 @@ class _AppPageState extends State<AppPage> {
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
title: const Text(
'App Already up to Date?'),
title: Text(tr(
'alreadyUpToDateQuestion')),
content: Text(
tr('onlyWorksWithNonEVDApps'),
style: const TextStyle(
fontWeight:
FontWeight.bold,
fontStyle:
FontStyle.italic)),
actions: [
TextButton(
onPressed: () {
Expand Down
28 changes: 19 additions & 9 deletions lib/pages/apps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -285,14 +285,17 @@ class AppsPageState extends State<AppsPage> {
mode: LaunchMode
.externalApplication);
},
child: Text(
'${tr('updateAvailable')}${sortedApps[index].app.trackOnly ? ' ${tr('estimateInBracketsShort')}' : ''}',
style: TextStyle(
fontStyle: FontStyle.italic,
decoration: changesUrl == null
? TextDecoration.none
: TextDecoration.underline),
))
child: appsProvider.areDownloadsRunning()
? Text(tr('pleaseWait'))
: Text(
'${tr('updateAvailable')}${sortedApps[index].app.trackOnly ? ' ${tr('estimateInBracketsShort')}' : ''}',
style: TextStyle(
fontStyle: FontStyle.italic,
decoration: changesUrl == null
? TextDecoration.none
: TextDecoration
.underline),
))
: const SizedBox(),
],
))),
Expand Down Expand Up @@ -510,7 +513,14 @@ class AppsPageState extends State<AppsPage> {
.toString()
])),
content: Text(
tr('onlyAppliesToInstalledAndOutdatedApps')),
tr('onlyWorksWithNonEVDApps'),
style: const TextStyle(
fontWeight:
FontWeight
.bold,
fontStyle:
FontStyle.italic),
),
actions: [
TextButton(
onPressed:
Expand Down
184 changes: 127 additions & 57 deletions lib/providers/apps_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:install_plugin_v2/install_plugin_v2.dart';
import 'package:installed_apps/app_info.dart';
import 'package:installed_apps/installed_apps.dart';
Expand Down Expand Up @@ -38,42 +37,69 @@ class DownloadedApk {
DownloadedApk(this.appId, this.file);
}

List<String> generateStandardVersionRegExStrings() {
// TODO: Look into RegEx for non-Latin characters / non-Arabic numerals
var basics = [
'[0-9]+',
'[0-9]+\\.[0-9]+',
'[0-9]+\\.[0-9]+\\.[0-9]+',
'[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+'
];
var preSuffixes = ['-', '\\+'];
var suffixes = ['alpha', 'beta', 'ose'];
var finals = ['\\+[0-9]+', '[0-9]+'];
List<String> results = [];
for (var b in basics) {
results.add(b);
for (var p in preSuffixes) {
for (var s in suffixes) {
results.add('$b$s');
results.add('$b$p$s');
for (var f in finals) {
results.add('$b$s$f');
results.add('$b$p$s$f');
}
}
}
}
return results;
}

List<String> standardVersionRegExStrings =
generateStandardVersionRegExStrings();

class AppsProvider with ChangeNotifier {
// In memory App state (should always be kept in sync with local storage versions)
Map<String, AppInMemory> apps = {};
bool loadingApps = false;
bool gettingUpdates = false;
bool forBGTask = false;
LogsProvider logs = LogsProvider();

// Variables to keep track of the app foreground status (installs can't run in the background)
bool isForeground = true;
late Stream<FGBGType>? foregroundStream;
late StreamSubscription<FGBGType>? foregroundSubscription;

AppsProvider({this.forBGTask = false}) {
// Many setup tasks should only be done in the foreground isolate
if (!forBGTask) {
// Subscribe to changes in the app foreground status
foregroundStream = FGBGEvents.stream.asBroadcastStream();
foregroundSubscription = foregroundStream?.listen((event) async {
isForeground = event == FGBGType.foreground;
if (isForeground) await loadApps();
AppsProvider() {
// Subscribe to changes in the app foreground status
foregroundStream = FGBGEvents.stream.asBroadcastStream();
foregroundSubscription = foregroundStream?.listen((event) async {
isForeground = event == FGBGType.foreground;
if (isForeground) await loadApps();
});
() async {
// Load Apps into memory (in background, this is done later instead of in the constructor)
await loadApps();
// Delete existing APKs
(await getExternalStorageDirectory())
?.listSync()
.where((element) =>
element.path.endsWith('.apk') ||
element.path.endsWith('.apk.part'))
.forEach((apk) {
apk.delete();
});
() async {
// Load Apps into memory (in background, this is done later instead of in the constructor)
await loadApps();
// Delete existing APKs
(await getExternalStorageDirectory())
?.listSync()
.where((element) =>
element.path.endsWith('.apk') ||
element.path.endsWith('.apk.part'))
.forEach((apk) {
apk.delete();
});
}();
}
}();
}

downloadFile(String url, String fileName, Function? onProgress,
Expand Down Expand Up @@ -172,8 +198,8 @@ class AppsProvider with ChangeNotifier {

Future<bool> canInstallSilently(App app) async {
return false;
// TODO: Uncomment the below once silentupdates are ever figured out
// // TODO: This is unreliable - try to get from OS in the future
// TODO: Uncomment the below if silent updates are ever figured out
// // NOTE: This is unreliable - try to get from OS in the future
// if (app.apkUrls.length > 1) {
// return false;
// }
Expand Down Expand Up @@ -330,7 +356,8 @@ class AppsProvider with ChangeNotifier {
}
}

// Move everything to the regular install list (since silent updates don't currently work) - TODO
// Move everything to the regular install list (since silent updates don't currently work)
// TODO: Remove this when silent updates work
regularInstalls.addAll(silentUpdates);

// If Obtainium is being installed, it should be the last one
Expand Down Expand Up @@ -401,49 +428,92 @@ class AppsProvider with ChangeNotifier {
}

// If the App says it is installed but installedInfo is null, set it to not installed
// If the App says is is not installed but installedInfo exists, try to set it to installed as latest version...
// ...if the latestVersion seems to match the version in installedInfo (not guaranteed)
// If there is any other mismatch between installedInfo and installedVersion, try reconciling them intelligently
// If that fails, just set it to the actual version string (all we can do at that point)
// Don't save changes, just return the object if changes were made (else null)
// If in a background isolate, return null straight away as the required plugin won't work anyways
App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) {
if (forBGTask) {
return null; // Can't correct in the background isolate
}
var modded = false;
if (installedInfo == null &&
app.installedVersion != null &&
!app.trackOnly) {
app.installedVersion = null;
modded = true;
}
if (installedInfo != null && app.installedVersion == null) {
if (app.latestVersion.characters
.where((p0) => [
// TODO: Won't work for other charsets
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'.'
].contains(p0))
.join('') ==
installedInfo.versionName) {
app.installedVersion = app.latestVersion;
} else {
app.installedVersion = installedInfo.versionName;
} else if (installedInfo?.versionName != null &&
app.installedVersion == null) {
app.installedVersion = installedInfo!.versionName;
modded = true;
} else if (installedInfo?.versionName != null &&
installedInfo!.versionName != app.installedVersion) {
String? correctedInstalledVersion = reconcileRealAndInternalVersions(
installedInfo.versionName!, app.installedVersion!);
if (correctedInstalledVersion != null) {
app.installedVersion = correctedInstalledVersion;
modded = true;
}
}
if (app.installedVersion != null &&
app.installedVersion != app.latestVersion) {
app.installedVersion = reconcileRealAndInternalVersions(
app.installedVersion!, app.latestVersion,
matchMode: true) ??
app.installedVersion;
modded = true;
}
return modded ? app : null;
}

String? reconcileRealAndInternalVersions(
String realVersion, String internalVersion,
{bool matchMode = false}) {
// 1. If one or both of these can't be converted to a "standard" format, return null (leave as is)
// 2. If both have a "standard" format under which they are equal, return null (leave as is)
// 3. If both have a "standard" format in common but are unequal, return realVersion (this means it was changed externally)
// If in matchMode, the outcomes of rules 2 and 3 are reversed, and the "real" version is not matched strictly
// Matchmode to be used when comparing internal install version and internal latest version

bool doStringsMatchUnderRegEx(
String pattern, String value1, String value2) {
var r = RegExp(pattern);
var m1 = r.firstMatch(value1);
var m2 = r.firstMatch(value2);
return m1 != null && m2 != null
? value1.substring(m1.start, m1.end) ==
value2.substring(m2.start, m2.end)
: false;
}

Set<String> findStandardFormatsForVersion(String version, bool strict) {
Set<String> results = {};
for (var pattern in standardVersionRegExStrings) {
if (RegExp('${strict ? '^' : ''}$pattern${strict ? '\$' : ''}')
.hasMatch(version)) {
results.add(pattern);
}
}
return results;
}

var realStandardVersionFormats =
findStandardFormatsForVersion(realVersion, true);
var internalStandardVersionFormats =
findStandardFormatsForVersion(internalVersion, false);
var commonStandardFormats =
realStandardVersionFormats.intersection(internalStandardVersionFormats);
if (commonStandardFormats.isEmpty) {
return null; // Incompatible; no "enhanced detection"
}
for (String pattern in commonStandardFormats) {
if (doStringsMatchUnderRegEx(pattern, internalVersion, realVersion)) {
return matchMode
? internalVersion
: null; // Enhanced detection says no change
}
}
return matchMode
? null
: realVersion; // Enhanced detection says something changed
}

Future<void> loadApps() async {
while (loadingApps) {
await Future.delayed(const Duration(microseconds: 1));
Expand Down Expand Up @@ -489,7 +559,7 @@ class AppsProvider with ChangeNotifier {
}
}
if (modifiedApps.isNotEmpty) {
await saveApps(modifiedApps);
await saveApps(modifiedApps, attemptToCorrectInstallStatus: false);
}
}

Expand Down Expand Up @@ -609,7 +679,7 @@ class AppsProvider with ChangeNotifier {

Future<String> exportApps() async {
Directory? exportDir = Directory('/storage/emulated/0/Download');
String path = 'Downloads'; // TODO: Is this true on non-english phones?
String path = 'Downloads'; // TODO: See if hardcoding this can be avoided
if (!exportDir.existsSync()) {
exportDir = await getExternalStorageDirectory();
path = exportDir!.path;
Expand Down
Loading

0 comments on commit f81f637

Please sign in to comment.