diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index abd718e1..2ca2fb11 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -4,10 +4,11 @@
-
-
-
+
+
+
+
-
> getContributors(String org, repoName) async {
try {
- var contributors = await github.repositories.listContributors(
+ var contributors = github.repositories.listContributors(
RepositorySlug(org, repoName),
);
return contributors.toList();
} on Exception {
- print(Exception);
- return [];
+ return List.empty();
}
}
}
diff --git a/lib/services/patcher_api.dart b/lib/services/patcher_api.dart
index cd6e11ea..d201bc88 100644
--- a/lib/services/patcher_api.dart
+++ b/lib/services/patcher_api.dart
@@ -32,11 +32,8 @@ class PatcherAPI {
Future handlePlatformChannelMethods() async {
platform.setMethodCallHandler((call) async {
- switch (call.method) {
- case 'updateInstallerLog':
- var message = call.arguments('message');
- locator().addLog(message);
- return 'OK';
+ if (call.method == 'updateInstallerLog' && call.arguments != null) {
+ locator().addLog(call.arguments);
}
});
}
@@ -298,4 +295,17 @@ class PatcherAPI {
return false;
}
}
+
+ Future checkOldPatch(PatchedApplication patchedApp) async {
+ if (patchedApp.isRooted) {
+ return await rootAPI.checkApp(patchedApp.packageName);
+ }
+ return false;
+ }
+
+ Future deleteOldPatch(PatchedApplication patchedApp) async {
+ if (patchedApp.isRooted) {
+ await rootAPI.deleteApp(patchedApp.packageName, patchedApp.apkFilePath);
+ }
+ }
}
diff --git a/lib/services/root_api.dart b/lib/services/root_api.dart
index 36a8bca7..d7a4eed3 100644
--- a/lib/services/root_api.dart
+++ b/lib/services/root_api.dart
@@ -1,5 +1,3 @@
-import 'dart:io';
-
import 'package:injectable/injectable.dart';
import 'package:root/root.dart';
@@ -9,17 +7,35 @@ class RootAPI {
final String postFsDataDirPath = "/data/adb/post-fs-data.d";
final String serviceDDirPath = "/data/adb/service.d";
- bool deleteApp(String packageName) {
+ Future checkApp(String packageName) async {
try {
- File('$managerDirPath/$packageName.apk').deleteSync();
- File('$serviceDDirPath/$packageName.sh').deleteSync();
- File('$postFsDataDirPath/$packageName.sh').deleteSync();
- return true;
+ String? res = await Root.exec(
+ cmd: 'ls -la "$managerDirPath/$packageName"',
+ );
+ return res != null && res.isNotEmpty;
} on Exception {
return false;
}
}
+ Future deleteApp(String packageName, String originalFilePath) async {
+ await Root.exec(
+ cmd: 'am force-stop "$packageName"',
+ );
+ await Root.exec(
+ cmd: 'su -mm -c "umount -l $originalFilePath"',
+ );
+ await Root.exec(
+ cmd: 'rm -rf "$managerDirPath/$packageName"',
+ );
+ await Root.exec(
+ cmd: 'rm -rf "$serviceDDirPath/$packageName.sh"',
+ );
+ await Root.exec(
+ cmd: 'rm -rf "$postFsDataDirPath/$packageName.sh"',
+ );
+ }
+
Future installApp(
String packageName,
String originalFilePath,
@@ -27,66 +43,76 @@ class RootAPI {
) async {
try {
await Root.exec(
- cmd: 'mkdir "$managerDirPath"',
- );
- String newPatchedFilePath = '$managerDirPath/$packageName.apk';
- installServiceDScript(
- packageName,
- originalFilePath,
- newPatchedFilePath,
- );
- installPostFsDataScript(
- packageName,
- originalFilePath,
- newPatchedFilePath,
- );
- await Root.exec(
- cmd: 'cp $patchedFilePath $newPatchedFilePath',
- );
- await Root.exec(
- cmd: 'chmod 644 "$newPatchedFilePath"',
- );
- await Root.exec(
- cmd: 'chown system:system "$newPatchedFilePath"',
- );
- await Root.exec(
- cmd: 'chcon u:object_r:apk_data_file:s0 "$newPatchedFilePath"',
+ cmd: 'mkdir -p "$managerDirPath/$packageName"',
);
+ installServiceDScript(packageName);
+ installPostFsDataScript(packageName);
+ installApk(packageName, patchedFilePath);
+ mountApk(packageName, originalFilePath, patchedFilePath);
return true;
} on Exception {
return false;
}
}
- Future installServiceDScript(
- String packageName,
- String originalFilePath,
- String patchedFilePath,
- ) async {
+ Future installServiceDScript(String packageName) async {
String content = '#!/system/bin/sh\n'
- 'while [ "\$(getprop sys.boot_completed | tr -d \'\r\')" != "1" ]; do sleep 1; done\n'
- 'sleep 1\n'
- 'chcon u:object_r:apk_data_file:s0 $patchedFilePath\n'
- 'mount -o bind $patchedFilePath $originalFilePath';
+ 'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 1; done\n'
+ 'base_path=$managerDirPath/$packageName/base.apk\n'
+ 'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
+ '[ ! -z \$stock_path ] && mount -o bind \$base_path \$stock_path';
String scriptFilePath = '$serviceDDirPath/$packageName.sh';
await Root.exec(
- cmd: 'echo "$content" > "$scriptFilePath"',
+ cmd: 'echo \'$content\' > "$scriptFilePath"',
+ );
+ await Root.exec(
+ cmd: 'chmod 744 "$scriptFilePath"',
);
- await Root.exec(cmd: 'chmod 744 "$scriptFilePath"');
}
- Future installPostFsDataScript(
+ Future installPostFsDataScript(String packageName) async {
+ String content = '#!/system/bin/sh\n'
+ 'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
+ '[ ! -z \$stock_path ] && umount -l \$stock_path';
+ String scriptFilePath = '$postFsDataDirPath/$packageName.sh';
+ await Root.exec(
+ cmd: 'echo \'$content\' > "$scriptFilePath"',
+ );
+ await Root.exec(
+ cmd: 'chmod 744 "$scriptFilePath"',
+ );
+ }
+
+ Future installApk(String packageName, String patchedFilePath) async {
+ String newPatchedFilePath = '$managerDirPath/$packageName/base.apk';
+ await Root.exec(
+ cmd: 'cp "$patchedFilePath" "$newPatchedFilePath"',
+ );
+ await Root.exec(
+ cmd: 'chmod 644 "$newPatchedFilePath"',
+ );
+ await Root.exec(
+ cmd: 'chown system:system "$newPatchedFilePath"',
+ );
+ await Root.exec(
+ cmd: 'chcon u:object_r:apk_data_file:s0 "$newPatchedFilePath"',
+ );
+ }
+
+ Future mountApk(
String packageName,
String originalFilePath,
String patchedFilePath,
) async {
- String content = '#!/system/bin/sh\n'
- 'while read line; do echo \$line | grep $originalFilePath | '
- 'awk \'{print \$2}\' | xargs umount -l; done< /proc/mounts';
- String scriptFilePath = '$postFsDataDirPath/$packageName.sh';
+ String newPatchedFilePath = '$managerDirPath/$packageName/base.apk';
await Root.exec(
- cmd: 'echo "$content" > "$scriptFilePath"',
+ cmd: 'am force-stop "$packageName"',
+ );
+ await Root.exec(
+ cmd: 'su -mm -c "umount -l $originalFilePath"',
+ );
+ await Root.exec(
+ cmd: 'su -mm -c "mount -o bind $newPatchedFilePath $originalFilePath"',
);
- await Root.exec(cmd: 'chmod 744 $scriptFilePath');
}
}
diff --git a/lib/ui/views/installer/installer_view.dart b/lib/ui/views/installer/installer_view.dart
index a90c7058..f915a568 100644
--- a/lib/ui/views/installer/installer_view.dart
+++ b/lib/ui/views/installer/installer_view.dart
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
-import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart';
@@ -18,130 +17,89 @@ class InstallerView extends StatelessWidget {
disposeViewModel: false,
onModelReady: (model) => model.initialize(),
viewModelBuilder: () => locator(),
- builder: (context, model, child) => WillStartForegroundTask(
- onWillStart: () async => model.isPatching,
- androidNotificationOptions: AndroidNotificationOptions(
- channelId: 'revanced-patcher-patching',
- channelName: 'Patching',
- channelDescription: 'This notification appears when the patching '
- 'foreground service is running.',
- channelImportance: NotificationChannelImportance.LOW,
- priority: NotificationPriority.LOW,
- ),
- notificationTitle: 'Patching',
- notificationText: 'ReVanced Manager is patching',
- callback: () => {},
- child: WillPopScope(
- child: Scaffold(
- body: SafeArea(
- child: LayoutBuilder(
- builder: (context, constraints) => SingleChildScrollView(
- padding: const EdgeInsets.symmetric(horizontal: 12),
- controller: _controller,
- child: ConstrainedBox(
- constraints: BoxConstraints(
- minWidth: constraints.maxWidth,
- minHeight: constraints.maxHeight,
+ builder: (context, model, child) => WillPopScope(
+ child: Scaffold(
+ floatingActionButton: Visibility(
+ visible: model.showButtons,
+ child: FloatingActionButton.extended(
+ onPressed: () =>
+ model.isInstalled ? model.openApp() : model.installResult(),
+ label: I18nText(model.isInstalled
+ ? 'installerView.fabOpenButton'
+ : 'installerView.fabInstallButton'),
+ icon: model.isInstalled
+ ? const Icon(Icons.open_in_new)
+ : const Icon(Icons.install_mobile),
+ backgroundColor: Theme.of(context).colorScheme.secondary,
+ foregroundColor: Colors.white,
+ ),
+ ),
+ body: SafeArea(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(horizontal: 12),
+ controller: _controller,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ I18nText(
+ 'installerView.widgetTitle',
+ child: Text(
+ '',
+ style: Theme.of(context).textTheme.headline5,
+ ),
+ ),
+ Visibility(
+ visible: model.showButtons,
+ child: IconButton(
+ icon: const Icon(Icons.share),
+ onPressed: () => model.shareResult(),
+ ),
+ ),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: 16.0,
+ horizontal: 4.0,
),
- child: IntrinsicHeight(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- I18nText(
- 'installerView.widgetTitle',
- child: Text(
- '',
- style: Theme.of(context).textTheme.headline5,
- ),
- ),
- Padding(
- padding: const EdgeInsets.symmetric(
- vertical: 16.0,
- horizontal: 4.0,
- ),
- child: LinearProgressIndicator(
- color: Theme.of(context).colorScheme.secondary,
- backgroundColor: Colors.white,
- value: model.progress,
- ),
- ),
- Container(
- padding: const EdgeInsets.all(12.0),
- width: double.infinity,
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.primary,
- borderRadius: BorderRadius.circular(8),
- ),
- child: SelectableText(
- model.logs,
- style: const TextStyle(
- fontFamily: 'monospace',
- fontSize: 15,
- height: 1.5,
- ),
- ),
- ),
- const Spacer(),
- Visibility(
- visible: model.showButtons,
- child: Row(
- children: [
- Expanded(
- child: MaterialButton(
- textColor: Colors.white,
- color:
- Theme.of(context).colorScheme.secondary,
- padding: const EdgeInsets.symmetric(
- vertical: 12,
- horizontal: 8,
- ),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- onPressed: () => model.installResult(),
- child: I18nText(
- 'installerView.installButton',
- ),
- ),
- ),
- const SizedBox(width: 12),
- Expanded(
- child: MaterialButton(
- textColor: Colors.white,
- color:
- Theme.of(context).colorScheme.secondary,
- padding: const EdgeInsets.symmetric(
- vertical: 12,
- horizontal: 8,
- ),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- onPressed: () => model.shareResult(),
- child: I18nText(
- 'installerView.shareButton',
- ),
- ),
- ),
- ],
- ),
- ),
- ],
+ child: LinearProgressIndicator(
+ color: Theme.of(context).colorScheme.secondary,
+ backgroundColor: Colors.white,
+ value: model.progress,
+ ),
+ ),
+ Container(
+ padding: const EdgeInsets.all(12.0),
+ width: double.infinity,
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.primary,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: SelectableText(
+ model.logs,
+ style: const TextStyle(
+ fontFamily: 'monospace',
+ fontSize: 15,
+ height: 1.5,
),
),
),
- ),
+ ],
),
),
),
- onWillPop: () async {
- if (!model.isPatching) {
- model.cleanWorkplace();
- Navigator.of(context).pop();
- }
- return false;
- },
),
+ onWillPop: () async {
+ if (!model.isPatching) {
+ model.cleanWorkplace();
+ Navigator.of(context).pop();
+ }
+ return false;
+ },
),
);
}
diff --git a/lib/ui/views/installer/installer_viewmodel.dart b/lib/ui/views/installer/installer_viewmodel.dart
index d2203988..c9e5ab28 100644
--- a/lib/ui/views/installer/installer_viewmodel.dart
+++ b/lib/ui/views/installer/installer_viewmodel.dart
@@ -1,3 +1,5 @@
+import 'package:device_apps/device_apps.dart';
+import 'package:flutter_background/flutter_background.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart';
@@ -11,9 +13,22 @@ class InstallerViewModel extends BaseViewModel {
double? progress = 0.2;
String logs = '';
bool isPatching = false;
+ bool isInstalled = false;
bool showButtons = false;
Future initialize() async {
+ await FlutterBackground.initialize(
+ androidConfig: const FlutterBackgroundAndroidConfig(
+ notificationTitle: 'Patching',
+ notificationText: 'ReVanced Manager is patching',
+ notificationImportance: AndroidNotificationImportance.Default,
+ notificationIcon: AndroidResource(
+ name: 'ic_launcher_foreground',
+ defType: 'drawable',
+ ),
+ ),
+ );
+ await FlutterBackground.enableBackgroundExecution();
await locator().handlePlatformChannelMethods();
runPatcher();
}
@@ -28,6 +43,7 @@ class InstallerViewModel extends BaseViewModel {
void updateProgress(double value) {
progress = value;
+ isInstalled = false;
isPatching = progress == 1.0 ? false : true;
showButtons = progress == 1.0 ? true : false;
if (progress == 0.0) {
@@ -46,6 +62,18 @@ class InstallerViewModel extends BaseViewModel {
locator().selectedPatches;
if (selectedPatches.isNotEmpty) {
addLog('Initializing installer...');
+ if (selectedApp.isRooted) {
+ addLog('Checking if an old patched version exists...');
+ bool oldExists =
+ await locator().checkOldPatch(selectedApp);
+ addLog('Done');
+ if (oldExists) {
+ addLog('Deleting old patched version...');
+ await locator().deleteOldPatch(selectedApp);
+ addLog('Done');
+ }
+ }
+ addLog('Creating working directory...');
bool? isSuccess = await locator().initPatcher();
if (isSuccess != null && isSuccess) {
addLog('Done');
@@ -108,6 +136,7 @@ class InstallerViewModel extends BaseViewModel {
} else {
addLog('No app selected! Aborting...');
}
+ await FlutterBackground.disableBackgroundExecution();
isPatching = false;
}
@@ -118,9 +147,8 @@ class InstallerViewModel extends BaseViewModel {
addLog(selectedApp.isRooted
? 'Installing patched file using root method...'
: 'Installing patched file using nonroot method...');
- bool isSucess =
- await locator().installPatchedFile(selectedApp);
- if (isSucess) {
+ isInstalled = await locator().installPatchedFile(selectedApp);
+ if (isInstalled) {
addLog('Done');
} else {
addLog('An error occurred! Aborting...');
@@ -139,10 +167,18 @@ class InstallerViewModel extends BaseViewModel {
}
}
- void cleanWorkplace() {
+ Future cleanWorkplace() async {
locator().cleanPatcher();
locator().selectedApp = null;
locator().selectedPatches.clear();
locator().notifyListeners();
}
+
+ void openApp() {
+ PatchedApplication? selectedApp =
+ locator().selectedApp;
+ if (selectedApp != null) {
+ DeviceApps.openApp(selectedApp.packageName);
+ }
+ }
}
diff --git a/lib/ui/views/patches_selector/patches_selector_viewmodel.dart b/lib/ui/views/patches_selector/patches_selector_viewmodel.dart
index df1bf875..1bb79e68 100644
--- a/lib/ui/views/patches_selector/patches_selector_viewmodel.dart
+++ b/lib/ui/views/patches_selector/patches_selector_viewmodel.dart
@@ -25,11 +25,13 @@ class PatchesSelectorViewModel extends BaseViewModel {
void selectPatches(List patchItems) {
selectedPatches.clear();
if (patches != null) {
- for (PatchItem patch in patchItems) {
- if (patch.isSelected) {
- selectedPatches.add(
- patches!.firstWhere((element) => element.name == patch.name),
- );
+ for (PatchItem item in patchItems) {
+ if (item.isSelected) {
+ Patch patch =
+ patches!.firstWhere((element) => element.name == item.name);
+ if (!selectedPatches.contains(patch)) {
+ selectedPatches.add(patch);
+ }
}
}
}
diff --git a/lib/ui/widgets/patch_selector_card.dart b/lib/ui/widgets/patch_selector_card.dart
index 5d3b7dcb..98eb0635 100644
--- a/lib/ui/widgets/patch_selector_card.dart
+++ b/lib/ui/widgets/patch_selector_card.dart
@@ -43,7 +43,7 @@ class PatchSelectorCard extends StatelessWidget {
const SizedBox(height: 10),
locator().selectedApp == null
? I18nText(
- 'patchSelectorCard.widgetFirstSubtitle',
+ 'patchSelectorCard.widgetSubtitle',
child: Text(
'',
style: robotoTextStyle,
@@ -51,24 +51,18 @@ class PatchSelectorCard extends StatelessWidget {
)
: locator().selectedPatches.isEmpty
? I18nText(
- 'patchSelectorCard.widgetSecondSubtitle',
+ 'patchSelectorCard.widgetEmptySubtitle',
child: Text(
'',
style: robotoTextStyle,
),
)
- : I18nText(
- 'patchSelectorCard.widgetThirdSubtitle',
- translationParams: {
- 'selected': locator()
- .selectedPatches
- .length
- .toString()
- },
- child: Text(
- '',
- style: robotoTextStyle,
- ),
+ : Text(
+ locator()
+ .selectedPatches
+ .map((e) => e.simpleName)
+ .toList()
+ .join('\n'),
),
],
),
diff --git a/pubspec.yaml b/pubspec.yaml
index 86a98033..bb504833 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -17,8 +17,8 @@ dependencies:
file_picker: ^5.0.1
flutter:
sdk: flutter
+ flutter_background: ^1.1.0
flutter_cache_manager: ^3.3.0
- flutter_foreground_task: ^3.8.1
flutter_i18n: ^0.32.4
flutter_svg: ^1.1.1+1
fluttertoast: ^8.0.9