From 68e98ec7197cd2e2b611291d846bfc9dc3beca16 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Thu, 16 May 2024 22:21:52 -0400 Subject: [PATCH 1/4] Don't use partial downloads for BG tasks (more reliable) --- lib/providers/apps_provider.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 6f10338..06ebe84 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -421,7 +421,8 @@ class AppsProvider with ChangeNotifier { } Future downloadApp(App app, BuildContext? context, - {NotificationsProvider? notificationsProvider}) async { + {NotificationsProvider? notificationsProvider, + bool useExisting = true}) async { var notifId = DownloadNotification(app.finalName, 0).id; if (apps[app.id] != null) { apps[app.id]!.downloadProgress = 0; @@ -453,7 +454,7 @@ class AppsProvider with ChangeNotifier { notificationsProvider?.notify(notif); } prevProg = prog; - }, APKDir.path); + }, APKDir.path, useExisting: useExisting); // Set to 90 for remaining steps, will make null in 'finally' if (apps[app.id] != null) { apps[app.id]!.downloadProgress = -1; @@ -766,7 +767,8 @@ class AppsProvider with ChangeNotifier { Future> downloadAndInstallLatestApps( List appIds, BuildContext? context, {NotificationsProvider? notificationsProvider, - bool forceParallelDownloads = false}) async { + bool forceParallelDownloads = false, + bool useExisting = true}) async { notificationsProvider = notificationsProvider ?? context?.read(); List appsToInstall = []; @@ -823,7 +825,8 @@ class AppsProvider with ChangeNotifier { var downloadedArtifact = // ignore: use_build_context_synchronously await downloadApp(apps[id]!.app, context, - notificationsProvider: notificationsProvider); + notificationsProvider: notificationsProvider, + useExisting: useExisting); DownloadedApk? downloadedFile; DownloadedXApkDir? downloadedDir; if (downloadedArtifact is DownloadedApk) { @@ -1927,7 +1930,8 @@ Future bgUpdateCheck(String taskId, Map? params) async { await appsProvider.downloadAndInstallLatestApps( toInstall.map((e) => e.key).toList(), null, notificationsProvider: notificationsProvider, - forceParallelDownloads: true); + forceParallelDownloads: true, + useExisting: false); } catch (e) { if (e is MultiAppMultiError) { e.idsByErrorString.forEach((key, value) { From fbfeaf2a91d41abb886f224d1fef9bb51dfd6c39 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Thu, 16 May 2024 22:31:21 -0400 Subject: [PATCH 2/4] Update Flutter, packages, increment version --- .flutter | 2 +- lib/main.dart | 2 +- lib/pages/app.dart | 2 +- lib/pages/apps.dart | 2 +- lib/pages/home.dart | 2 +- lib/pages/import_export.dart | 2 +- pubspec.lock | 54 ++++++++++++++++++------------------ pubspec.yaml | 3 +- 8 files changed, 35 insertions(+), 34 deletions(-) diff --git a/.flutter b/.flutter index 54e6646..5dcb86f 160000 --- a/.flutter +++ b/.flutter @@ -1 +1 @@ -Subproject commit 54e66469a933b60ddf175f858f82eaeb97e48c8d +Subproject commit 5dcb86f68f239346676ceb1ed1ea385bd215fba1 diff --git a/lib/main.dart b/lib/main.dart index 2b61de8..d6c47b8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -224,7 +224,7 @@ class _ObtainiumState extends State { // set the background and surface colors to pure black in the amoled theme if (settingsProvider.useBlackTheme) { darkColorScheme = darkColorScheme - .copyWith(background: Colors.black, surface: Colors.black) + .copyWith(surface: Colors.black) .harmonized(); } diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 3ed140c..ada2054 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -286,7 +286,7 @@ class _AppPageState extends State { ? WebViewWidget( controller: WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setBackgroundColor(Theme.of(context).colorScheme.background) + ..setBackgroundColor(Theme.of(context).colorScheme.surface) ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 3b4c9fd..d55a393 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -503,7 +503,7 @@ class AppsPageState extends State { ); var transparent = - Theme.of(context).colorScheme.background.withAlpha(0).value; + Theme.of(context).colorScheme.surface.withAlpha(0).value; List stops = [ ...listedApps[index].app.categories.asMap().entries.map( (e) => ((e.key / (listedApps[index].app.categories.length - 1)))), diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 00a89af..e9f02fe 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -103,7 +103,7 @@ class _HomePageState extends State { }) != null) { // ignore: use_build_context_synchronously - var appsProvider = await context.read(); + var appsProvider = context.read(); var result = await appsProvider.import(action == 'app' ? '{ "apps": [$dataStr] }' : '{ "apps": $dataStr }'); diff --git a/lib/pages/import_export.dart b/lib/pages/import_export.dart index 4d1da07..08fdb36 100644 --- a/lib/pages/import_export.dart +++ b/lib/pages/import_export.dart @@ -33,7 +33,7 @@ class _ImportExportPageState extends State { var settingsProvider = context.watch(); var outlineButtonStyle = ButtonStyle( - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( StadiumBorder( side: BorderSide( width: 1, diff --git a/pubspec.lock b/pubspec.lock index 15d7f79..fa8d129 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -47,10 +47,10 @@ packages: dependency: "direct main" description: name: app_links - sha256: "1c2b9e9c56d80d17610bcbd111b37187875c5d0ded8654caa1bda14ea753d001" + sha256: "8c6ef5ba9e26b720d4c9073826befb87df2ab5e7a81c22b6c3145080b5e736c9" url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.0.2" archive: dependency: transitive description: @@ -279,18 +279,18 @@ packages: dependency: "direct main" description: name: flex_color_picker - sha256: "5c846437069fb7afdd7ade6bf37e628a71d2ab0787095ddcb1253bf9345d5f3a" + sha256: "31b27677d8d8400e4cff5edb3f189f606dd964d608779b6ae1b7ddad37ea48c6" url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.5.0" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: "4cee2f1d07259f77e8b36f4ec5f35499d19e74e17c7dce5b819554914082bc01" + sha256: fb66cdb8ca89084e79efcad2bc2d9deb144666875116f08cdd8d9f8238c8b3ab url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" flutter: dependency: "direct main" description: flutter @@ -451,10 +451,10 @@ packages: dependency: transitive description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" json_annotation: dependency: transitive description: @@ -467,26 +467,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -496,7 +496,7 @@ packages: source: hosted version: "4.0.0" markdown: - dependency: transitive + dependency: "direct main" description: name: markdown sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 @@ -523,10 +523,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: @@ -857,10 +857,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" timezone: dependency: transitive description: @@ -961,10 +961,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" web: dependency: transitive description: @@ -1001,18 +1001,18 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: f12f8d8a99784b863e8b85e4a9a5e3cf1839d6803d2c0c3e0533a8f3c5a992a7 + sha256: "7affdf9d680c015b11587181171d3cad8093e449db1f7d9f0f08f4f33d24f9a0" url: "https://pub.dev" source: hosted - version: "3.13.0" + version: "3.13.1" win32: dependency: transitive description: name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.5.1" win32_registry: dependency: transitive description: @@ -1046,5 +1046,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.3 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index aa90e99..b648824 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: 1.1.8+2265 +version: 1.1.9+2266 environment: sdk: '>=3.0.0 <4.0.0' @@ -79,6 +79,7 @@ dependencies: url: https://github.com/re7gog/shizuku_apk_installer ref: master + markdown: any dev_dependencies: flutter_test: sdk: flutter From 389aebe54ec18191d80840aec74b696ca5002c69 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Fri, 17 May 2024 15:56:41 -0400 Subject: [PATCH 3/4] Various bugfixes --- lib/providers/apps_provider.dart | 215 ++++++++++++++++--------------- 1 file changed, 113 insertions(+), 102 deletions(-) diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 06ebe84..6542f7c 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -820,22 +820,82 @@ class AppsProvider with ChangeNotifier { appsToInstall = moveStrToEnd(appsToInstall, obtainiumId, strB: obtainiumTempId); - Future updateFn(String id, {bool skipInstalls = false}) async { + Future installFn(String id, bool willBeSilent, + DownloadedApk? downloadedFile, DownloadedXApkDir? downloadedDir) async { + apps[id]?.downloadProgress = -1; + notifyListeners(); + try { + bool sayInstalled = true; + var contextIfNewInstall = + apps[id]?.installedInfo == null ? context : null; + bool needBGWorkaround = + willBeSilent && context == null && !settingsProvider.useShizuku; + if (downloadedFile != null) { + if (needBGWorkaround) { + // ignore: use_build_context_synchronously + installApk(downloadedFile, contextIfNewInstall, + needsBGWorkaround: true); + } else { + // ignore: use_build_context_synchronously + sayInstalled = await installApk(downloadedFile, contextIfNewInstall, + shizukuPretendToBeGooglePlay: apps[id]! + .app + .additionalSettings['shizukuPretendToBeGooglePlay'] == + true); + } + } else { + if (needBGWorkaround) { + // ignore: use_build_context_synchronously + installXApkDir(downloadedDir!, contextIfNewInstall, + needsBGWorkaround: true); + } else { + // ignore: use_build_context_synchronously + sayInstalled = await installXApkDir( + downloadedDir!, contextIfNewInstall, + shizukuPretendToBeGooglePlay: apps[id]! + .app + .additionalSettings['shizukuPretendToBeGooglePlay'] == + true); + } + } + if (willBeSilent && context == null) { + if (!settingsProvider.useShizuku) { + notificationsProvider?.notify(SilentUpdateAttemptNotification( + [apps[id]!.app], + id: id.hashCode)); + } else { + notificationsProvider?.notify(SilentUpdateNotification( + [apps[id]!.app], sayInstalled, + id: id.hashCode)); + } + } + if (sayInstalled) { + installedIds.add(id); + } + } finally { + apps[id]?.downloadProgress = null; + notifyListeners(); + } + } + + Future> downloadFn(String id, + {bool skipInstalls = false}) async { + bool willBeSilent = false; + DownloadedApk? downloadedFile; + DownloadedXApkDir? downloadedDir; try { var downloadedArtifact = // ignore: use_build_context_synchronously await downloadApp(apps[id]!.app, context, notificationsProvider: notificationsProvider, useExisting: useExisting); - DownloadedApk? downloadedFile; - DownloadedXApkDir? downloadedDir; if (downloadedArtifact is DownloadedApk) { downloadedFile = downloadedArtifact; } else { downloadedDir = downloadedArtifact as DownloadedXApkDir; } id = downloadedFile?.appId ?? downloadedDir!.appId; - bool willBeSilent = await canInstallSilently(apps[id]!.app); + willBeSilent = await canInstallSilently(apps[id]!.app); if (!settingsProvider.useShizuku) { if (!(await settingsProvider.getInstallPermission(enforce: false))) { throw ObtainiumError(tr('cancelled')); @@ -856,80 +916,33 @@ class AppsProvider with ChangeNotifier { // ignore: use_build_context_synchronously await waitForUserToReturnToForeground(context); } - apps[id]?.downloadProgress = -1; - notifyListeners(); - try { - if (!skipInstalls) { - bool sayInstalled = true; - var contextIfNewInstall = - apps[id]?.installedInfo == null ? context : null; - bool needBGWorkaround = - willBeSilent && context == null && !settingsProvider.useShizuku; - if (downloadedFile != null) { - if (needBGWorkaround) { - // ignore: use_build_context_synchronously - installApk(downloadedFile, contextIfNewInstall, - needsBGWorkaround: true); - } else { - // ignore: use_build_context_synchronously - sayInstalled = await installApk( - downloadedFile, contextIfNewInstall, - shizukuPretendToBeGooglePlay: - apps[id]!.app.additionalSettings[ - 'shizukuPretendToBeGooglePlay'] == - true); - } - } else { - if (needBGWorkaround) { - // ignore: use_build_context_synchronously - installXApkDir(downloadedDir!, contextIfNewInstall, - needsBGWorkaround: true); - } else { - // ignore: use_build_context_synchronously - sayInstalled = await installXApkDir( - downloadedDir!, contextIfNewInstall, - shizukuPretendToBeGooglePlay: - apps[id]!.app.additionalSettings[ - 'shizukuPretendToBeGooglePlay'] == - true); - } - } - if (willBeSilent && context == null) { - if (!settingsProvider.useShizuku) { - notificationsProvider?.notify(SilentUpdateAttemptNotification( - [apps[id]!.app], - id: id.hashCode)); - } else { - notificationsProvider?.notify(SilentUpdateNotification( - [apps[id]!.app], sayInstalled, - id: id.hashCode)); - } - } - if (sayInstalled) { - installedIds.add(id); - } - } - } finally { - apps[id]?.downloadProgress = null; - notifyListeners(); - } } catch (e) { errors.add(id, e, appName: apps[id]?.name); } - return id; + return { + 'id': id, + 'willBeSilent': willBeSilent, + 'downloadedFile': downloadedFile, + 'downloadedDir': downloadedDir + }; } + List> downloadResults = []; if (forceParallelDownloads || !settingsProvider.parallelDownloads) { for (var id in appsToInstall) { - await updateFn(id); + downloadResults.add(await downloadFn(id)); } } else { - List ids = await Future.wait( - appsToInstall.map((id) => updateFn(id, skipInstalls: true))); - for (var id in ids) { - if (!errors.appIdNames.containsKey(id)) { - await updateFn(id); - } + downloadResults = await Future.wait( + appsToInstall.map((id) => downloadFn(id, skipInstalls: true))); + } + for (var res in downloadResults) { + if (!errors.appIdNames.containsKey(res['id'])) { + await installFn( + res['id'] as String, + res['willBeSilent'] as bool, + res['downloadedFile'] as DownloadedApk?, + res['downloadedDir'] as DownloadedXApkDir?); } } @@ -1166,40 +1179,38 @@ class AppsProvider with ChangeNotifier { notifyListeners(); var sp = SourceProvider(); List> errors = []; - List newApps = (await getAppsDir()) // Parse Apps from JSON + await Future.wait((await getAppsDir()) // Parse Apps from JSON .listSync() - .where((item) => item.path.toLowerCase().endsWith('.json')) - .where((item) => - singleId == null || - item.path.split('/').last.toLowerCase() == - '${singleId.toLowerCase()}.json') - .map((e) { - try { - return App.fromJson(jsonDecode(File(e.path).readAsStringSync())); - } catch (err) { - if (err is FormatException) { - logs.add('Corrupt JSON when loading App (will be ignored): $e'); - e.renameSync('${e.path}.corrupt'); - } else { - rethrow; + .map((item) async { + App? app; + if (item.path.toLowerCase().endsWith('.json') && + (singleId == null || + item.path.split('/').last.toLowerCase() == + '${singleId.toLowerCase()}.json')) { + try { + app = App.fromJson(jsonDecode(File(item.path).readAsStringSync())); + } catch (err) { + if (err is FormatException) { + logs.add('Corrupt JSON when loading App (will be ignored): $e'); + item.renameSync('${item.path}.corrupt'); + } else { + rethrow; + } } } - }).toList(); - for (var app in newApps) { - // Put Apps into memory to list them (fast) if (app != null) { try { sp.getSource(app.url, overrideSource: app.overrideSource); apps.update( app.id, - (value) => AppInMemory( - app, value.downloadProgress, value.installedInfo, value.icon), - ifAbsent: () => AppInMemory(app, null, null, null)); + (value) => AppInMemory(app!, value.downloadProgress, + value.installedInfo, value.icon), + ifAbsent: () => AppInMemory(app!, null, null, null)); } catch (e) { errors.add([app.id, app.finalName, e.toString()]); } } - } + })); notifyListeners(); if (errors.isNotEmpty) { removeApps(errors.map((e) => e[0]).toList()); @@ -1207,19 +1218,17 @@ class AppsProvider with ChangeNotifier { AppsRemovedNotification(errors.map((e) => [e[1], e[2]]).toList())); } // Get install status and other OS info for each App (slow) - await Future.wait(apps.values.map((app) { - return updateInstallStatusInMemory(app); - })); - notifyListeners(); - // Reconcile version differences List modifiedApps = []; - for (var app in apps.values) { + await Future.wait(apps.values.map((app) async { + await updateInstallStatusInMemory(app); var moddedApp = getCorrectedInstallStatusAppIfPossible(app.app, app.installedInfo); if (moddedApp != null) { modifiedApps.add(moddedApp); } - } + })); + notifyListeners(); + // Reconcile version differences if (modifiedApps.isNotEmpty) { await saveApps(modifiedApps, attemptToCorrectInstallStatus: false); var removedAppIds = modifiedApps @@ -1241,7 +1250,7 @@ class AppsProvider with ChangeNotifier { {bool attemptToCorrectInstallStatus = true, bool onlyIfExists = true}) async { attemptToCorrectInstallStatus = attemptToCorrectInstallStatus; - for (var a in apps) { + await Future.wait(apps.map((a) async { var app = a.deepCopy(); PackageInfo? info = await getInstalledInfo(app.id); var icon = await info?.applicationInfo?.getAppIcon(); @@ -1263,14 +1272,14 @@ class AppsProvider with ChangeNotifier { rethrow; } } - } + })); notifyListeners(); export(isAuto: true); } Future removeApps(List appIds) async { var apkFiles = APKDir.listSync(); - for (var appId in appIds) { + await Future.wait(appIds.map((appId) async { File file = File('${(await getAppsDir()).path}/$appId.json'); if (file.existsSync()) { file.deleteSync(recursive: true); @@ -1284,7 +1293,7 @@ class AppsProvider with ChangeNotifier { if (apps.containsKey(appId)) { apps.remove(appId); } - } + })); if (appIds.isNotEmpty) { notifyListeners(); export(isAuto: true); @@ -1543,6 +1552,8 @@ class AppsProvider with ChangeNotifier { settingsMap.forEach((key, value) { if (value is int) { settingsProvider.prefs?.setInt(key, value); + } else if (value is double) { + settingsProvider.prefs?.setDouble(key, value); } else if (value is bool) { settingsProvider.prefs?.setBool(key, value); } else if (value is List) { From 0e2fa96b9fb08b761a598b672e5f82c36a2ec24d Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Fri, 17 May 2024 16:17:51 -0400 Subject: [PATCH 4/4] More accurate error reports for Huawei AppGallery fails --- lib/app_sources/huaweiappgallery.dart | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/app_sources/huaweiappgallery.dart b/lib/app_sources/huaweiappgallery.dart index 8948faf..0f2be00 100644 --- a/lib/app_sources/huaweiappgallery.dart +++ b/lib/app_sources/huaweiappgallery.dart @@ -73,21 +73,23 @@ class HuaweiAppGallery extends AppSource { throw NoReleasesError(); } String appId = appIdFromRedirectDlUrl(res.headers['location']!); + if (appId.isEmpty) { + throw NoReleasesError(); + } var relDateStr = res.headers['location']?.split('?')[0].split('.').reversed.toList()[1]; - var relDateStrAdj = relDateStr?.split(''); - var tempLen = relDateStrAdj?.length ?? 0; - var i = 2; - while (i < tempLen) { - relDateStrAdj?.insert((i + i ~/ 2 - 1), '-'); - i += 2; - } - var relDate = relDateStrAdj == null - ? null - : DateFormat('yy-MM-dd-HH-mm', 'en_US').parse(relDateStrAdj.join('')); - if (relDateStr == null) { + if (relDateStr == null || relDateStr.length != 10) { throw NoVersionError(); } + var relDateStrAdj = relDateStr.split(''); + var tempLen = relDateStrAdj.length; + var i = 2; + while (i < tempLen) { + relDateStrAdj.insert((i + i ~/ 2 - 1), '-'); + i += 2; + } + var relDate = + DateFormat('yy-MM-dd-HH-mm', 'en_US').parse(relDateStrAdj.join('')); return APKDetails( relDateStr, [MapEntry('$appId.apk', dlUrl)], AppNames(name, appId), releaseDate: relDate);