fix: root installation and foreground task and improve installer a bit

This commit is contained in:
Alberto Ponces 2022-08-15 03:31:36 +01:00
parent 8fd942a808
commit 5c71930ec1
10 changed files with 231 additions and 207 deletions

View File

@ -4,10 +4,11 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application <application
android:label="ReVanced Manager" android:label="ReVanced Manager"
android:name="${applicationName}" android:name="${applicationName}"
@ -34,7 +35,6 @@
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<service android:name="com.pravera.flutter_foreground_task.service.ForegroundService" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider" android:authorities="${applicationId}.fileProvider"

View File

@ -38,9 +38,8 @@
}, },
"patchSelectorCard": { "patchSelectorCard": {
"widgetTitle": "Select patches", "widgetTitle": "Select patches",
"widgetFirstSubtitle": "Select an application first.", "widgetSubtitle": "Select an application first.",
"widgetSecondSubtitle": "No patches selected.", "widgetEmptySubtitle": "No patches selected."
"widgetThirdSubtitle": "{selected} patch(es) selected."
}, },
"appSelectorView": { "appSelectorView": {
"searchBarHint": "Search applications", "searchBarHint": "Search applications",
@ -53,8 +52,8 @@
}, },
"installerView": { "installerView": {
"widgetTitle": "Installer", "widgetTitle": "Installer",
"installButton": "Install", "fabInstallButton": "Install",
"shareButton": "Share" "fabOpenButton": "Open"
}, },
"settingsView": { "settingsView": {
"widgetTitle": "Settings", "widgetTitle": "Settings",

View File

@ -40,13 +40,12 @@ class GithubAPI {
Future<List<Contributor>> getContributors(String org, repoName) async { Future<List<Contributor>> getContributors(String org, repoName) async {
try { try {
var contributors = await github.repositories.listContributors( var contributors = github.repositories.listContributors(
RepositorySlug(org, repoName), RepositorySlug(org, repoName),
); );
return contributors.toList(); return contributors.toList();
} on Exception { } on Exception {
print(Exception); return List.empty();
return [];
} }
} }
} }

View File

@ -32,11 +32,8 @@ class PatcherAPI {
Future<dynamic> handlePlatformChannelMethods() async { Future<dynamic> handlePlatformChannelMethods() async {
platform.setMethodCallHandler((call) async { platform.setMethodCallHandler((call) async {
switch (call.method) { if (call.method == 'updateInstallerLog' && call.arguments != null) {
case 'updateInstallerLog': locator<InstallerViewModel>().addLog(call.arguments);
var message = call.arguments<String>('message');
locator<InstallerViewModel>().addLog(message);
return 'OK';
} }
}); });
} }
@ -298,4 +295,17 @@ class PatcherAPI {
return false; return false;
} }
} }
Future<bool> checkOldPatch(PatchedApplication patchedApp) async {
if (patchedApp.isRooted) {
return await rootAPI.checkApp(patchedApp.packageName);
}
return false;
}
Future<void> deleteOldPatch(PatchedApplication patchedApp) async {
if (patchedApp.isRooted) {
await rootAPI.deleteApp(patchedApp.packageName, patchedApp.apkFilePath);
}
}
} }

