feat: Improve installation robustness (#1528)

Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: Ushie <ushiekane@gmail.com>
Co-authored-by: Dhruvan Bhalara <53393418+dhruvanbhalara@users.noreply.github.com>
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
This commit is contained in:
aAbed 2023-12-23 09:01:28 +05:45 committed by GitHub
parent 8b28a33b73
commit c23275f2fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 610 additions and 252 deletions

View File

@ -55,5 +55,22 @@
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<receiver
android:name=".utils.packageInstaller.InstallerReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="APP_INSTALL_ACTION" />
</intent-filter>
</receiver>
<receiver
android:name=".utils.packageInstaller.UninstallerReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="APP_UNINSTALL_ACTION" />
</intent-filter>
</receiver>
</application> </application>
</manifest> </manifest>

View File

@ -1,11 +1,16 @@
package app.revanced.manager.flutter package app.revanced.manager.flutter
import android.app.PendingIntent
import android.app.SearchManager import android.app.SearchManager
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import app.revanced.manager.flutter.utils.Aapt import app.revanced.manager.flutter.utils.Aapt
import app.revanced.manager.flutter.utils.aligning.ZipAligner import app.revanced.manager.flutter.utils.aligning.ZipAligner
import app.revanced.manager.flutter.utils.packageInstaller.InstallerReceiver
import app.revanced.manager.flutter.utils.packageInstaller.UninstallerReceiver
import app.revanced.manager.flutter.utils.signing.Signer import app.revanced.manager.flutter.utils.signing.Signer
import app.revanced.manager.flutter.utils.zip.ZipFile import app.revanced.manager.flutter.utils.zip.ZipFile
import app.revanced.manager.flutter.utils.zip.structures.ZipEntry import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
@ -184,12 +189,24 @@ class MainActivity : FlutterActivity() {
}.toString().let(result::success) }.toString().let(result::success)
} }
"installApk" -> {
val apkPath = call.argument<String>("apkPath")!!
PackageInstallerManager.result = result
installApk(apkPath)
}
"uninstallApp" -> {
val packageName = call.argument<String>("packageName")!!
uninstallApp(packageName)
PackageInstallerManager.result = result
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
} }
fun openBrowser(query: String?) { private fun openBrowser(query: String?) {
val intent = Intent(Intent.ACTION_WEB_SEARCH).apply { val intent = Intent(Intent.ACTION_WEB_SEARCH).apply {
putExtra(SearchManager.QUERY, query) putExtra(SearchManager.QUERY, query)
} }
@ -407,4 +424,44 @@ class MainActivity : FlutterActivity() {
handler.post { result.success(null) } handler.post { result.success(null) }
}.start() }.start()
} }
private fun installApk(apkPath: String) {
val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
val sessionParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val sessionId: Int = packageInstaller.createSession(sessionParams)
val session: PackageInstaller.Session = packageInstaller.openSession(sessionId)
session.use { activeSession ->
val sessionOutputStream = activeSession.openWrite(applicationContext.packageName, 0, -1)
sessionOutputStream.use { outputStream ->
val apkFile = File(apkPath)
apkFile.inputStream().use { inputStream ->
inputStream.copyTo(outputStream)
}
}
}
val receiverIntent = Intent(applicationContext, InstallerReceiver::class.java).apply {
action = "APP_INSTALL_ACTION"
}
val receiverPendingIntent = PendingIntent.getBroadcast(context, sessionId, receiverIntent, PackageInstallerManager.flags)
session.commit(receiverPendingIntent.intentSender)
session.close()
}
private fun uninstallApp(packageName: String) {
val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
val receiverIntent = Intent(applicationContext, UninstallerReceiver::class.java).apply {
action = "APP_UNINSTALL_ACTION"
}
val receiverPendingIntent = PendingIntent.getBroadcast(context, 0, receiverIntent, PackageInstallerManager.flags)
packageInstaller.uninstall(packageName, receiverPendingIntent.intentSender)
}
object PackageInstallerManager {
var result: MethodChannel.Result? = null
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
}
} }

View File

