From 35a92cd227ce362259c84c3b14cd37509fad5f0c Mon Sep 17 00:00:00 2001 From: Ankit Saini Date: Tue, 25 Oct 2022 12:19:42 +0530 Subject: [PATCH] Adding `Export APK` support --- .../revanced/manager/flutter/MainActivity.kt | 485 ++++++++++-------- assets/i18n/en_US.json | 1 + lib/services/patcher_api.dart | 16 + lib/ui/views/installer/installer_view.dart | 11 +- .../views/installer/installer_viewmodel.dart | 11 + 5 files changed, 318 insertions(+), 206 deletions(-) diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt index 2d76f9fd62..89a4ec0a16 100644 --- a/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt +++ b/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt @@ -1,5 +1,8 @@ package app.revanced.manager.flutter +import android.app.Activity +import android.content.Intent +import android.net.Uri import android.os.Build import android.os.Handler import android.os.Looper @@ -18,19 +21,31 @@ import dalvik.system.DexClassLoader import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.Result import java.io.File +import java.io.OutputStream private const val PATCHER_CHANNEL = "app.revanced.manager.flutter/patcher" private const val INSTALLER_CHANNEL = "app.revanced.manager.flutter/installer" +private const val EXPORTER_CHANNEL = "app.revanced.manager.flutter/export" class MainActivity : FlutterActivity() { private val handler = Handler(Looper.getMainLooper()) private lateinit var installerChannel: MethodChannel + private lateinit var exporterChannel: MethodChannel + + internal var WRITE_REQUEST_CODE = 77777 // unique request code + internal var _result: Result? = null + internal var _apk_path: String? = null override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) val mainChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PATCHER_CHANNEL) - installerChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTALLER_CHANNEL) + installerChannel = + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTALLER_CHANNEL) + exporterChannel = + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, EXPORTER_CHANNEL) + mainChannel.setMethodCallHandler { call, result -> when (call.method) { "runPatcher" -> { @@ -45,28 +60,28 @@ class MainActivity : FlutterActivity() { val mergeIntegrations = call.argument("mergeIntegrations") val keyStoreFilePath = call.argument("keyStoreFilePath") if (patchBundleFilePath != null && - originalFilePath != null && - inputFilePath != null && - patchedFilePath != null && - outFilePath != null && - integrationsPath != null && - selectedPatches != null && - cacheDirPath != null && - mergeIntegrations != null && - keyStoreFilePath != null + originalFilePath != null && + inputFilePath != null && + patchedFilePath != null && + outFilePath != null && + integrationsPath != null && + selectedPatches != null && + cacheDirPath != null && + mergeIntegrations != null && + keyStoreFilePath != null ) { runPatcher( - result, - patchBundleFilePath, - originalFilePath, - inputFilePath, - patchedFilePath, - outFilePath, - integrationsPath, - selectedPatches, - cacheDirPath, - mergeIntegrations, - keyStoreFilePath + result, + patchBundleFilePath, + originalFilePath, + inputFilePath, + patchedFilePath, + outFilePath, + integrationsPath, + selectedPatches, + cacheDirPath, + mergeIntegrations, + keyStoreFilePath ) } else { result.notImplemented() @@ -75,20 +90,77 @@ class MainActivity : FlutterActivity() { else -> result.notImplemented() } } + + exporterChannel.setMethodCallHandler { call, result -> + // Note: this method is invoked on the main thread. + if (call.method == "exportFile") { + _result = result; + `_apk_path` = call.argument("current_path"); + var mime: String? = "application/vnd.android.package-archive"; + var name: String? = call.argument("name") + if (mime != null && name != null) createFile(mime, name) + } else { + result.notImplemented(); + } + } + } + private fun createFile(mimeType: String, fileName: String) { + val intent = + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + // Filter to only show results that can be "opened", such as + // a file (as opposed to a list of contacts or timezones). + addCategory(Intent.CATEGORY_OPENABLE) + + // Create a file with the requested MIME type. + type = mimeType + putExtra(Intent.EXTRA_TITLE, fileName) + } + + startActivityForResult(intent, WRITE_REQUEST_CODE) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + // Check which request we're responding to + if (requestCode == WRITE_REQUEST_CODE) { + // Make sure the request was successful + if (resultCode == Activity.RESULT_OK) { + if (data != null && data.getData() != null) { + // now write the data + writeInFile(data.getData() as Uri) // data.getData() is Uri + } else { + _result?.error("NO DATA", "No data", null) + } + } else { + _result?.error("CANCELED", "User cancelled", null) + } + } + } + + private fun writeInFile(uri: Uri) { + val outputStream: OutputStream + try { + outputStream = getContentResolver().openOutputStream(uri)!! + File(_apk_path).inputStream().copyTo(outputStream) + _result?.success("SUCCESS") + } catch (e: Exception) { + _result?.error("ERROR", "Unable to write", null) + } } private fun runPatcher( - result: MethodChannel.Result, - patchBundleFilePath: String, - originalFilePath: String, - inputFilePath: String, - patchedFilePath: String, - outFilePath: String, - integrationsPath: String, - selectedPatches: List, - cacheDirPath: String, - mergeIntegrations: Boolean, - keyStoreFilePath: String + result: MethodChannel.Result, + patchBundleFilePath: String, + originalFilePath: String, + inputFilePath: String, + patchedFilePath: String, + outFilePath: String, + integrationsPath: String, + selectedPatches: List, + cacheDirPath: String, + mergeIntegrations: Boolean, + keyStoreFilePath: String ) { val originalFile = File(originalFilePath) val inputFile = File(inputFilePath) @@ -98,203 +170,206 @@ class MainActivity : FlutterActivity() { val keyStoreFile = File(keyStoreFilePath) Thread { - try { - val patches = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) { - PatchBundle.Dex( - patchBundleFilePath, - DexClassLoader( - patchBundleFilePath, - cacheDirPath, - null, - javaClass.classLoader - ) - ).loadPatches().filter { patch -> selectedPatches.any { it == patch.patchName } } - } else { - TODO("VERSION.SDK_INT < CUPCAKE") - } + try { + val patches = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) { + PatchBundle.Dex( + patchBundleFilePath, + DexClassLoader( + patchBundleFilePath, + cacheDirPath, + null, + javaClass.classLoader + ) + ) + .loadPatches() + .filter { patch -> + selectedPatches.any { it == patch.patchName } + } + } else { + TODO("VERSION.SDK_INT < CUPCAKE") + } - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 0.1, - "header" to "", - "log" to "Copying original apk" - ) - ) - } - originalFile.copyTo(inputFile, true) + handler.post { + installerChannel.invokeMethod( + "update", + mapOf( + "progress" to 0.1, + "header" to "", + "log" to "Copying original apk" + ) + ) + } + originalFile.copyTo(inputFile, true) - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 0.2, - "header" to "Unpacking apk...", - "log" to "Unpacking input apk" - ) - ) - } - val patcher = - Patcher( - PatcherOptions( - inputFile, - cacheDirPath, - Aapt.binary(applicationContext).absolutePath, - cacheDirPath, - logger = ManagerLogger() - ) - ) + handler.post { + installerChannel.invokeMethod( + "update", + mapOf( + "progress" to 0.2, + "header" to "Unpacking apk...", + "log" to "Unpacking input apk" + ) + ) + } + val patcher = + Patcher( + PatcherOptions( + inputFile, + cacheDirPath, + Aapt.binary(applicationContext).absolutePath, + cacheDirPath, + logger = ManagerLogger() + ) + ) - handler.post { - installerChannel.invokeMethod( - "update", - mapOf("progress" to 0.3, "header" to "", "log" to "") - ) - } - if (mergeIntegrations) { - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 0.4, - "header" to "Merging integrations...", - "log" to "Merging integrations" + handler.post { + installerChannel.invokeMethod( + "update", + mapOf("progress" to 0.3, "header" to "", "log" to "") ) - ) - } - patcher.addFiles(listOf(integrations)) {} - } + } + if (mergeIntegrations) { + handler.post { + installerChannel.invokeMethod( + "update", + mapOf( + "progress" to 0.4, + "header" to "Merging integrations...", + "log" to "Merging integrations" + ) + ) + } + patcher.addFiles(listOf(integrations)) {} + } - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 0.5, - "header" to "Applying patches...", - "log" to "" - ) - ) - } + handler.post { + installerChannel.invokeMethod( + "update", + mapOf( + "progress" to 0.5, + "header" to "Applying patches...", + "log" to "" + ) + ) + } + + patcher.addPatches(patches) + patcher.executePatches().forEach { (patch, res) -> + if (res.isSuccess) { + val msg = "[success] $patch" + handler.post { + installerChannel.invokeMethod( + "update", + mapOf("progress" to 0.5, "header" to "", "log" to msg) + ) + } + return@forEach + } + val msg = "[error] $patch:" + res.exceptionOrNull()!!.printStackTrace() + handler.post { + installerChannel.invokeMethod( + "update", + mapOf("progress" to 0.5, "header" to "", "log" to msg) + ) + } + } - patcher.addPatches(patches) - patcher.executePatches().forEach { (patch, res) -> - if (res.isSuccess) { - val msg = "[success] $patch" handler.post { installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 0.5, - "header" to "", - "log" to msg + "update", + mapOf( + "progress" to 0.7, + "header" to "Repacking apk...", + "log" to "Repacking patched apk" + ) + ) + } + val res = patcher.save() + ZipFile(patchedFile).use { file -> + res.dexFiles.forEach { + file.addEntryCompressData( + ZipEntry.createWithName(it.name), + it.stream.readBytes() ) + } + res.resourceFile?.let { + file.copyEntriesFromFileAligned( + ZipFile(it), + ZipAligner::getEntryAlignment + ) + } + file.copyEntriesFromFileAligned( + ZipFile(inputFile), + ZipAligner::getEntryAlignment + ) + } + handler.post { + installerChannel.invokeMethod( + "update", + mapOf( + "progress" to 0.9, + "header" to "Signing apk...", + "log" to "" + ) ) } - return@forEach - } - val msg = "[error] $patch:" + res.exceptionOrNull()!!.printStackTrace() - handler.post { - installerChannel.invokeMethod( - "update", - mapOf("progress" to 0.5, "header" to "", "log" to msg) - ) - } - } - - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 0.7, - "header" to "Repacking apk...", - "log" to "Repacking patched apk" - ) - ) - } - val res = patcher.save() - ZipFile(patchedFile).use { file -> - res.dexFiles.forEach { - file.addEntryCompressData( - ZipEntry.createWithName(it.name), - it.stream.readBytes() - ) - } - res.resourceFile?.let { - file.copyEntriesFromFileAligned( - ZipFile(it), - ZipAligner::getEntryAlignment - ) - } - file.copyEntriesFromFileAligned( - ZipFile(inputFile), - ZipAligner::getEntryAlignment - ) - } - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 0.9, - "header" to "Signing apk...", - "log" to "" - ) - ) - } - // Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile, keyStoreFile) + // Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile, + // keyStoreFile) - try { - Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile, keyStoreFile) - } catch (e: Exception) { - //log to console - print("Error signing apk: ${e.message}") - e.printStackTrace() - } + try { + Signer("ReVanced", "s3cur3p@ssw0rd") + .signApk(patchedFile, outFile, keyStoreFile) + } catch (e: Exception) { + // log to console + print("Error signing apk: ${e.message}") + e.printStackTrace() + } - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 1.0, - "header" to "Finished!", - "log" to "Finished!" - ) - ) - } - } catch (ex: Throwable) { - val stack = ex.stackTraceToString() - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to -100.0, - "header" to "Aborting...", - "log" to "An error occurred! Aborting\nError:\n$stack" - ) - ) + handler.post { + installerChannel.invokeMethod( + "update", + mapOf( + "progress" to 1.0, + "header" to "Finished!", + "log" to "Finished!" + ) + ) + } + } catch (ex: Throwable) { + val stack = ex.stackTraceToString() + handler.post { + installerChannel.invokeMethod( + "update", + mapOf( + "progress" to -100.0, + "header" to "Aborting...", + "log" to "An error occurred! Aborting\nError:\n$stack" + ) + ) + } + } + handler.post { result.success(null) } } - } - handler.post { result.success(null) } - }.start() + .start() } inner class ManagerLogger : Logger { override fun error(msg: String) { handler.post { - installerChannel - .invokeMethod( + installerChannel.invokeMethod( "update", mapOf("progress" to -1.0, "header" to "", "log" to msg) - ) + ) } } override fun warn(msg: String) { handler.post { installerChannel.invokeMethod( - "update", - mapOf("progress" to -1.0, "header" to "", "log" to msg) + "update", + mapOf("progress" to -1.0, "header" to "", "log" to msg) ) } } @@ -302,8 +377,8 @@ class MainActivity : FlutterActivity() { override fun info(msg: String) { handler.post { installerChannel.invokeMethod( - "update", - mapOf("progress" to -1.0, "header" to "", "log" to msg) + "update", + mapOf("progress" to -1.0, "header" to "", "log" to msg) ) } } @@ -311,8 +386,8 @@ class MainActivity : FlutterActivity() { override fun trace(msg: String) { handler.post { installerChannel.invokeMethod( - "update", - mapOf("progress" to -1.0, "header" to "", "log" to msg) + "update", + mapOf("progress" to -1.0, "header" to "", "log" to msg) ) } } diff --git a/assets/i18n/en_US.json b/assets/i18n/en_US.json index 8b13f9ee31..91b3f4cb10 100644 --- a/assets/i18n/en_US.json +++ b/assets/i18n/en_US.json @@ -95,6 +95,7 @@ "notificationTitle": "ReVanced Manager is patching", "notificationText": "Tap to return to the installer", "shareApkMenuOption": "Share APK", + "exportApkMenuOption": "Export APK", "shareLogMenuOption": "Share log", "installErrorDialogTitle": "Error", "installErrorDialogText1": "Root install is not possible with the current patches selection.\nRepatch your app or choose non-root install.", diff --git a/lib/services/patcher_api.dart b/lib/services/patcher_api.dart index 31ad0cd436..996d7452fb 100644 --- a/lib/services/patcher_api.dart +++ b/lib/services/patcher_api.dart @@ -228,6 +228,22 @@ class PatcherAPI { return false; } + void exportPatchedFile(String appName, String version) { + try { + if (_outFile != null) { + const platform = MethodChannel("app.revanced.manager.flutter/export"); + String prefix = appName.toLowerCase().replaceAll(' ', '-'); + String newName = '$prefix-revanced_v$version.apk'; + platform.invokeMethod("exportFile", { + "current_path": _outFile!.path, + "name": newName + }); + } + } on Exception catch (e, s) { + Sentry.captureException(e, stackTrace: s); + } + } + void sharePatchedFile(String appName, String version) { try { if (_outFile != null) { diff --git a/lib/ui/views/installer/installer_view.dart b/lib/ui/views/installer/installer_view.dart index 7cfcbeb95f..e6b5672fd0 100644 --- a/lib/ui/views/installer/installer_view.dart +++ b/lib/ui/views/installer/installer_view.dart @@ -48,7 +48,16 @@ class InstallerView extends StatelessWidget { ), ), ), - 1: I18nText( + 1: I18nText( + 'installerView.exportApkMenuOption', + child: const Text( + '', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + 2: I18nText( 'installerView.shareLogMenuOption', child: const Text( '', diff --git a/lib/ui/views/installer/installer_viewmodel.dart b/lib/ui/views/installer/installer_viewmodel.dart index ae9fdc4d78..50fa006a78 100644 --- a/lib/ui/views/installer/installer_viewmodel.dart +++ b/lib/ui/views/installer/installer_viewmodel.dart @@ -217,6 +217,14 @@ class InstallerViewModel extends BaseViewModel { } } + void exportResult() { + try { + _patcherAPI.exportPatchedFile(_app.name, _app.version); + } on Exception catch (e, s) { + Sentry.captureException(e, stackTrace: s); + } + } + void shareResult() { try { _patcherAPI.sharePatchedFile(_app.name, _app.version); @@ -250,6 +258,9 @@ class InstallerViewModel extends BaseViewModel { shareResult(); break; case 1: + exportResult(); + break; + case 2: shareLog(); break; }