View File

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:root/root.dart'; import 'package:root/root.dart';
@ -9,17 +7,35 @@ class RootAPI {
final String postFsDataDirPath = "/data/adb/post-fs-data.d"; final String postFsDataDirPath = "/data/adb/post-fs-data.d";
final String serviceDDirPath = "/data/adb/service.d"; final String serviceDDirPath = "/data/adb/service.d";
bool deleteApp(String packageName) { Future<bool> checkApp(String packageName) async {
try { try {
File('$managerDirPath/$packageName.apk').deleteSync(); String? res = await Root.exec(
File('$serviceDDirPath/$packageName.sh').deleteSync(); cmd: 'ls -la "$managerDirPath/$packageName"',
File('$postFsDataDirPath/$packageName.sh').deleteSync(); );
return true; return res != null && res.isNotEmpty;
} on Exception { } on Exception {
return false; return false;
} }
} }
Future<void> 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<bool> installApp( Future<bool> installApp(
String packageName, String packageName,
String originalFilePath, String originalFilePath,
@ -27,66 +43,76 @@ class RootAPI {
) async { ) async {
try { try {
await Root.exec( await Root.exec(
cmd: 'mkdir "$managerDirPath"', cmd: 'mkdir -p "$managerDirPath/$packageName"',
);
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"',
); );
installServiceDScript(packageName);
installPostFsDataScript(packageName);
installApk(packageName, patchedFilePath);
mountApk(packageName, originalFilePath, patchedFilePath);
return true; return true;
} on Exception { } on Exception {
return false; return false;
} }
} }
Future<void> installServiceDScript( Future<void> installServiceDScript(String packageName) async {
String packageName,
String originalFilePath,
String patchedFilePath,
) async {
String content = '#!/system/bin/sh\n' String content = '#!/system/bin/sh\n'
'while [ "\$(getprop sys.boot_completed | tr -d \'\r\')" != "1" ]; do sleep 1; done\n' 'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 1; done\n'
'sleep 1\n' 'base_path=$managerDirPath/$packageName/base.apk\n'
'chcon u:object_r:apk_data_file:s0 $patchedFilePath\n' 'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
'mount -o bind $patchedFilePath $originalFilePath'; '[ ! -z \$stock_path ] && mount -o bind \$base_path \$stock_path';
String scriptFilePath = '$serviceDDirPath/$packageName.sh'; String scriptFilePath = '$serviceDDirPath/$packageName.sh';
await Root.exec( 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<void> installPostFsDataScript( Future<void> 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<void> 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<void> mountApk(
String packageName, String packageName,
String originalFilePath, String originalFilePath,
String patchedFilePath, String patchedFilePath,
) async { ) async {
String content = '#!/system/bin/sh\n' String newPatchedFilePath = '$managerDirPath/$packageName/base.apk';
'while read line; do echo \$line | grep $originalFilePath | '
'awk \'{print \$2}\' | xargs umount -l; done< /proc/mounts';
String scriptFilePath = '$postFsDataDirPath/$packageName.sh';
await Root.exec( 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');
} }
} }

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart'; import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart';
@ -18,130 +17,89 @@ class InstallerView extends StatelessWidget {
disposeViewModel: false, disposeViewModel: false,
onModelReady: (model) => model.initialize(), onModelReady: (model) => model.initialize(),
viewModelBuilder: () => locator<InstallerViewModel>(), viewModelBuilder: () => locator<InstallerViewModel>(),
builder: (context, model, child) => WillStartForegroundTask( builder: (context, model, child) => WillPopScope(
onWillStart: () async => model.isPatching, child: Scaffold(
androidNotificationOptions: AndroidNotificationOptions( floatingActionButton: Visibility(
channelId: 'revanced-patcher-patching', visible: model.showButtons,
channelName: 'Patching', child: FloatingActionButton.extended(
channelDescription: 'This notification appears when the patching ' onPressed: () =>
'foreground service is running.', model.isInstalled ? model.openApp() : model.installResult(),
channelImportance: NotificationChannelImportance.LOW, label: I18nText(model.isInstalled
priority: NotificationPriority.LOW, ? 'installerView.fabOpenButton'
), : 'installerView.fabInstallButton'),
notificationTitle: 'Patching', icon: model.isInstalled
notificationText: 'ReVanced Manager is patching', ? const Icon(Icons.open_in_new)
callback: () => {}, : const Icon(Icons.install_mobile),
child: WillPopScope( backgroundColor: Theme.of(context).colorScheme.secondary,
child: Scaffold( foregroundColor: Colors.white,
body: SafeArea( ),
child: LayoutBuilder( ),
builder: (context, constraints) => SingleChildScrollView( body: SafeArea(
padding: const EdgeInsets.symmetric(horizontal: 12), child: SingleChildScrollView(
controller: _controller, padding: const EdgeInsets.symmetric(horizontal: 12),
child: ConstrainedBox( controller: _controller,
constraints: BoxConstraints( child: Column(
minWidth: constraints.maxWidth, crossAxisAlignment: CrossAxisAlignment.start,
minHeight: constraints.maxHeight, children: <Widget>[
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: LinearProgressIndicator(
child: Column( color: Theme.of(context).colorScheme.secondary,
crossAxisAlignment: CrossAxisAlignment.start, backgroundColor: Colors.white,
children: <Widget>[ value: model.progress,
I18nText( ),
'installerView.widgetTitle', ),
child: Text( Container(
'', padding: const EdgeInsets.all(12.0),
style: Theme.of(context).textTheme.headline5, width: double.infinity,
), decoration: BoxDecoration(
), color: Theme.of(context).colorScheme.primary,
Padding( borderRadius: BorderRadius.circular(8),
padding: const EdgeInsets.symmetric( ),
vertical: 16.0, child: SelectableText(
horizontal: 4.0, model.logs,
), style: const TextStyle(
child: LinearProgressIndicator( fontFamily: 'monospace',
color: Theme.of(context).colorScheme.secondary, fontSize: 15,
backgroundColor: Colors.white, height: 1.5,
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',
),
),
),
],
),
),
],
), ),
), ),
), ),
), ],
), ),
), ),
), ),
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;
},
), ),
); );
} }