@ -0,0 +1,32 @@
package app.revanced.manager.flutter.utils.packageInstaller
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import app.revanced.manager.flutter.MainActivity
class InstallerReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
if (confirmationIntent != null) {
context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
else -> {
val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
val otherPackageName = intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME)
MainActivity.PackageInstallerManager.result!!.success(mapOf(
"status" to status,
"packageName" to packageName,
"message" to message,
"otherPackageName" to otherPackageName
))
}
}
}
}

View File

@ -0,0 +1,24 @@
package app.revanced.manager.flutter.utils.packageInstaller
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import app.revanced.manager.flutter.MainActivity
class UninstallerReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
if (confirmationIntent != null) {
context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
else -> {
MainActivity.PackageInstallerManager.result!!.success(status)
}
}
}
}

View File

@ -170,6 +170,8 @@
"installRootType": "Mount", "installRootType": "Mount",
"installNonRootType": "Normal", "installNonRootType": "Normal",
"warning": "Disable auto updates after installing the app to avoid unexpected issues.",
"pressBackAgain": "Press back again to cancel", "pressBackAgain": "Press back again to cancel",
"openButton": "Open", "openButton": "Open",
"shareButton": "Share file", "shareButton": "Share file",
@ -327,5 +329,34 @@
"integrationsContributors": "Integrations contributors", "integrationsContributors": "Integrations contributors",
"cliContributors": "CLI contributors", "cliContributors": "CLI contributors",
"managerContributors": "Manager contributors" "managerContributors": "Manager contributors"
},
"installErrorDialog": {
"mount_version_mismatch": "Version mismatch",
"mount_no_root": "No root access",
"mount_missing_installation": "Installation not found",
"status_failure_blocked": "Installation blocked",
"install_failed_verification_failure": "Verification failed",
"status_failure_invalid": "Installation invalid",
"install_failed_version_downgrade": "Can't downgrade",
"status_failure_conflict": "Installation conflict",
"status_failure_storage": "Installation storage issue",
"status_failure_incompatible": "Installation incompatible",
"status_failure_timeout": "Installation timeout",
"status_unknown": "Installation failed",
"mount_version_mismatch_description": "The installation failed due to the installed app being a different version than the patched app.\n\nInstall the version of the app you are mounting and try again.",
"mount_no_root_description": "The installation failed due to root access not being granted.\n\nGrant root access to ReVanced Manager and try again.",
"mount_missing_installation_description": "The installation failed due to the unpatched app not being installed on this device.\n\nInstall the app and try again.",
"status_failure_timeout_description": "The installation took too long to finish.\n\nWould you like to try again?",
"status_failure_storage_description": "The installation failed due to insufficient storage.\n\nFree up some space and try again.",
"status_failure_invalid_description": "The installation failed due to the patched app being invalid.\n\nUninstall the app and try again?",
"status_failure_incompatible_description": "The app is incompatible with this device.\n\nContact the developer of the app and ask for support.",
"status_failure_conflict_description": "The installation was prevented by an existing installation of the app.\n\nUninstall the app and try again?",
"status_failure_blocked_description": "The installation was blocked by {packageName}.\n\nAdjust your security settings and try again.",
"install_failed_verification_failure_description": "The installation failed due to a verification issue.\n\nAdjust your security settings and try again.",
"install_failed_version_downgrade_description": "The installation failed due to the patched app being a lower version than the installed app.\n\nUninstall the app and try again?",
"status_unknown_description": "The installation failed due to an unknown reason. Please try again."
} }
} }

View File

@ -8,6 +8,7 @@ import 'package:revanced_manager/services/download_manager.dart';
import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/revanced_api.dart'; import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart'; import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -24,6 +25,13 @@ Future main() async {
final String repoUrl = locator<ManagerAPI>().getRepoUrl(); final String repoUrl = locator<ManagerAPI>().getRepoUrl();
locator<GithubAPI>().initialize(repoUrl); locator<GithubAPI>().initialize(repoUrl);
tz.initializeTimeZones(); tz.initializeTimeZones();
// TODO(aAbed): remove in the future, keep it for now during migration.
final rootAPI = RootAPI();
if (await rootAPI.hasRootPermissions()) {
await rootAPI.removeOrphanedFiles();
}
prefs = await SharedPreferences.getInstance(); prefs = await SharedPreferences.getInstance();
runApp(const MyApp()); runApp(const MyApp());

View File

@ -72,4 +72,3 @@ class DownloadManager {
); );
} }
} }

View File

@ -3,16 +3,18 @@ import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:device_apps/device_apps.dart'; import 'package:device_apps/device_apps.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart'; import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:install_plugin/install_plugin.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.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';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/root_api.dart'; import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
@lazySingleton @lazySingleton
@ -79,7 +81,8 @@ class PatcherAPI {
} }
Future<List<ApplicationWithIcon>> getFilteredInstalledApps( Future<List<ApplicationWithIcon>> getFilteredInstalledApps(
bool showUniversalPatches,) async { bool showUniversalPatches,
) async {
final List<ApplicationWithIcon> filteredApps = []; final List<ApplicationWithIcon> filteredApps = [];
final bool allAppsIncluded = final bool allAppsIncluded =
_universalPatches.isNotEmpty && showUniversalPatches; _universalPatches.isNotEmpty && showUniversalPatches;
@ -137,22 +140,27 @@ class PatcherAPI {
return filteredPatches[packageName]; return filteredPatches[packageName];
} }
Future<List<Patch>> getAppliedPatches(List<String> appliedPatches,) async { Future<List<Patch>> getAppliedPatches(
List<String> appliedPatches,
) async {
return _patches return _patches
.where((patch) => appliedPatches.contains(patch.name)) .where((patch) => appliedPatches.contains(patch.name))
.toList(); .toList();
} }
Future<void> runPatcher(String packageName, Future<void> runPatcher(
String packageName,
String apkFilePath, String apkFilePath,
List<Patch> selectedPatches,) async { List<Patch> selectedPatches,
) async {
final File? integrationsFile = await _managerAPI.downloadIntegrations(); final File? integrationsFile = await _managerAPI.downloadIntegrations();
final Map<String, Map<String, dynamic>> options = {}; final Map<String, Map<String, dynamic>> options = {};
for (final patch in selectedPatches) { for (final patch in selectedPatches) {
if (patch.options.isNotEmpty) { if (patch.options.isNotEmpty) {
final Map<String, dynamic> patchOptions = {}; final Map<String, dynamic> patchOptions = {};
for (final option in patch.options) { for (final option in patch.options) {
final patchOption = _managerAPI.getPatchOption(packageName, patch.name, option.key); final patchOption =
_managerAPI.getPatchOption(packageName, patch.name, option.key);
if (patchOption != null) { if (patchOption != null) {
patchOptions[patchOption.key] = patchOption.value; patchOptions[patchOption.key] = patchOption.value;
} }
@ -206,30 +214,147 @@ Future<void> stopPatcher() async {
} }
} }
Future<bool> installPatchedFile(PatchedApplication patchedApp) async { Future<int> installPatchedFile(
BuildContext context,
PatchedApplication patchedApp,
) async {
if (outFile != null) { if (outFile != null) {
_managerAPI.ctx = context;
try { try {
if (patchedApp.isRooted) { if (patchedApp.isRooted) {
final bool hasRootPermissions = await _rootAPI.hasRootPermissions(); final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) { final packageVersion = await DeviceApps.getApp(patchedApp.packageName)
return _rootAPI.installApp( .then((app) => app?.versionName);
if (!hasRootPermissions) {
installErrorDialog(1);
} else if (packageVersion == null) {
installErrorDialog(1.2);
} else if (packageVersion == patchedApp.version) {
return await _rootAPI.installApp(
patchedApp.packageName, patchedApp.packageName,
patchedApp.apkFilePath, patchedApp.apkFilePath,
outFile!.path, outFile!.path,
); )
? 0
: 1;
} else {
installErrorDialog(1.1);
} }
} else { } else {
final install = await InstallPlugin.installApk(outFile!.path); if (await _rootAPI.hasRootPermissions()) {
return install['isSuccess']; await _rootAPI.unmount(patchedApp.packageName);
}
if (context.mounted) {
return await installApk(
context,
outFile!.path,
);
}
} }
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
print(e); print(e);
} }
return false;
} }
} }
return false; return 1;
}
Future<int> installApk(
BuildContext context,
String apkPath,
) async {
try {
final status = await patcherChannel.invokeMethod('installApk', {
'apkPath': apkPath,
});
final int statusCode = status['status'];
final String message = status['message'];
final bool hasExtra =
message.contains('INSTALL_FAILED_VERIFICATION_FAILURE') ||
message.contains('INSTALL_FAILED_VERSION_DOWNGRADE');
if (statusCode == 0 || (statusCode == 3 && !hasExtra)) {
return statusCode;
} else {
_managerAPI.ctx = context;
return await installErrorDialog(
statusCode,
status,
hasExtra,
);
}
} on Exception catch (e) {
if (kDebugMode) {
print(e);
}
return 3;
}
}
Future<int> installErrorDialog(
num statusCode, [
status,
bool hasExtra = false,
]) async {
final String statusValue = InstallStatus.byCode(
hasExtra ? double.parse('$statusCode.1') : statusCode,
);
bool cleanInstall = false;
final bool isFixable = statusCode == 4 || statusCode == 5;
await showDialog(
context: _managerAPI.ctx!,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
title: I18nText('installErrorDialog.$statusValue'),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
I18nText(
'installErrorDialog.${statusValue}_description',
translationParams: statusCode == 2
? {
'packageName': status['otherPackageName'],
}
: null,
),
],
),
actions: (status == null)
? <Widget>[
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () async {
Navigator.pop(context);
},
),
]
: <Widget>[
CustomMaterialButton(
isFilled: !isFixable,
label: I18nText('cancelButton'),
onPressed: () {
Navigator.pop(context);
},
),
if (isFixable)
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () async {
final int response = await patcherChannel.invokeMethod(
'uninstallApp',
{'packageName': status['packageName']},
);
if (response == 0 && context.mounted) {
cleanInstall = true;
Navigator.pop(context);
}
},
),
],
),
);
return cleanInstall ? 10 : 1;
} }
void exportPatchedFile(String appName, String version) { void exportPatchedFile(String appName, String version) {
@ -271,7 +396,8 @@ void sharePatchedFile(String appName, String version) {
String _getFileName(String appName, String version) { String _getFileName(String appName, String version) {
final String patchVersion = _managerAPI.patchesVersion!; final String patchVersion = _managerAPI.patchesVersion!;
final String prefix = appName.toLowerCase().replaceAll(' ', '-'); final String prefix = appName.toLowerCase().replaceAll(' ', '-');
final String newName = '$prefix-revanced_v$version-patches_$patchVersion.apk'; final String newName =
'$prefix-revanced_v$version-patches_$patchVersion.apk';
return newName; return newName;
} }
@ -319,8 +445,65 @@ String getSuggestedVersion(String packageName) {
..clear() ..clear()
..addEntries(entries); ..addEntries(entries);
versions.removeWhere((key, value) => value != versions.values.last); versions.removeWhere((key, value) => value != versions.values.last);
return (versions.keys.toList() return (versions.keys.toList()..sort()).last;
..sort()).last;
} }
return ''; return '';
}} }
}
enum InstallStatus {
mountNoRoot(1),
mountVersionMismatch(1.1),
mountMissingInstallation(1.2),
statusFailureBlocked(2),
installFailedVerificationFailure(3.1),
statusFailureInvalid(4),
installFailedVersionDowngrade(4.1),
statusFailureConflict(5),
statusFailureStorage(6),
statusFailureIncompatible(7),
statusFailureTimeout(8);
const InstallStatus(this.statusCode);
final double statusCode;
static String byCode(num code) {
try {
return InstallStatus.values
.firstWhere((flag) => flag.statusCode == code)
.status;
} catch (e) {
return 'status_unknown';
}
}
}
extension InstallStatusExtension on InstallStatus {
String get status {
switch (this) {
case InstallStatus.mountNoRoot:
return 'mount_no_root';
case InstallStatus.mountVersionMismatch:
return 'mount_version_mismatch';
case InstallStatus.mountMissingInstallation:
return 'mount_missing_installation';
case InstallStatus.statusFailureBlocked:
return 'status_failure_blocked';
case InstallStatus.installFailedVerificationFailure:
return 'install_failed_verification_failure';
case InstallStatus.statusFailureInvalid:
return 'status_failure_invalid';
case InstallStatus.installFailedVersionDowngrade:
return 'install_failed_version_downgrade';
case InstallStatus.statusFailureConflict:
return 'status_failure_conflict';
case InstallStatus.statusFailureStorage:
return 'status_failure_storage';
case InstallStatus.statusFailureIncompatible:
return 'status_failure_incompatible';
case InstallStatus.statusFailureTimeout:
return 'status_failure_timeout';
}
}
}

View File

@ -2,10 +2,10 @@ import 'package:flutter/foundation.dart';
import 'package:root/root.dart'; import 'package:root/root.dart';
class RootAPI { class RootAPI {
// TODO(ponces): remove in the future, keep it for now during migration. // TODO(aAbed): remove in the future, keep it for now during migration.
final String _revancedOldDirPath = '/data/local/tmp/revanced-manager';
final String _revancedDirPath = '/data/adb/revanced';
final String _postFsDataDirPath = '/data/adb/post-fs-data.d'; final String _postFsDataDirPath = '/data/adb/post-fs-data.d';
final String _revancedDirPath = '/data/adb/revanced';
final String _serviceDDirPath = '/data/adb/service.d'; final String _serviceDDirPath = '/data/adb/service.d';
Future<bool> isRooted() async { Future<bool> isRooted() async {
@ -75,7 +75,7 @@ class RootAPI {
Future<List<String>> getInstalledApps() async { Future<List<String>> getInstalledApps() async {
final List<String> apps = List.empty(growable: true); final List<String> apps = List.empty(growable: true);
try { try {
String? res = await Root.exec( final String? res = await Root.exec(
cmd: 'ls "$_revancedDirPath"', cmd: 'ls "$_revancedDirPath"',
); );
if (res != null) { if (res != null) {
@ -83,15 +83,6 @@ class RootAPI {
list.removeWhere((pack) => pack.isEmpty); list.removeWhere((pack) => pack.isEmpty);
apps.addAll(list.map((pack) => pack.trim()).toList()); apps.addAll(list.map((pack) => pack.trim()).toList());
} }
// TODO(ponces): remove in the future, keep it for now during migration.
res = await Root.exec(
cmd: 'ls "$_revancedOldDirPath"',
);
if (res != null) {
final List<String> list = res.split('\n');
list.removeWhere((pack) => pack.isEmpty);
apps.addAll(list.map((pack) => pack.trim()).toList());
}
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
print(e); print(e);
@ -100,16 +91,9 @@ class RootAPI {
return apps; return apps;
} }
Future<void> deleteApp(String packageName, String originalFilePath) async { Future<void> unmount(String packageName) async {
await Root.exec( await Root.exec(
cmd: 'am force-stop "$packageName"', cmd: 'grep $packageName /proc/mounts | while read -r line; do echo \$line | cut -d " " -f 2 | sed "s/apk.*/apk/" | xargs -r umount -l; done',
);
await Root.exec(
cmd: 'su -mm -c "umount -l $originalFilePath"',
);
// TODO(ponces): remove in the future, keep it for now during migration.
await Root.exec(
cmd: 'rm -rf "$_revancedOldDirPath/$packageName"',
); );
await Root.exec( await Root.exec(
cmd: 'rm -rf "$_revancedDirPath/$packageName"', cmd: 'rm -rf "$_revancedDirPath/$packageName"',
@ -117,8 +101,21 @@ class RootAPI {
await Root.exec( await Root.exec(
cmd: 'rm -rf "$_serviceDDirPath/$packageName.sh"', cmd: 'rm -rf "$_serviceDDirPath/$packageName.sh"',
); );
}
// TODO(aAbed): remove in the future, keep it for now during migration.
Future<void> removeOrphanedFiles() async {
await Root.exec( await Root.exec(
cmd: 'rm -rf "$_postFsDataDirPath/$packageName.sh"', cmd: '''
find "$_revancedDirPath" -type f -name original.apk -delete
for file in "$_serviceDDirPath"/*; do
filename=\$(basename "\$file")
if [ -f "$_postFsDataDirPath/\$filename" ]; then
rm "$_postFsDataDirPath/\$filename"
fi
done
'''
.trim(),
); );
} }
@ -128,7 +125,6 @@ class RootAPI {
String patchedFilePath, String patchedFilePath,
) async { ) async {
try { try {
await deleteApp(packageName, originalFilePath);
await Root.exec( await Root.exec(
cmd: 'mkdir -p "$_revancedDirPath/$packageName"', cmd: 'mkdir -p "$_revancedDirPath/$packageName"',
); );
@ -138,11 +134,9 @@ class RootAPI {
'', '',
'$_revancedDirPath/$packageName', '$_revancedDirPath/$packageName',
); );
await saveOriginalFilePath(packageName, originalFilePath);
await installServiceDScript(packageName); await installServiceDScript(packageName);
await installPostFsDataScript(packageName);
await installApk(packageName, patchedFilePath); await installApk(packageName, patchedFilePath);
await mountApk(packageName, originalFilePath); await mountApk(packageName);
return true; return true;
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {
@ -156,26 +150,25 @@ class RootAPI {
await Root.exec( await Root.exec(
cmd: 'mkdir -p "$_serviceDDirPath"', cmd: 'mkdir -p "$_serviceDDirPath"',
); );
final String content = '#!/system/bin/sh\n' final String content = '''
'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 3; done\n' #!/system/bin/sh
'base_path=$_revancedDirPath/$packageName/base.apk\n' MAGISKTMP="\$(magisk --path)" || MAGISKTMP=/sbin
'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n' MIRROR="\$MAGISKTMP/.magisk/mirror"
r'[ ! -z $stock_path ] && mount -o bind $base_path $stock_path';
final String scriptFilePath = '$_serviceDDirPath/$packageName.sh';
await Root.exec(
cmd: 'echo \'$content\' > "$scriptFilePath"',
);
await setPermissions('0744', '', '', scriptFilePath);
}
Future<void> installPostFsDataScript(String packageName) async { until [ "\$(getprop sys.boot_completed)" = 1 ]; do sleep 3; done
await Root.exec( until [ -d "/sdcard/Android" ]; do sleep 1; done
cmd: 'mkdir -p "$_postFsDataDirPath"',
); base_path=$_revancedDirPath/$packageName/base.apk
final String content = '#!/system/bin/sh\n' stock_path=\$(pm path $packageName | grep base | sed 's/package://g' )
'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
r'[ ! -z $stock_path ] && umount -l $stock_path'; chcon u:object_r:apk_data_file:s0 \$base_path
final String scriptFilePath = '$_postFsDataDirPath/$packageName.sh'; mount -o bind \$MIRROR\$base_path \$stock_path
# Kill the app to force it to restart the mounted APK in case it's already running
am force-stop $packageName
'''
.trim();
final String scriptFilePath = '$_serviceDDirPath/$packageName.sh';
await Root.exec( await Root.exec(
cmd: 'echo \'$content\' > "$scriptFilePath"', cmd: 'echo \'$content\' > "$scriptFilePath"',
); );
@ -195,49 +188,12 @@ class RootAPI {
); );
} }
Future<void> mountApk(String packageName, String originalFilePath) async { Future<void> mountApk(String packageName,) async {
final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk';
await Root.exec( await Root.exec(
cmd: 'am force-stop "$packageName"', cmd: '''
); grep $packageName /proc/mounts | while read -r line; do echo \$line | cut -d " " -f 2 | sed "s/apk.*/apk/" | xargs -r umount -l; done
await Root.exec( .$_serviceDDirPath/$packageName.sh
cmd: 'su -mm -c "umount -l $originalFilePath"', '''.trim(),
);
await Root.exec(
cmd: 'su -mm -c "mount -o bind $newPatchedFilePath $originalFilePath"',
);
}
Future<bool> isMounted(String packageName) async {
final String? res = await Root.exec(
cmd: 'cat /proc/mounts | grep $packageName',
);
return res != null && res.isNotEmpty;
}
Future<void> saveOriginalFilePath(
String packageName,
String originalFilePath,
) async {
final String originalRootPath =
'$_revancedDirPath/$packageName/original.apk';
await Root.exec(
cmd: 'mkdir -p "$_revancedDirPath/$packageName"',
);
await setPermissions(
'0755',
'shell:shell',
'',
'$_revancedDirPath/$packageName',
);
await Root.exec(
cmd: 'cp "$originalFilePath" "$originalRootPath"',
);
await setPermissions(
'0644',
'shell:shell',
'u:object_r:apk_data_file:s0',
originalFilePath,
); );
} }

View File

@ -8,7 +8,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:install_plugin/install_plugin.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/app/app.router.dart'; import 'package:revanced_manager/app/app.router.dart';
@ -53,7 +52,7 @@ class HomeViewModel extends BaseViewModel {
_toast.showBottom('homeView.installingMessage'); _toast.showBottom('homeView.installingMessage');
final File? managerApk = await _managerAPI.downloadManager(); final File? managerApk = await _managerAPI.downloadManager();
if (managerApk != null) { if (managerApk != null) {
await InstallPlugin.installApk(managerApk.path); await _patcherAPI.installApk(context, managerApk.path);
} else { } else {
_toast.showBottom('homeView.errorDownloadMessage'); _toast.showBottom('homeView.errorDownloadMessage');
} }
@ -75,7 +74,7 @@ class HomeViewModel extends BaseViewModel {
_toast.showBottom('homeView.installingMessage'); _toast.showBottom('homeView.installingMessage');
final File? managerApk = await _managerAPI.downloadManager(); final File? managerApk = await _managerAPI.downloadManager();
if (managerApk != null) { if (managerApk != null) {
await InstallPlugin.installApk(managerApk.path); await _patcherAPI.installApk(context, managerApk.path);
} else { } else {
_toast.showBottom('homeView.errorDownloadMessage'); _toast.showBottom('homeView.errorDownloadMessage');
} }
@ -84,6 +83,7 @@ class HomeViewModel extends BaseViewModel {
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps()); _managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
} }
void navigateToAppInfo(PatchedApplication app) { void navigateToAppInfo(PatchedApplication app) {
_navigationService.navigateTo( _navigationService.navigateTo(
Routes.appInfoView, Routes.appInfoView,
@ -268,6 +268,7 @@ class HomeViewModel extends BaseViewModel {
valueListenable: downloaded, valueListenable: downloaded,
builder: (context, value, child) { builder: (context, value, child) {
return SimpleDialog( return SimpleDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
contentPadding: const EdgeInsets.all(16.0), contentPadding: const EdgeInsets.all(16.0),
title: I18nText( title: I18nText(
!value !value
@ -365,9 +366,7 @@ class HomeViewModel extends BaseViewModel {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: FilledButton( child: FilledButton(
onPressed: () async { onPressed: () async {
await InstallPlugin.installApk( await _patcherAPI.installApk(context, downloadedApk!.path);
downloadedApk!.path,
);
}, },
child: I18nText('updateButton'), child: I18nText('updateButton'),
), ),
@ -412,7 +411,7 @@ class HomeViewModel extends BaseViewModel {
// UILocalNotificationDateInterpretation.absoluteTime, // UILocalNotificationDateInterpretation.absoluteTime,
// ); // );
_toast.showBottom('homeView.installingMessage'); _toast.showBottom('homeView.installingMessage');
await InstallPlugin.installApk(managerApk.path); await _patcherAPI.installApk(context, managerApk.path);
} else { } else {
_toast.showBottom('homeView.errorDownloadMessage'); _toast.showBottom('homeView.errorDownloadMessage');
} }

View File

@ -316,7 +316,7 @@ class InstallerViewModel extends BaseViewModel {
await showDialog( await showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (context) => AlertDialog( builder: (innerContext) => AlertDialog(
title: I18nText( title: I18nText(
'installerView.installType', 'installerView.installType',
), ),
@ -367,6 +367,19 @@ class InstallerViewModel extends BaseViewModel {
installType.value = selected!; installType.value = selected!;
}, },
), ),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: I18nText(
'installerView.warning',
child: Text(
'',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.error,
),
),
),
),
], ],
); );
}, },
@ -375,13 +388,13 @@ class InstallerViewModel extends BaseViewModel {
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(innerContext).pop();
}, },
child: I18nText('cancelButton'), child: I18nText('cancelButton'),
), ),
FilledButton( FilledButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(innerContext).pop();
installResult(context, installType.value == 1); installResult(context, installType.value == 1);
}, },
child: I18nText('installerView.installButton'), child: I18nText('installerView.installButton'),
@ -390,7 +403,32 @@ class InstallerViewModel extends BaseViewModel {
), ),
); );
} else { } else {
await showDialog(
context: context,
barrierDismissible: false,
builder: (innerContext) => AlertDialog(
title: I18nText(
'warning',
),
contentPadding: const EdgeInsets.all(16),
content: I18nText('installerView.warning'),
actions: [
TextButton(
onPressed: () {
Navigator.of(innerContext).pop();
},
child: I18nText('cancelButton'),
),
FilledButton(
onPressed: () {
Navigator.of(innerContext).pop();
installResult(context, false); installResult(context, false);
},
child: I18nText('installerView.installButton'),
),
],
),
);
} }
} }
@ -411,15 +449,18 @@ class InstallerViewModel extends BaseViewModel {
Future<void> installResult(BuildContext context, bool installAsRoot) async { Future<void> installResult(BuildContext context, bool installAsRoot) async {
try { try {
_app.isRooted = installAsRoot; _app.isRooted = installAsRoot;
if (headerLogs != 'Installing...') {
update( update(
1.0, 1.0,
'Installing...', 'Installing...',
_app.isRooted _app.isRooted
? 'Installing patched file using root method' ? 'Mounting patched app'
: 'Installing patched file using nonroot method', : 'Installing patched app',
); );
isInstalled = await _patcherAPI.installPatchedFile(_app); }
if (isInstalled) { final int response = await _patcherAPI.installPatchedFile(context, _app);
if (response == 0) {
isInstalled = true;
_app.isFromStorage = false; _app.isFromStorage = false;
_app.patchDate = DateTime.now(); _app.patchDate = DateTime.now();
_app.appliedPatches = _patches.map((p) => p.name).toList(); _app.appliedPatches = _patches.map((p) => p.name).toList();
@ -435,9 +476,26 @@ class InstallerViewModel extends BaseViewModel {
await _managerAPI.savePatchedApp(_app); await _managerAPI.savePatchedApp(_app);
update(1.0, 'Installed!', 'Installed!'); update(1.0, 'Installed', 'Installed');
} else if (response == 3) {
update(
1.0,
'Installation canceled',
'Installation canceled',
);
} else if (response == 10) {
installResult(context, installAsRoot);
update(
1.0,
'',
'Starting installer',
);
} else { } else {
// TODO(aabed): Show error message. update(
1.0,
'Installation failed',
'Installation failed',
);
} }
} on Exception catch (e) { } on Exception catch (e) {
if (kDebugMode) { if (kDebugMode) {

View File

@ -184,10 +184,6 @@ class PatchesSelectorViewModel extends BaseViewModel {
void selectPatches() { void selectPatches() {
locator<PatcherViewModel>().selectedPatches = selectedPatches; locator<PatcherViewModel>().selectedPatches = selectedPatches;
saveSelectedPatches(); saveSelectedPatches();
if (_managerAPI.ctx != null) {
Navigator.pop(_managerAPI.ctx!);
_managerAPI.ctx = null;
}
locator<PatcherViewModel>().notifyListeners(); locator<PatcherViewModel>().notifyListeners();
} }

View File

@ -29,7 +29,9 @@ class AppInfoViewModel extends BaseViewModel {
if (app.isRooted) { if (app.isRooted) {
final bool hasRootPermissions = await _rootAPI.hasRootPermissions(); final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
if (hasRootPermissions) { if (hasRootPermissions) {
await _rootAPI.deleteApp(app.packageName, app.apkFilePath); await _rootAPI.unmount(
app.packageName,
);
if (!onlyUnpatch) { if (!onlyUnpatch) {
await DeviceApps.uninstallApp(app.packageName); await DeviceApps.uninstallApp(app.packageName);
} }

View File

@ -65,10 +65,6 @@ dependencies:
flutter_dotenv: ^5.0.2 flutter_dotenv: ^5.0.2
flutter_markdown: ^0.6.14 flutter_markdown: ^0.6.14
dio_cache_interceptor: ^3.4.0 dio_cache_interceptor: ^3.4.0
install_plugin:
git: # remove once https://github.com/hui-z/flutter_install_plugin/pull/67 is merged
url: https://github.com/BenjaminHalko/flutter_install_plugin
ref: 5f9b1a8c956fc3355ae655eefcbcadb457bd10f7 # Branch: master
screenshot_callback: screenshot_callback:
git: # remove once https://github.com/flutter-moum/flutter_screenshot_callback/pull/81 is merged git: # remove once https://github.com/flutter-moum/flutter_screenshot_callback/pull/81 is merged
url: https://github.com/BenjaminHalko/flutter_screenshot_callback url: https://github.com/BenjaminHalko/flutter_screenshot_callback