mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-04-29 21:44:26 +02:00
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:
parent
8b28a33b73
commit
c23275f2fe
@ -32,17 +32,17 @@
|
|||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"/>
|
android:resource="@style/NormalTheme" />
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ExportSettingsActivity"
|
android:name=".ExportSettingsActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
</activity>
|
</activity>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
@ -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>
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
@ -72,4 +72,3 @@ class DownloadManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,22 +3,24 @@ 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
|
||||||
class PatcherAPI {
|
class PatcherAPI {
|
||||||
static const patcherChannel =
|
static const patcherChannel =
|
||||||
MethodChannel('app.revanced.manager.flutter/patcher');
|
MethodChannel('app.revanced.manager.flutter/patcher');
|
||||||
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
final ManagerAPI _managerAPI = locator<ManagerAPI>();
|
||||||
final RootAPI _rootAPI = RootAPI();
|
final RootAPI _rootAPI = RootAPI();
|
||||||
late Directory _dataDir;
|
late Directory _dataDir;
|
||||||
@ -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;
|
||||||
@ -121,11 +124,11 @@ class PatcherAPI {
|
|||||||
final List<Patch> patches = _patches
|
final List<Patch> patches = _patches
|
||||||
.where(
|
.where(
|
||||||
(patch) =>
|
(patch) =>
|
||||||
patch.compatiblePackages.isEmpty ||
|
patch.compatiblePackages.isEmpty ||
|
||||||
!patch.name.contains('settings') &&
|
!patch.name.contains('settings') &&
|
||||||
patch.compatiblePackages
|
patch.compatiblePackages
|
||||||
.any((pack) => pack.name == packageName),
|
.any((pack) => pack.name == packageName),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
if (!_managerAPI.areUniversalPatchesEnabled()) {
|
if (!_managerAPI.areUniversalPatchesEnabled()) {
|
||||||
filteredPatches[packageName] = patches
|
filteredPatches[packageName] = patches
|
||||||
@ -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 apkFilePath,
|
String packageName,
|
||||||
List<Patch> selectedPatches,) async {
|
String apkFilePath,
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@ -194,133 +202,308 @@ class PatcherAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stopPatcher() async {
|
Future<void> stopPatcher() async {
|
||||||
try {
|
try {
|
||||||
await patcherChannel.invokeMethod('stopPatcher');
|
await patcherChannel.invokeMethod('stopPatcher');
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print(e);
|
print(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> installPatchedFile(PatchedApplication patchedApp) async {
|
Future<int> installPatchedFile(
|
||||||
if (outFile != null) {
|
BuildContext context,
|
||||||
try {
|
PatchedApplication patchedApp,
|
||||||
if (patchedApp.isRooted) {
|
) async {
|
||||||
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
if (outFile != null) {
|
||||||
if (hasRootPermissions) {
|
_managerAPI.ctx = context;
|
||||||
return _rootAPI.installApp(
|
try {
|
||||||
patchedApp.packageName,
|
if (patchedApp.isRooted) {
|
||||||
patchedApp.apkFilePath,
|
final bool hasRootPermissions = await _rootAPI.hasRootPermissions();
|
||||||
outFile!.path,
|
final packageVersion = await DeviceApps.getApp(patchedApp.packageName)
|
||||||
);
|
.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.apkFilePath,
|
||||||
|
outFile!.path,
|
||||||
|
)
|
||||||
|
? 0
|
||||||
|
: 1;
|
||||||
|
} else {
|
||||||
|
installErrorDialog(1.1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (await _rootAPI.hasRootPermissions()) {
|
||||||
|
await _rootAPI.unmount(patchedApp.packageName);
|
||||||
|
}
|
||||||
|
if (context.mounted) {
|
||||||
|
return await installApk(
|
||||||
|
context,
|
||||||
|
outFile!.path,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
} else {
|
||||||
final install = await InstallPlugin.installApk(outFile!.path);
|
_managerAPI.ctx = context;
|
||||||
return install['isSuccess'];
|
return await installErrorDialog(
|
||||||
|
statusCode,
|
||||||
|
status,
|
||||||
|
hasExtra,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print(e);
|
print(e);
|
||||||
}
|
}
|
||||||
return false;
|
return 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void exportPatchedFile(String appName, String version) {
|
Future<int> installErrorDialog(
|
||||||
try {
|
num statusCode, [
|
||||||
if (outFile != null) {
|
status,
|
||||||
final String newName = _getFileName(appName, version);
|
bool hasExtra = false,
|
||||||
FlutterFileDialog.saveFile(
|
]) async {
|
||||||
params: SaveFileDialogParams(
|
final String statusValue = InstallStatus.byCode(
|
||||||
sourceFilePath: outFile!.path,
|
hasExtra ? double.parse('$statusCode.1') : statusCode,
|
||||||
fileName: newName,
|
|
||||||
mimeTypesFilter: ['application/vnd.android.package-archive'],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void sharePatchedFile(String appName, String version) {
|
|
||||||
try {
|
|
||||||
if (outFile != null) {
|
|
||||||
final String newName = _getFileName(appName, version);
|
|
||||||
final int lastSeparator = outFile!.path.lastIndexOf('/');
|
|
||||||
final String newPath =
|
|
||||||
outFile!.path.substring(0, lastSeparator + 1) + newName;
|
|
||||||
final File shareFile = outFile!.copySync(newPath);
|
|
||||||
Share.shareXFiles([XFile(shareFile.path)]);
|
|
||||||
}
|
|
||||||
} on Exception catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getFileName(String appName, String version) {
|
|
||||||
final String patchVersion = _managerAPI.patchesVersion!;
|
|
||||||
final String prefix = appName.toLowerCase().replaceAll(' ', '-');
|
|
||||||
final String newName = '$prefix-revanced_v$version-patches_$patchVersion.apk';
|
|
||||||
return newName;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> exportPatcherLog(String logs) async {
|
|
||||||
final Directory appCache = await getTemporaryDirectory();
|
|
||||||
final Directory logDir = Directory('${appCache.path}/logs');
|
|
||||||
logDir.createSync();
|
|
||||||
final String dateTime = DateTime.now()
|
|
||||||
.toIso8601String()
|
|
||||||
.replaceAll('-', '')
|
|
||||||
.replaceAll(':', '')
|
|
||||||
.replaceAll('T', '')
|
|
||||||
.replaceAll('.', '');
|
|
||||||
final String fileName = 'revanced-manager_patcher_$dateTime.txt';
|
|
||||||
final File log = File('${logDir.path}/$fileName');
|
|
||||||
log.writeAsStringSync(logs);
|
|
||||||
FlutterFileDialog.saveFile(
|
|
||||||
params:SaveFileDialogParams(
|
|
||||||
sourceFilePath: log.path,
|
|
||||||
fileName: fileName,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String getSuggestedVersion(String packageName) {
|
|
||||||
final Map<String, int> versions = {};
|
|
||||||
for (final Patch patch in _patches) {
|
|
||||||
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
|
||||||
(pack) => pack.name == packageName,
|
|
||||||
);
|
);
|
||||||
if (package != null) {
|
bool cleanInstall = false;
|
||||||
for (final String version in package.versions) {
|
final bool isFixable = statusCode == 4 || statusCode == 5;
|
||||||
versions.update(
|
await showDialog(
|
||||||
version,
|
context: _managerAPI.ctx!,
|
||||||
(value) => versions[version]! + 1,
|
builder: (context) => AlertDialog(
|
||||||
ifAbsent: () => 1,
|
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) {
|
||||||
|
try {
|
||||||
|
if (outFile != null) {
|
||||||
|
final String newName = _getFileName(appName, version);
|
||||||
|
FlutterFileDialog.saveFile(
|
||||||
|
params: SaveFileDialogParams(
|
||||||
|
sourceFilePath: outFile!.path,
|
||||||
|
fileName: newName,
|
||||||
|
mimeTypesFilter: ['application/vnd.android.package-archive'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (versions.isNotEmpty) {
|
|
||||||
final entries = versions.entries.toList()
|
void sharePatchedFile(String appName, String version) {
|
||||||
..sort((a, b) => a.value.compareTo(b.value));
|
try {
|
||||||
versions
|
if (outFile != null) {
|
||||||
..clear()
|
final String newName = _getFileName(appName, version);
|
||||||
..addEntries(entries);
|
final int lastSeparator = outFile!.path.lastIndexOf('/');
|
||||||
versions.removeWhere((key, value) => value != versions.values.last);
|
final String newPath =
|
||||||
return (versions.keys.toList()
|
outFile!.path.substring(0, lastSeparator + 1) + newName;
|
||||||
..sort()).last;
|
final File shareFile = outFile!.copySync(newPath);
|
||||||
|
Share.shareXFiles([XFile(shareFile.path)]);
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return '';
|
|
||||||
}}
|
String _getFileName(String appName, String version) {
|
||||||
|
final String patchVersion = _managerAPI.patchesVersion!;
|
||||||
|
final String prefix = appName.toLowerCase().replaceAll(' ', '-');
|
||||||
|
final String newName =
|
||||||
|
'$prefix-revanced_v$version-patches_$patchVersion.apk';
|
||||||
|
return newName;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> exportPatcherLog(String logs) async {
|
||||||
|
final Directory appCache = await getTemporaryDirectory();
|
||||||
|
final Directory logDir = Directory('${appCache.path}/logs');
|
||||||
|
logDir.createSync();
|
||||||
|
final String dateTime = DateTime.now()
|
||||||
|
.toIso8601String()
|
||||||
|
.replaceAll('-', '')
|
||||||
|
.replaceAll(':', '')
|
||||||
|
.replaceAll('T', '')
|
||||||
|
.replaceAll('.', '');
|
||||||
|
final String fileName = 'revanced-manager_patcher_$dateTime.txt';
|
||||||
|
final File log = File('${logDir.path}/$fileName');
|
||||||
|
log.writeAsStringSync(logs);
|
||||||
|
FlutterFileDialog.saveFile(
|
||||||
|
params: SaveFileDialogParams(
|
||||||
|
sourceFilePath: log.path,
|
||||||
|
fileName: fileName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String getSuggestedVersion(String packageName) {
|
||||||
|
final Map<String, int> versions = {};
|
||||||
|
for (final Patch patch in _patches) {
|
||||||
|
final Package? package = patch.compatiblePackages.firstWhereOrNull(
|
||||||
|
(pack) => pack.name == packageName,
|
||||||
|
);
|
||||||
|
if (package != null) {
|
||||||
|
for (final String version in package.versions) {
|
||||||
|
versions.update(
|
||||||
|
version,
|
||||||
|
(value) => versions[version]! + 1,
|
||||||
|
ifAbsent: () => 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (versions.isNotEmpty) {
|
||||||
|
final entries = versions.entries.toList()
|
||||||
|
..sort((a, b) => a.value.compareTo(b.value));
|
||||||
|
versions
|
||||||
|
..clear()
|
||||||
|
..addEntries(entries);
|
||||||
|
versions.removeWhere((key, value) => value != versions.values.last);
|
||||||
|
return (versions.keys.toList()..sort()).last;
|
||||||
|
}
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
installResult(context, false);
|
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);
|
||||||
|
},
|
||||||
|
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;
|
||||||
update(
|
if (headerLogs != 'Installing...') {
|
||||||
1.0,
|
update(
|
||||||
'Installing...',
|
1.0,
|
||||||
_app.isRooted
|
'Installing...',
|
||||||
? 'Installing patched file using root method'
|
_app.isRooted
|
||||||
: 'Installing patched file using nonroot method',
|
? 'Mounting patched app'
|
||||||
);
|
: '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) {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user