View File

@ -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/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
@ -11,9 +13,22 @@ class InstallerViewModel extends BaseViewModel {
double? progress = 0.2; double? progress = 0.2;
String logs = ''; String logs = '';
bool isPatching = false; bool isPatching = false;
bool isInstalled = false;
bool showButtons = false; bool showButtons = false;
Future<void> initialize() async { Future<void> 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<PatcherAPI>().handlePlatformChannelMethods(); await locator<PatcherAPI>().handlePlatformChannelMethods();
runPatcher(); runPatcher();
} }
@ -28,6 +43,7 @@ class InstallerViewModel extends BaseViewModel {
void updateProgress(double value) { void updateProgress(double value) {
progress = value; progress = value;
isInstalled = false;
isPatching = progress == 1.0 ? false : true; isPatching = progress == 1.0 ? false : true;
showButtons = progress == 1.0 ? true : false; showButtons = progress == 1.0 ? true : false;
if (progress == 0.0) { if (progress == 0.0) {
@ -46,6 +62,18 @@ class InstallerViewModel extends BaseViewModel {
locator<PatchesSelectorViewModel>().selectedPatches; locator<PatchesSelectorViewModel>().selectedPatches;
if (selectedPatches.isNotEmpty) { if (selectedPatches.isNotEmpty) {
addLog('Initializing installer...'); addLog('Initializing installer...');
if (selectedApp.isRooted) {
addLog('Checking if an old patched version exists...');
bool oldExists =
await locator<PatcherAPI>().checkOldPatch(selectedApp);
addLog('Done');
if (oldExists) {
addLog('Deleting old patched version...');
await locator<PatcherAPI>().deleteOldPatch(selectedApp);
addLog('Done');
}
}
addLog('Creating working directory...');
bool? isSuccess = await locator<PatcherAPI>().initPatcher(); bool? isSuccess = await locator<PatcherAPI>().initPatcher();
if (isSuccess != null && isSuccess) { if (isSuccess != null && isSuccess) {
addLog('Done'); addLog('Done');
@ -108,6 +136,7 @@ class InstallerViewModel extends BaseViewModel {
} else { } else {
addLog('No app selected! Aborting...'); addLog('No app selected! Aborting...');
} }
await FlutterBackground.disableBackgroundExecution();
isPatching = false; isPatching = false;
} }
@ -118,9 +147,8 @@ class InstallerViewModel extends BaseViewModel {
addLog(selectedApp.isRooted addLog(selectedApp.isRooted
? 'Installing patched file using root method...' ? 'Installing patched file using root method...'
: 'Installing patched file using nonroot method...'); : 'Installing patched file using nonroot method...');
bool isSucess = isInstalled = await locator<PatcherAPI>().installPatchedFile(selectedApp);
await locator<PatcherAPI>().installPatchedFile(selectedApp); if (isInstalled) {
if (isSucess) {
addLog('Done'); addLog('Done');
} else { } else {
addLog('An error occurred! Aborting...'); addLog('An error occurred! Aborting...');
@ -139,10 +167,18 @@ class InstallerViewModel extends BaseViewModel {
} }
} }
void cleanWorkplace() { Future<void> cleanWorkplace() async {
locator<PatcherAPI>().cleanPatcher(); locator<PatcherAPI>().cleanPatcher();
locator<AppSelectorViewModel>().selectedApp = null; locator<AppSelectorViewModel>().selectedApp = null;
locator<PatchesSelectorViewModel>().selectedPatches.clear(); locator<PatchesSelectorViewModel>().selectedPatches.clear();
locator<PatcherViewModel>().notifyListeners(); locator<PatcherViewModel>().notifyListeners();
} }
void openApp() {
PatchedApplication? selectedApp =
locator<AppSelectorViewModel>().selectedApp;
if (selectedApp != null) {
DeviceApps.openApp(selectedApp.packageName);
}
}
} }

View File

@ -25,11 +25,13 @@ class PatchesSelectorViewModel extends BaseViewModel {
void selectPatches(List<PatchItem> patchItems) { void selectPatches(List<PatchItem> patchItems) {
selectedPatches.clear(); selectedPatches.clear();
if (patches != null) { if (patches != null) {
for (PatchItem patch in patchItems) { for (PatchItem item in patchItems) {
if (patch.isSelected) { if (item.isSelected) {
selectedPatches.add( Patch patch =
patches!.firstWhere((element) => element.name == patch.name), patches!.firstWhere((element) => element.name == item.name);
); if (!selectedPatches.contains(patch)) {
selectedPatches.add(patch);
}
} }
} }
} }

View File

@ -43,7 +43,7 @@ class PatchSelectorCard extends StatelessWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
locator<AppSelectorViewModel>().selectedApp == null locator<AppSelectorViewModel>().selectedApp == null
? I18nText( ? I18nText(
'patchSelectorCard.widgetFirstSubtitle', 'patchSelectorCard.widgetSubtitle',
child: Text( child: Text(
'', '',
style: robotoTextStyle, style: robotoTextStyle,
@ -51,24 +51,18 @@ class PatchSelectorCard extends StatelessWidget {
) )
: locator<PatchesSelectorViewModel>().selectedPatches.isEmpty : locator<PatchesSelectorViewModel>().selectedPatches.isEmpty
? I18nText( ? I18nText(
'patchSelectorCard.widgetSecondSubtitle', 'patchSelectorCard.widgetEmptySubtitle',
child: Text( child: Text(
'', '',
style: robotoTextStyle, style: robotoTextStyle,
), ),
) )
: I18nText( : Text(
'patchSelectorCard.widgetThirdSubtitle', locator<PatchesSelectorViewModel>()
translationParams: { .selectedPatches
'selected': locator<PatchesSelectorViewModel>() .map((e) => e.simpleName)
.selectedPatches .toList()
.length .join('\n'),
.toString()
},
child: Text(
'',
style: robotoTextStyle,
),
), ),
], ],
), ),

View File

@ -17,8 +17,8 @@ dependencies:
file_picker: ^5.0.1 file_picker: ^5.0.1
flutter: flutter:
sdk: flutter sdk: flutter
flutter_background: ^1.1.0
flutter_cache_manager: ^3.3.0 flutter_cache_manager: ^3.3.0
flutter_foreground_task: ^3.8.1
flutter_i18n: ^0.32.4 flutter_i18n: ^0.32.4
flutter_svg: ^1.1.1+1 flutter_svg: ^1.1.1+1
fluttertoast: ^8.0.9 fluttertoast: ^8.0.9