Compare commits

...

3 Commits

Author SHA1 Message Date
27ee6b9e88 Bugfix: Mass install not working 2022-10-29 18:59:27 -04:00
d1a3529036 Switched to package names as app ids (#69)
* Fixes #14 (although detection is disabled in background processes due to the bug described in #60)
    * Added App icons and basic installed detection
    * Real Package Names Used as IDs + App Icons (INCONVENIENT FOR PREVIOUS VERSION USERS)
    * Switch to using extracted names (no custom names)

* Fixes #57

* Fixes #67

* Fixes #64

* Fixes #61

* Commented out APKMirror and added code to remove their Apps

* Updated README

* Switched to Flutter stable (causes some UI elements to switch back to the old material design style, but this will be fixed in later Flutter releases)

* BG task silently retries on network errors

* Updated screenshots
2022-10-29 13:13:28 -04:00
a954a627fd Fixed 2 issues:
- Rate-limit regression in previous release
- Update notifications not sent when >1 apps have errors
2022-10-11 18:39:53 -04:00
16 changed files with 592 additions and 433 deletions

View File

@ -13,7 +13,6 @@ Currently supported App sources:
- [IzzyOnDroid](https://android.izzysoft.de/)
- [Mullvad](https://mullvad.net/en/)
- [Signal](https://signal.org/)
- [APKMirror](https://apkmirror.com/)
## Limitations
- App installs are assumed to have succeeded; failures and cancelled installs cannot be detected.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/pages/home.dart';
import 'package:obtainium/providers/apps_provider.dart';
@ -13,12 +14,14 @@ import 'package:workmanager/workmanager.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:device_info_plus/device_info_plus.dart';
const String currentVersion = '0.6.1';
const String currentReleaseTag =
'v0.5.9-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
const String bgUpdateCheckTaskName = 'bg-update-check';
bgUpdateCheck(int? ignoreAfterMicroseconds) async {
WidgetsFlutterBinding.ensureInitialized();
DateTime? ignoreAfter = ignoreAfterMicroseconds != null
? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds)
: null;
@ -27,22 +30,28 @@ bgUpdateCheck(int? ignoreAfterMicroseconds) async {
try {
var appsProvider = AppsProvider();
await notificationsProvider.cancel(ErrorCheckingUpdatesNotification('').id);
await appsProvider.loadApps();
await appsProvider.loadApps(shouldCorrectInstallStatus: false);
List<String> existingUpdateIds =
appsProvider.getExistingUpdates(installedOnly: true);
DateTime nextIgnoreAfter = DateTime.now();
String? err;
try {
await appsProvider.checkUpdates(ignoreAfter: ignoreAfter);
await appsProvider.checkUpdates(
ignoreAfter: ignoreAfter,
immediatelyThrowRateLimitError: true,
immediatelyThrowSocketError: true,
shouldCorrectInstallStatus: false);
} catch (e) {
if (e is RateLimitError) {
if (e is RateLimitError || e is SocketException) {
String nextTaskName =
'$bgUpdateCheckTaskName-${nextIgnoreAfter.microsecondsSinceEpoch.toString()}';
Workmanager().registerOneOffTask(nextTaskName, nextTaskName,
constraints: Constraints(networkType: NetworkType.connected),
initialDelay: Duration(minutes: e.remainingMinutes),
initialDelay: Duration(
minutes: e is RateLimitError ? e.remainingMinutes : 15),
inputData: {'ignoreAfter': nextIgnoreAfter.microsecondsSinceEpoch});
} else {
rethrow;
err = e.toString();
}
}
List<App> newUpdates = appsProvider
@ -66,13 +75,15 @@ bgUpdateCheck(int? ignoreAfterMicroseconds) async {
// }
if (newUpdates.isNotEmpty) {
notificationsProvider.notify(UpdateNotification(newUpdates),
cancelExisting: true);
notificationsProvider.notify(UpdateNotification(newUpdates));
}
if (err != null) {
throw err;
}
return Future.value(true);
} catch (e) {
notificationsProvider.notify(ErrorCheckingUpdatesNotification(e.toString()),
cancelExisting: true);
notificationsProvider
.notify(ErrorCheckingUpdatesNotification(e.toString()));
return Future.error(false);
} finally {
await notificationsProvider.cancel(checkingUpdatesNotification.id);
@ -89,7 +100,7 @@ void bgTaskCallback() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt! >= 29) {
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) {
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent),
);
@ -138,7 +149,7 @@ class _ObtainiumState extends State<Obtainium> {
Permission.notification.request();
appsProvider.saveApps([
App(
'imranr98_obtainium_${GitHub().host}',
'dev.imranr.obtainium',
'https://github.com/ImranR98/Obtainium',
'ImranR98',
'Obtainium',

View File

@ -22,7 +22,6 @@ class _AddAppPageState extends State<AddAppPage> {
String userInput = '';
AppSource? pickedSource;
List<String> additionalData = [];
String customName = '';
bool validAdditionalData = true;
@override
@ -80,9 +79,6 @@ class _AddAppPageState extends State<AddAppPage> {
.doesSourceHaveRequiredAdditionalData(
source)
: true;
if (source == null) {
customName = '';
}
}
});
},
@ -90,56 +86,72 @@ class _AddAppPageState extends State<AddAppPage> {
const SizedBox(
width: 16,
),
ElevatedButton(
onPressed: gettingAppInfo ||
pickedSource == null ||
(pickedSource!.additionalDataFormItems
.isNotEmpty &&
!validAdditionalData)
? null
: () {
HapticFeedback.selectionClick();
setState(() {
gettingAppInfo = true;
});
sourceProvider
.getApp(pickedSource!, userInput,
additionalData,
customName: customName)
.then((app) {
var appsProvider =
context.read<AppsProvider>();
var settingsProvider =
context.read<SettingsProvider>();
if (appsProvider.apps
.containsKey(app.id)) {
throw 'App already added';
}
settingsProvider
.getInstallPermission()
.then((_) {
appsProvider
.saveApps([app]).then((_) {
gettingAppInfo
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: gettingAppInfo ||
pickedSource == null ||
(pickedSource!.additionalDataFormItems
.isNotEmpty &&
!validAdditionalData)
? null
: () async {
setState(() {
gettingAppInfo = true;
});
var appsProvider =
context.read<AppsProvider>();
var settingsProvider =
context.read<SettingsProvider>();
() async {
HapticFeedback.selectionClick();
App app =
await sourceProvider.getApp(
pickedSource!,
userInput,
additionalData);
await settingsProvider
.getInstallPermission();
// ignore: use_build_context_synchronously
var apkUrl = await appsProvider
.selectApkUrl(app, context);
if (apkUrl == null) {
throw 'Cancelled';
}
app.preferredApkIndex =
app.apkUrls.indexOf(apkUrl);
var downloadedApk =
await appsProvider
.downloadApp(app);
app.id = downloadedApk.appId;
if (appsProvider.apps
.containsKey(app.id)) {
throw 'App already added';
}
await appsProvider.saveApps([app]);
return app;
}()
.then((app) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
AppPage(
appId: app.id)));
}).catchError((e) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(e.toString())),
);
}).whenComplete(() {
setState(() {
gettingAppInfo = false;
});
});
});
}).catchError((e) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(content: Text(e.toString())),
);
}).whenComplete(() {
setState(() {
gettingAppInfo = false;
});
});
},
child: const Text('Add'))
},
child: const Text('Add'))
],
),
if (pickedSource != null)
@ -174,21 +186,6 @@ class _AddAppPageState extends State<AddAppPage> {
const SizedBox(
height: 8,
),
if (pickedSource != null)
GeneratedForm(
items: [
[
GeneratedFormItem(
label: 'Custom App Name',
required: false)
]
],
onValueChanges: (values, valid) {
setState(() {
customName = values[0];
});
},
defaultValues: [customName])
],
)
else

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/providers/settings_provider.dart';
@ -46,6 +45,7 @@ class _AppPageState extends State<AppPage> {
body: RefreshIndicator(
child: settingsProvider.showAppWebpage
? WebView(
backgroundColor: Theme.of(context).colorScheme.background,
initialUrl: app?.app.url,
javascriptMode: JavascriptMode.unrestricted,
)
@ -56,8 +56,18 @@ class _AppPageState extends State<AppPage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
app?.installedInfo != null
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.memory(
app!.installedInfo!.icon!,
scale: 1.5,
)
])
: Container(),
Text(
app?.app.name ?? 'App',
app?.installedInfo?.name ?? app?.app.name ?? 'App',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.displayLarge,
),
@ -126,7 +136,8 @@ class _AppPageState extends State<AppPage> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (app?.app.installedVersion != app?.app.latestVersion)
if (app?.app.installedVersion != null &&
app?.app.installedVersion != app?.app.latestVersion)
IconButton(
onPressed: app?.downloadProgress != null
? null
@ -135,8 +146,8 @@ class _AppPageState extends State<AppPage> {
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
title: Text(
'App Already ${app?.app.installedVersion == null ? 'Installed' : 'Updated'}?'),
title: const Text(
'App Already up to Date?'),
actions: [
TextButton(
onPressed: () {
@ -161,54 +172,13 @@ class _AppPageState extends State<AppPage> {
.pop();
},
child: const Text(
'Yes, Mark as Installed'))
'Yes, Mark as Updated'))
],
);
});
},
tooltip: 'Mark as Installed',
icon: const Icon(Icons.done))
else
IconButton(
onPressed: app?.downloadProgress != null
? null
: () {
showDialog(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
title: const Text(
'App Not Installed?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context)
.pop();
},
child: const Text('No')),
TextButton(
onPressed: () {
HapticFeedback
.selectionClick();
var updatedApp = app?.app;
if (updatedApp != null) {
updatedApp
.installedVersion =
null;
appsProvider.saveApps(
[updatedApp]);
}
Navigator.of(context)
.pop();
},
child: const Text(
'Yes, Mark as Not Installed'))
],
);
});
},
tooltip: 'Mark as Not Installed',
icon: const Icon(Icons.no_cell_outlined)),
tooltip: 'Mark as Updated',
icon: const Icon(Icons.done)),
if (source != null &&
source.additionalDataFormItems.isNotEmpty)
IconButton(
@ -220,30 +190,15 @@ class _AppPageState extends State<AppPage> {
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: 'Additional Options',
items: [
...source
.additionalDataFormItems,
[
GeneratedFormItem(
label: 'App Name',
required: true)
]
],
items: source
.additionalDataFormItems,
defaultValues: app != null
? [
...app
.app.additionalData,
app.app.name
]
: [
...source
.additionalDataDefaults
]);
? app.app.additionalData
: source
.additionalDataDefaults);
}).then((values) {
if (app != null && values != null) {
var changedApp = app.app;
var name = values.removeLast();
changedApp.name = name;
changedApp.additionalData = values;
appsProvider.saveApps(
[changedApp]).then((value) {
@ -265,12 +220,18 @@ class _AppPageState extends State<AppPage> {
? () {
HapticFeedback.heavyImpact();
appsProvider
.downloadAndInstallLatestApp(
.downloadAndInstallLatestApps(
[app!.app.id],
context).then((res) {
if (res.isNotEmpty && mounted) {
Navigator.of(context).pop();
}
}).catchError((e) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(e.toString())),
);
});
}
: null,
@ -288,7 +249,7 @@ class _AppPageState extends State<AppPage> {
return AlertDialog(
title: const Text('Remove App?'),
content: Text(
'This will remove \'${app?.app.name}\' from Obtainium.${app?.app.installedVersion != null ? '\n\nNote that while Obtainium will no longer track its updates, the App will remain installed.' : ''}'),
'This will remove \'${app?.installedInfo?.name ?? app?.app.name}\' from Obtainium.${app?.app.installedVersion != null ? '\n\nNote that while Obtainium will no longer track its updates, the App will remain installed.' : ''}'),
actions: [
TextButton(
onPressed: () {

View File

@ -89,7 +89,8 @@ class AppsPageState extends State<AppsPage> {
.toList();
for (var t in nameTokens) {
if (!app.app.name.toLowerCase().contains(t.toLowerCase())) {
var name = app.installedInfo?.name ?? app.app.name;
if (!name.toLowerCase().contains(t.toLowerCase())) {
return false;
}
}
@ -103,13 +104,13 @@ class AppsPageState extends State<AppsPage> {
}
sortedApps.sort((a, b) {
var nameA = a.installedInfo?.name ?? a.app.name;
var nameB = b.installedInfo?.name ?? b.app.name;
int result = 0;
if (settingsProvider.sortColumn == SortColumnSettings.authorName) {
result =
(a.app.author + a.app.name).compareTo(b.app.author + b.app.name);
result = (a.app.author + nameA).compareTo(b.app.author + nameB);
} else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) {
result =
(a.app.name + a.app.author).compareTo(b.app.name + b.app.author);
result = (nameA + a.app.author).compareTo(nameB + b.app.author);
}
return result;
});
@ -166,7 +167,11 @@ class AppsPageState extends State<AppsPage> {
onLongPress: () {
toggleAppSelected(sortedApps[index].app.id);
},
title: Text(sortedApps[index].app.name),
leading: sortedApps[index].installedInfo != null
? Image.memory(sortedApps[index].installedInfo!.icon!)
: null,
title: Text(sortedApps[index].installedInfo?.name ??
sortedApps[index].app.name),
subtitle: Text('By ${sortedApps[index].app.author}'),
trailing: sortedApps[index].downloadProgress != null
? Text(
@ -178,7 +183,9 @@ class AppsPageState extends State<AppsPage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const Text('Update Available'),
Text(appsProvider.areDownloadsRunning()
? 'Please Wait...'
: 'Update Available'),
SourceProvider()
.getSource(sortedApps[index].app.url)
.changeLogPageFromStandardUrl(
@ -303,15 +310,20 @@ class AppsPageState extends State<AppsPage> {
message:
'${existingUpdateIdsAllOrSelected.length} update${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'} and ${newInstallIdsAllOrSelected.length} new install${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}.',
items: formInputs,
defaultValues: const ['true', 'true'],
defaultValues: [
'true',
existingUpdateIdsAllOrSelected.isEmpty
? 'true'
: ''
],
initValid: true,
);
}).then((values) {
if (values != null) {
bool shouldInstallUpdates =
values.length < 2 || values[0] == 'true';
bool shouldInstallNew =
values.length < 2 || values[1] == 'true';
values.isEmpty || values[0] == 'true';
bool shouldInstallNew = values.isEmpty ||
(values.length >= 2 && values[1] == 'true');
settingsProvider
.getInstallPermission()
.then((_) {
@ -324,8 +336,14 @@ class AppsPageState extends State<AppsPage> {
toInstall
.addAll(newInstallIdsAllOrSelected);
}
appsProvider.downloadAndInstallLatestApp(
toInstall, context);
appsProvider
.downloadAndInstallLatestApps(
toInstall, context)
.catchError((e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString())),
);
});
});
}
});
@ -349,7 +367,7 @@ class AppsPageState extends State<AppsPage> {
padding: const EdgeInsets.only(top: 6),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
MainAxisAlignment.spaceAround,
children: [
IconButton(
onPressed:
@ -364,7 +382,10 @@ class AppsPageState extends State<AppsPage> {
ctx) {
return AlertDialog(
title: Text(
'Mark ${selectedIds.length} Selected Apps as Not Installed?'),
'Mark ${selectedIds.length} Selected Apps as Updated?'),
content:
const Text(
'Only applies to installed but out of date Apps.'),
actions: [
TextButton(
onPressed:
@ -383,8 +404,10 @@ class AppsPageState extends State<AppsPage> {
.saveApps(selectedIds.map((e) {
var a =
appsProvider.apps[e]!.app;
a.installedVersion =
null;
if (a.installedVersion !=
null) {
a.installedVersion = a.latestVersion;
}
return a;
}).toList());
@ -398,57 +421,7 @@ class AppsPageState extends State<AppsPage> {
});
},
tooltip:
'Mark Selected Apps as Not Installed',
icon: const Icon(
Icons.no_cell_outlined)),
IconButton(
onPressed:
appsProvider
.areDownloadsRunning()
? null
: () {
showDialog(
context: context,
builder:
(BuildContext
ctx) {
return AlertDialog(
title: Text(
'Mark ${selectedIds.length} Selected Apps as Installed/Updated?'),
actions: [
TextButton(
onPressed:
() {
Navigator.of(context)
.pop();
},
child: const Text(
'No')),
TextButton(
onPressed:
() {
HapticFeedback
.selectionClick();
appsProvider
.saveApps(selectedIds.map((e) {
var a =
appsProvider.apps[e]!.app;
a.installedVersion =
a.latestVersion;
return a;
}).toList());
Navigator.of(context)
.pop();
},
child: const Text(
'Yes'))
],
);
});
},
tooltip:
'Mark Selected Apps as Installed/Updated',
'Mark Selected Apps as Updated',
icon: const Icon(Icons.done)),
IconButton(
onPressed: () {

View File

@ -9,8 +9,12 @@ import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:install_plugin_v2/install_plugin_v2.dart';
import 'package:installed_apps/app_info.dart';
import 'package:installed_apps/installed_apps.dart';
import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/notifications_provider.dart';
import 'package:package_archive_info/package_archive_info.dart';
import 'package:provider/provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_fgbg/flutter_fgbg.dart';
@ -20,14 +24,15 @@ import 'package:http/http.dart';
class AppInMemory {
late App app;
double? downloadProgress;
AppInfo? installedInfo; // Also indicates that an App is installed
AppInMemory(this.app, this.downloadProgress);
AppInMemory(this.app, this.downloadProgress, this.installedInfo);
}
class ApkFile {
class DownloadedApp {
String appId;
File file;
ApkFile(this.appId, this.file);
DownloadedApp(this.appId, this.file);
}
class AppsProvider with ChangeNotifier {
@ -38,24 +43,24 @@ class AppsProvider with ChangeNotifier {
// 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;
late Stream<FGBGType>? foregroundStream;
late StreamSubscription<FGBGType>? foregroundSubscription;
AppsProvider(
{bool shouldLoadApps = false,
bool shouldCheckUpdatesAfterLoad = false,
bool shouldDeleteAPKs = false}) {
// 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();
});
if (shouldDeleteAPKs) {
deleteSavedAPKs();
}
if (shouldLoadApps) {
// 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();
});
loadApps().then((_) {
if (shouldDeleteAPKs) {
deleteSavedAPKs();
}
if (shouldCheckUpdatesAfterLoad) {
checkUpdates();
}
@ -63,38 +68,85 @@ class AppsProvider with ChangeNotifier {
}
}
Future<ApkFile> downloadApp(String apkUrl, String appId) async {
apkUrl = await SourceProvider()
.getSource(apps[appId]!.app.url)
.apkUrlPrefetchModifier(apkUrl);
downloadApk(String apkUrl, String fileName, Function? onProgress,
Function? urlModifier,
{bool useExistingIfExists = true}) async {
var destDir = (await getExternalStorageDirectory())!.path;
if (urlModifier != null) {
apkUrl = await urlModifier(apkUrl);
}
StreamedResponse response =
await Client().send(Request('GET', Uri.parse(apkUrl)));
File downloadFile =
File('${(await getExternalStorageDirectory())!.path}/$appId.apk');
if (downloadFile.existsSync()) {
downloadFile.deleteSync();
}
var length = response.contentLength;
var received = 0;
var sink = downloadFile.openWrite();
File downloadFile = File('$destDir/$fileName.apk');
var alreadyExists = downloadFile.existsSync();
if (!alreadyExists || !useExistingIfExists) {
if (alreadyExists) {
downloadFile.deleteSync();
}
await response.stream.map((s) {
received += s.length;
apps[appId]!.downloadProgress =
(length != null ? received / length * 100 : 30);
var length = response.contentLength;
var received = 0;
double? progress;
var sink = downloadFile.openWrite();
await response.stream.map((s) {
received += s.length;
progress = (length != null ? received / length * 100 : 30);
if (onProgress != null) {
onProgress(progress);
}
return s;
}).pipe(sink);
await sink.close();
progress = null;
if (onProgress != null) {
onProgress(progress);
}
if (response.statusCode != 200) {
downloadFile.deleteSync();
throw response.reasonPhrase ?? 'Unknown Error';
}
}
return downloadFile;
}
// Downloads the App (preferred URL) and returns an ApkFile object
// If the app was already saved, updates it's download progress % in memory
// But also works for Apps that are not saved
Future<DownloadedApp> downloadApp(App app) async {
var fileName = '${app.id}-${app.latestVersion}-${app.preferredApkIndex}';
File downloadFile = await downloadApk(app.apkUrls[app.preferredApkIndex],
'${app.id}-${app.latestVersion}-${app.preferredApkIndex}',
(double? progress) {
if (apps[app.id] != null) {
apps[app.id]!.downloadProgress = progress;
}
notifyListeners();
return s;
}).pipe(sink);
await sink.close();
apps[appId]!.downloadProgress = null;
notifyListeners();
if (response.statusCode != 200) {
downloadFile.deleteSync();
throw response.reasonPhrase ?? 'Unknown Error';
}, SourceProvider().getSource(app.url).apkUrlPrefetchModifier);
// Delete older versions of the APK if any
for (var file in downloadFile.parent.listSync()) {
var fn = file.path.split('/').last;
if (fn.startsWith('${app.id}-') &&
fn.endsWith('.apk') &&
fn != '$fileName.apk') {
file.delete();
}
}
return ApkFile(appId, downloadFile);
// If the ID has changed (as it should on first download), replace it
var newInfo = await PackageArchiveInfo.fromPath(downloadFile.path);
if (app.id != newInfo.packageName) {
var originalAppId = app.id;
app.id = newInfo.packageName;
downloadFile = downloadFile.renameSync(
'${downloadFile.parent.path}/${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk');
if (apps[originalAppId] != null) {
await removeApps([originalAppId]);
await saveApps([app]);
}
}
return DownloadedApp(app.id, downloadFile);
}
bool areDownloadsRunning() => apps.values
@ -105,8 +157,8 @@ class AppsProvider with ChangeNotifier {
// TODO: This is unreliable - try to get from OS in the future
var osInfo = await DeviceInfoPlugin().androidInfo;
return app.installedVersion != null &&
osInfo.version.sdkInt! >= 30 &&
osInfo.version.release!.compareTo('12') >= 0;
osInfo.version.sdkInt >= 30 &&
osInfo.version.release.compareTo('12') >= 0;
}
Future<void> askUserToReturnToForeground(BuildContext context,
@ -127,11 +179,62 @@ class AppsProvider with ChangeNotifier {
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
// If appropriate criteria are met, the update (never a fresh install) happens silently in the background
// But even then, we don't know if it actually succeeded
Future<void> installApk(ApkFile file) async {
await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium');
Future<void> installApk(DownloadedApp file) async {
var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
AppInfo? appInfo;
try {
appInfo = await InstalledApps.getAppInfo(apps[file.appId]!.app.id);
} catch (e) {
// OK
}
if (appInfo != null &&
int.parse(newInfo.buildNumber) < appInfo.versionCode!) {
throw 'Can\'t install an older version';
}
if (appInfo == null ||
int.parse(newInfo.buildNumber) > appInfo.versionCode!) {
await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium');
}
apps[file.appId]!.app.installedVersion =
apps[file.appId]!.app.latestVersion;
await saveApps([apps[file.appId]!.app]);
// Don't correct install status as installation may not be done yet
await saveApps([apps[file.appId]!.app], shouldCorrectInstallStatus: false);
}
Future<String?> selectApkUrl(App app, BuildContext? context) async {
// If the App has more than one APK, the user should pick one (if context provided)
String? apkUrl = app.apkUrls[app.preferredApkIndex];
if (app.apkUrls.length > 1 && context != null) {
apkUrl = await showDialog(
context: context,
builder: (BuildContext ctx) {
return APKPicker(app: app, initVal: apkUrl);
});
}
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
if (apkUrl != null &&
Uri.parse(apkUrl).origin != Uri.parse(app.url).origin &&
context != null) {
if (await showDialog(
context: context,
builder: (BuildContext ctx) {
return APKOriginWarningDialog(
sourceUrl: app.url, apkUrl: apkUrl!);
}) !=
true) {
apkUrl = null;
}
}
return apkUrl;
}
Map<String, List<String>> addToErrorMap(
Map<String, List<String>> errors, String appId, String error) {
var tempIds = errors.remove(error);
tempIds ??= [];
tempIds.add(appId);
errors.putIfAbsent(error, () => tempIds!);
return errors;
}
// Given a list of AppIds, uses stored info about the apps to download APKs and install them
@ -139,37 +242,16 @@ 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>> downloadAndInstallLatestApp(
Future<List<String>> downloadAndInstallLatestApps(
List<String> appIds, BuildContext? context) async {
Map<String, String> appsToInstall = {};
List<String> appsToInstall = [];
for (var id in appIds) {
if (apps[id] == null) {
throw 'App not found';
}
// If the App has more than one APK, the user should pick one (if context provided)
String? apkUrl = apps[id]!.app.apkUrls[apps[id]!.app.preferredApkIndex];
if (apps[id]!.app.apkUrls.length > 1 && context != null) {
apkUrl = await showDialog(
context: context,
builder: (BuildContext ctx) {
return APKPicker(app: apps[id]!.app, initVal: apkUrl);
});
}
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
if (apkUrl != null &&
Uri.parse(apkUrl).origin != Uri.parse(apps[id]!.app.url).origin &&
context != null) {
if (await showDialog(
context: context,
builder: (BuildContext ctx) {
return APKOriginWarningDialog(
sourceUrl: apps[id]!.app.url, apkUrl: apkUrl!);
}) !=
true) {
apkUrl = null;
}
}
String? apkUrl = await selectApkUrl(apps[id]!.app, context);
if (apkUrl != null) {
int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl);
if (urlInd != apps[id]!.app.preferredApkIndex) {
@ -179,18 +261,28 @@ class AppsProvider with ChangeNotifier {
if (context != null ||
(await canInstallSilently(apps[id]!.app) &&
apps[id]!.app.apkUrls.length == 1)) {
appsToInstall.putIfAbsent(id, () => apkUrl!);
appsToInstall.add(id);
}
}
}
Map<String, List<String>> errors = {};
List<ApkFile> downloadedFiles = await Future.wait(appsToInstall.entries
.map((entry) => downloadApp(entry.value, entry.key)));
List<DownloadedApp?> downloadedFiles =
await Future.wait(appsToInstall.map((id) async {
try {
return await downloadApp(apps[id]!.app);
} catch (e) {
addToErrorMap(errors, id, e.toString());
}
return null;
}));
downloadedFiles =
downloadedFiles.where((element) => element != null).toList();
List<ApkFile> silentUpdates = [];
List<ApkFile> regularInstalls = [];
List<DownloadedApp> silentUpdates = [];
List<DownloadedApp> regularInstalls = [];
for (var f in downloadedFiles) {
bool willBeSilent = await canInstallSilently(apps[f.appId]!.app);
bool willBeSilent = await canInstallSilently(apps[f!.appId]!.app);
if (willBeSilent) {
silentUpdates.add(f);
} else {
@ -199,9 +291,9 @@ class AppsProvider with ChangeNotifier {
}
// If Obtainium is being installed, it should be the last one
List<ApkFile> moveObtainiumToEnd(List<ApkFile> items) {
List<DownloadedApp> moveObtainiumToEnd(List<DownloadedApp> items) {
String obtainiumId = 'imranr98_obtainium_${GitHub().host}';
ApkFile? temp;
DownloadedApp? temp;
items.removeWhere((element) {
bool res = element.appId == obtainiumId;
if (res) {
@ -232,11 +324,23 @@ class AppsProvider with ChangeNotifier {
await askUserToReturnToForeground(context, waitForFG: true);
}
for (var i in regularInstalls) {
await installApk(i);
try {
await installApk(i);
} catch (e) {
addToErrorMap(errors, i.appId, e.toString());
}
}
}
if (errors.isNotEmpty) {
String finalError = '';
for (var e in errors.keys) {
finalError +=
'$e ${errors[e]!.map((e) => apps[e]!.app.name).toString()}. ';
}
throw finalError;
}
return downloadedFiles.map((e) => e.appId).toList();
return downloadedFiles.map((e) => e!.appId).toList();
}
Future<Directory> getAppsDir() async {
@ -248,16 +352,83 @@ class AppsProvider with ChangeNotifier {
return appsDir;
}
// Delete all stored APKs except those likely to still be needed
Future<void> deleteSavedAPKs() async {
(await getExternalStorageDirectory())
List<FileSystemEntity>? apks = (await getExternalStorageDirectory())
?.listSync()
.where((element) => element.path.endsWith('.apk'))
.forEach((element) {
element.deleteSync();
});
.toList();
if (apks != null && apks.isNotEmpty) {
for (var apk in apks) {
var shouldDelete = true;
var temp = apk.path.split('/').last;
temp = temp.substring(0, temp.length - 4);
var fn = temp.split('-');
if (fn.length == 3) {
var possibleId = fn[0];
var possibleVersion = fn[1];
var possibleApkUrlIndex = fn[2];
if (apps[possibleId] != null) {
if (apps[possibleId] != null &&
apps[possibleId]?.app != null &&
apps[possibleId]!.app.installedVersion !=
apps[possibleId]!.app.latestVersion &&
apps[possibleId]!.app.latestVersion == possibleVersion &&
apps[possibleId]!.app.preferredApkIndex.toString() ==
possibleApkUrlIndex) {
shouldDelete = false;
}
}
}
if (shouldDelete) apk.delete();
}
}
}
Future<void> loadApps() async {
Future<AppInfo?> getInstalledInfo(String? packageName) async {
if (packageName != null) {
try {
return await InstalledApps.getAppInfo(packageName);
} catch (e) {
// OK
}
}
return null;
}
String standardizeVersionString(String versionString) {
return versionString.characters
.where((p0) => ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.']
.contains(p0))
.join('');
}
// If the App says it is installed by 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)
App? correctInstallStatus(App app, AppInfo? installedInfo) {
var modded = false;
if (installedInfo == null && app.installedVersion != null) {
app.installedVersion = null;
modded = true;
}
if (installedInfo != null && app.installedVersion == null) {
if (standardizeVersionString(app.latestVersion) ==
installedInfo.versionName) {
app.installedVersion = app.latestVersion;
} else {
app.installedVersion = installedInfo.versionName;
}
modded = true;
}
return modded ? app : null;
}
Future<void> loadApps({shouldCorrectInstallStatus = true}) async {
while (loadingApps) {
await Future.delayed(const Duration(microseconds: 1));
}
loadingApps = true;
notifyListeners();
List<FileSystemEntity> appFiles = (await getAppsDir())
@ -265,22 +436,54 @@ class AppsProvider with ChangeNotifier {
.where((item) => item.path.toLowerCase().endsWith('.json'))
.toList();
apps.clear();
var sp = SourceProvider();
List<List<String>> errors = [];
for (int i = 0; i < appFiles.length; i++) {
App app =
App.fromJson(jsonDecode(File(appFiles[i].path).readAsStringSync()));
apps.putIfAbsent(app.id, () => AppInMemory(app, null));
var info = await getInstalledInfo(app.id);
try {
sp.getSource(app.url);
apps.putIfAbsent(app.id, () => AppInMemory(app, null, info));
} catch (e) {
errors.add([app.id, app.name, e.toString()]);
}
}
if (errors.isNotEmpty) {
removeApps(errors.map((e) => e[0]).toList());
NotificationsProvider().notify(
AppsRemovedNotification(errors.map((e) => [e[1], e[2]]).toList()));
}
loadingApps = false;
notifyListeners();
// For any that are not installed (by ID == package name), set to not installed if needed
if (shouldCorrectInstallStatus) {
List<App> modifiedApps = [];
for (var app in apps.values) {
var moddedApp = correctInstallStatus(app.app, app.installedInfo);
if (moddedApp != null) {
modifiedApps.add(moddedApp);
}
}
if (modifiedApps.isNotEmpty) {
await saveApps(modifiedApps, shouldCorrectInstallStatus: false);
}
}
}
Future<void> saveApps(List<App> apps) async {
Future<void> saveApps(List<App> apps,
{bool shouldCorrectInstallStatus = true}) async {
for (var app in apps) {
AppInfo? info = await getInstalledInfo(app.id);
app.name = info?.name ?? app.name;
if (shouldCorrectInstallStatus) {
app = correctInstallStatus(app, info) ?? app;
}
File('${(await getAppsDir()).path}/${app.id}.json')
.writeAsStringSync(jsonEncode(app.toJson()));
this.apps.update(
app.id, (value) => AppInMemory(app, value.downloadProgress),
ifAbsent: () => AppInMemory(app, null));
app.id, (value) => AppInMemory(app, value.downloadProgress, info),
ifAbsent: () => AppInMemory(app, null, info));
}
notifyListeners();
}
@ -307,56 +510,73 @@ class AppsProvider with ChangeNotifier {
return app.latestVersion != apps[app.id]?.app.installedVersion;
}
Future<App?> getUpdate(String appId) async {
Future<App?> getUpdate(String appId,
{bool shouldCorrectInstallStatus = true}) async {
App? currentApp = apps[appId]!.app;
SourceProvider sourceProvider = SourceProvider();
App newApp = await sourceProvider.getApp(
sourceProvider.getSource(currentApp.url),
currentApp.url,
currentApp.additionalData,
customName: currentApp.name);
name: currentApp.name,
id: currentApp.id);
newApp.installedVersion = currentApp.installedVersion;
if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
newApp.preferredApkIndex = currentApp.preferredApkIndex;
}
await saveApps([newApp]);
await saveApps([newApp],
shouldCorrectInstallStatus: shouldCorrectInstallStatus);
return newApp.latestVersion != currentApp.latestVersion ? newApp : null;
}
Future<List<App>> checkUpdates({DateTime? ignoreAfter}) async {
Future<List<App>> checkUpdates(
{DateTime? ignoreAfter,
bool immediatelyThrowRateLimitError = false,
bool shouldCorrectInstallStatus = true,
bool immediatelyThrowSocketError = false}) async {
List<App> updates = [];
Map<String, List<String>> errors = {};
if (!gettingUpdates) {
gettingUpdates = true;
List<String> appIds = apps.keys.toList();
if (ignoreAfter != null) {
appIds = appIds
.where((id) =>
apps[id]!.app.lastUpdateCheck == null ||
apps[id]!.app.lastUpdateCheck!.isBefore(ignoreAfter))
.toList();
}
appIds.sort((a, b) => (apps[a]!.app.lastUpdateCheck ??
DateTime.fromMicrosecondsSinceEpoch(0))
.compareTo(apps[b]!.app.lastUpdateCheck ??
DateTime.fromMicrosecondsSinceEpoch(0)));
try {
List<String> appIds = apps.keys.toList();
if (ignoreAfter != null) {
appIds = appIds
.where((id) =>
apps[id]!.app.lastUpdateCheck == null ||
apps[id]!.app.lastUpdateCheck!.isBefore(ignoreAfter))
.toList();
}
appIds.sort((a, b) => (apps[a]!.app.lastUpdateCheck ??
DateTime.fromMicrosecondsSinceEpoch(0))
.compareTo(apps[b]!.app.lastUpdateCheck ??
DateTime.fromMicrosecondsSinceEpoch(0)));
for (int i = 0; i < appIds.length; i++) {
App? newApp;
try {
newApp = await getUpdate(appIds[i]);
} catch (e) {
var tempIds = errors.remove(e.toString());
tempIds ??= [];
tempIds.add(appIds[i]);
errors.putIfAbsent(e.toString(), () => tempIds!);
}
if (newApp != null) {
updates.add(newApp);
for (int i = 0; i < appIds.length; i++) {
App? newApp;
try {
newApp = await getUpdate(appIds[i],
shouldCorrectInstallStatus: shouldCorrectInstallStatus);
} catch (e) {
if (e is RateLimitError && immediatelyThrowRateLimitError) {
rethrow;
}
if (e is SocketException && immediatelyThrowSocketError) {
rethrow;
}
var tempIds = errors.remove(e.toString());
tempIds ??= [];
tempIds.add(appIds[i]);
errors.putIfAbsent(e.toString(), () => tempIds!);
}
if (newApp != null) {
updates.add(newApp);
}
}
} finally {
gettingUpdates = false;
}
gettingUpdates = false;
}
if (errors.isNotEmpty) {
String finalError = '';
@ -407,9 +627,13 @@ class AppsProvider with ChangeNotifier {
List<App> importedApps = (jsonDecode(appsJSON) as List<dynamic>)
.map((e) => App.fromJson(e))
.toList();
while (loadingApps) {
await Future.delayed(const Duration(microseconds: 1));
}
for (App a in importedApps) {
a.installedVersion =
apps.containsKey(a.id) ? apps[a]?.app.installedVersion : null;
if (apps[a.id]?.app.installedVersion != null) {
a.installedVersion = apps[a.id]?.app.installedVersion;
}
}
await saveApps(importedApps);
notifyListeners();
@ -418,7 +642,7 @@ class AppsProvider with ChangeNotifier {
@override
void dispose() {
foregroundSubscription.cancel();
foregroundSubscription?.cancel();
super.dispose();
}
}

View File

@ -61,6 +61,24 @@ class ErrorCheckingUpdatesNotification extends ObtainiumNotification {
Importance.high);
}
class AppsRemovedNotification extends ObtainiumNotification {
AppsRemovedNotification(List<List<String>> namedReasons)
: super(
6,
'Apps Removed',
'',
'APPS_REMOVED',
'Apps Removed',
'Notifies the user that one or more Apps were removed due to errors while loading them',
Importance.max) {
message = '';
for (var r in namedReasons) {
message += '${r[0]} was removed due to this error: ${r[1]}. \n';
}
message = message.trim();
}
}
final completeInstallationNotification = ObtainiumNotification(
1,
'Complete App Installation',

View File

@ -4,7 +4,6 @@
import 'dart:convert';
import 'package:html/dom.dart';
import 'package:obtainium/app_sources/apkmirror.dart';
import 'package:obtainium/app_sources/fdroid.dart';
import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/app_sources/gitlab.dart';
@ -54,7 +53,7 @@ class App {
@override
String toString() {
return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls';
return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALDATA: ${additionalData.toString()} LASTCHECK: ${lastUpdateCheck.toString()}';
}
factory App.fromJson(Map<String, dynamic> json) => App(
@ -157,7 +156,7 @@ class SourceProvider {
Mullvad(),
Signal(),
SourceForge(),
APKMirror()
// APKMirror()
];
// Add more mass source classes here so they are available via the service
@ -189,18 +188,21 @@ class SourceProvider {
return false;
}
String generateTempID(AppNames names, AppSource source) =>
'${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}';
Future<App> getApp(AppSource source, String url, List<String> additionalData,
{String customName = ''}) async {
{String name = '', String? id}) async {
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
AppNames names = source.getAppNames(standardUrl);
APKDetails apk =
await source.getLatestAPKDetails(standardUrl, additionalData);
return App(
'${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}',
id ?? generateTempID(names, source),
standardUrl,
names.author[0].toUpperCase() + names.author.substring(1),
customName.trim().isNotEmpty
? customName
name.trim().isNotEmpty
? name
: names.name[0].toUpperCase() + names.name.substring(1),
null,
apk.version,

View File

@ -14,7 +14,7 @@ packages:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.1"
version: "3.3.2"
args:
dependency: transitive
description:
@ -112,42 +112,14 @@ packages:
name: device_info_plus
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.5"
device_info_plus_linux:
dependency: transitive
description:
name: device_info_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.2"
device_info_plus_macos:
dependency: transitive
description:
name: device_info_plus_macos
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.2"
version: "8.0.0"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.1"
device_info_plus_web:
dependency: transitive
description:
name: device_info_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.2"
device_info_plus_windows:
dependency: transitive
description:
name: device_info_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.2"
version: "7.0.0"
dynamic_color:
dependency: "direct main"
description:
@ -182,7 +154,7 @@ packages:
name: file_picker
url: "https://pub.dartlang.org"
source: hosted
version: "5.2.0+1"
version: "5.2.2"
flutter:
dependency: "direct main"
description: flutter
@ -215,14 +187,14 @@ packages:
name: flutter_local_notifications
url: "https://pub.dartlang.org"
source: hosted
version: "12.0.0"
version: "12.0.3"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "2.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
@ -253,14 +225,14 @@ packages:
name: fluttertoast
url: "https://pub.dartlang.org"
source: hosted
version: "8.0.9"
version: "8.1.1"
html:
dependency: "direct main"
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.15.0"
version: "0.15.1"
http:
dependency: "direct main"
description:
@ -274,14 +246,14 @@ packages:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.1"
version: "4.0.2"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
version: "3.2.2"
install_plugin_v2:
dependency: "direct main"
description:
@ -289,6 +261,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
installed_apps:
dependency: "direct main"
description:
name: installed_apps
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
js:
dependency: transitive
description:
@ -309,7 +288,7 @@ packages:
name: lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.0.1"
matcher:
dependency: transitive
description:
@ -323,7 +302,7 @@ packages:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
version: "0.1.5"
meta:
dependency: transitive
description:
@ -345,6 +324,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
package_archive_info:
dependency: "direct main"
description:
name: package_archive_info
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
package_info:
dependency: transitive
description:
name: package_info
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
path:
dependency: transitive
description:
@ -407,21 +400,21 @@ packages:
name: permission_handler
url: "https://pub.dartlang.org"
source: hosted
version: "10.1.0"
version: "10.2.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
url: "https://pub.dartlang.org"
source: hosted
version: "10.1.0"
version: "10.2.0"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
url: "https://pub.dartlang.org"
source: hosted
version: "9.0.6"
version: "9.0.7"
permission_handler_platform_interface:
dependency: transitive
description:
@ -435,14 +428,14 @@ packages:
name: permission_handler_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1"
version: "0.1.2"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
version: "5.1.0"
platform:
dependency: transitive
description:
@ -470,49 +463,21 @@ packages:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.3"
version: "6.0.4"
share_plus:
dependency: "direct main"
description:
name: share_plus
url: "https://pub.dartlang.org"
source: hosted
version: "4.5.3"
share_plus_linux:
dependency: transitive
description:
name: share_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
share_plus_macos:
dependency: transitive
description:
name: share_plus_macos
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "6.1.0"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.1"
share_plus_web:
dependency: transitive
description:
name: share_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
share_plus_windows:
dependency: transitive
description:
name: share_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.2.0"
shared_preferences:
dependency: "direct main"
description:
@ -526,7 +491,7 @@ packages:
name: shared_preferences_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.13"
version: "2.0.14"
shared_preferences_ios:
dependency: transitive
description:
@ -580,7 +545,7 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.1"
version: "1.9.0"
stack_trace:
dependency: transitive
description:
@ -594,7 +559,7 @@ packages:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
version: "2.1.0"
string_scanner:
dependency: transitive
description:
@ -615,7 +580,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.14"
version: "0.4.12"
timezone:
dependency: transitive
description:
@ -643,7 +608,7 @@ packages:
name: url_launcher_android
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.19"
version: "6.0.21"
url_launcher_ios:
dependency: transitive
description:
@ -686,13 +651,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
uuid:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.6"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
version: "2.1.2"
webview_flutter:
dependency: "direct main"
description:
@ -727,14 +699,14 @@ packages:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
version: "3.0.1"
workmanager:
dependency: "direct main"
description:
name: workmanager
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
version: "0.5.1"
xdg_directories:
dependency: transitive
description:
@ -757,5 +729,5 @@ packages:
source: hosted
version: "3.1.1"
sdks:
dart: ">=2.19.0-79.0.dev <3.0.0"
dart: ">=2.18.2 <3.0.0"
flutter: ">=3.3.0"

View File

@ -17,10 +17,10 @@ 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.5.9+30 # When changing this, update the tag in main() accordingly
version: 0.6.1+45 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=2.19.0-79.0.dev <3.0.0'
sdk: '>=2.18.2 <3.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
@ -49,11 +49,13 @@ dependencies:
url_launcher: ^6.1.5
permission_handler: ^10.0.0
fluttertoast: ^8.0.9
device_info_plus: ^5.0.5
device_info_plus: ^8.0.0
file_picker: ^5.1.0
animations: ^2.0.4
install_plugin_v2: ^1.0.0
share_plus: ^4.4.0
share_plus: ^6.0.1
installed_apps: ^1.3.1
package_archive_info: ^0.1.0
dev_dependencies: