From a9a05098e5de0f879013138cf581f5c9bde8b6b0 Mon Sep 17 00:00:00 2001 From: Giacomo Ferretti Date: Mon, 15 Nov 2021 19:50:49 +0100 Subject: [PATCH] refactor: use cache folder instead of files folder --- .../java/me/hexile/odexpatcher/art/Dex2Oat.kt | 114 +---- .../me/hexile/odexpatcher/ktx/FilesExt.kt | 37 +- .../odexpatcher/viewmodels/MainViewModel.kt | 470 ++---------------- 3 files changed, 98 insertions(+), 523 deletions(-) diff --git a/app/src/main/java/me/hexile/odexpatcher/art/Dex2Oat.kt b/app/src/main/java/me/hexile/odexpatcher/art/Dex2Oat.kt index 6495d56..50838e7 100644 --- a/app/src/main/java/me/hexile/odexpatcher/art/Dex2Oat.kt +++ b/app/src/main/java/me/hexile/odexpatcher/art/Dex2Oat.kt @@ -1,104 +1,20 @@ package me.hexile.odexpatcher.art -object Dex2Oat { - /*fun run(context: Context) { - when { - Build.VERSION.SDK_INT < Build.VERSION_CODES.M -> { - android44_51(context) - } - Build.VERSION.SDK_INT < Build.VERSION_CODES.Q -> { - android6_9(context) - } - else -> { - android10(context) - } - } - }*/ - - /*private fun android44_51(context: Context) { - // Android 4.4 - 5.1 workflow - // - copy from /data/app/packagename.apk to /data/data/me.hexile.odexpatcher/files/backup.apk - // - copy from /data/data/me.hexile.odexpatcher/files/base.apk to /data/app/packagename.apk - // - dex2oat /data/app/packagename.apk - // - copy from /data/data/me.hexile.odexpatcher/files/backup.apk to /data/app/packagename.apk - val backupApk = context.getFileInFilesDir("backup.apk").absolutePath - - //viewModel.addLog("[I] Backing up target apk…") - var shellResult = Shell.su("cp $targetApk $backupApk").exec() - if (!shellResult.isSuccess) { - //viewModel.addLog("[E] ERROR: cp exit code was ${shellResult.code}.") - //viewModel.state.postValue(true) - eventChannel.send(MainViewModel.Event.SnackBarString("ERROR: cp exit code was ${shellResult.code}.")) - return@launch - } - //viewModel.addLog(" Done!", false) - - //viewModel.addLog("[I] Copying over input file…") - shellResult = Shell.su("cp ${baseApk.absolutePath} $targetApk").exec() - if (!shellResult.isSuccess) { - //viewModel.addLog("[E] ERROR: cp exit code was ${shellResult.code}.") - //viewModel.state.postValue(true) - eventChannel.send(MainViewModel.Event.SnackBarString("ERROR: cp exit code was ${shellResult.code}.")) - return@launch - } - //viewModel.addLog(" Done!", false) - - viewModel.addLog("[I] Running dex2oat…") - shellResult = Shell.sh( - "dex2oat --dex-file=$targetApk --oat-file=${ - App.context.getFileInFilesDir(Const.BASE_ODEX_FILE_NAME).absolutePath - }" - ).exec() - if (!shellResult.isSuccess) { - viewModel.addLog("[E] ERROR: dex2oat exit code was ${shellResult.code}.") - viewModel.state.postValue(true) - return@launch - } - viewModel.addLog(" Done!", false) +import com.topjohnwu.superuser.Shell +import me.hexile.odexpatcher.core.App +import me.hexile.odexpatcher.core.Const +import me.hexile.odexpatcher.ktx.getFileInCacheDir +import me.hexile.odexpatcher.utils.logd - viewModel.addLog("[I] Restoring backup…") - shellResult = Shell.su("cp $backupApk $targetApk").exec() - if (!shellResult.isSuccess) { - viewModel.addLog("[E] ERROR: cp exit code was ${shellResult.code}.") - viewModel.state.postValue(true) - return@launch - } - viewModel.addLog(" Done!", false) - } - - private fun android6_9(context: Context) { - // Android 6.0 - 9.0 workflow - // - cd /data/data/me.hexile.odexpatcher/files/ - // - dex2oat base.apk - viewModel.addLog("[I] Running dex2oat…") - val shellResult = Shell.sh( - "cd ${App.context.getFileInFilesDir("").absolutePath} && dex2oat --dex-file=${Const.BASE_APK_FILE_NAME} --oat-file=${ - App.context.getFileInFilesDir(Const.BASE_ODEX_FILE_NAME).absolutePath - }" - ).exec() - if (!shellResult.isSuccess) { - viewModel.addLog("[E] ERROR: dex2oat exit code was ${shellResult.code}.") - viewModel.state.postValue(true) - return@launch - } - viewModel.addLog(" Done!", false) +object Dex2Oat { + fun run( + dexFile: String, + dexLocation: String, + oatFile: String = App.getContext().getFileInCacheDir(Const.BASE_ODEX_FILE_NAME).absolutePath + ): Boolean { + // Run dex2oat + val dex2OatCommand = "dex2oat --dex-file=$dexFile --dex-location=$dexLocation --oat-file=$oatFile --instruction-set=${Art.ISA} --instruction-set-variant=${Art.ISA_VARIANT} --instruction-set-features=${Art.ISA_FEATURES}" + logd("dex2oat", dex2OatCommand) + return Shell.sh(dex2OatCommand).exec().isSuccess } - - private fun android10(context: Context) { - // Android 10+ workflow - // - cd /data/data/me.hexile.odexpatcher/files/ - // - su dex2oat base.apk - viewModel.addLog("[I] Running dex2oat…") - val shellResult = Shell.su( - "cd ${App.context.getFileInFilesDir("").absolutePath} && dex2oat64 --dex-file=${Const.BASE_APK_FILE_NAME} --oat-file=${ - App.context.getFileInFilesDir(Const.BASE_ODEX_FILE_NAME).absolutePath - }" - ).exec() - if (!shellResult.isSuccess) { - viewModel.addLog("[E] ERROR: dex2oat exit code was ${shellResult.code}.") - viewModel.state.postValue(true) - return@launch - } - viewModel.addLog(" Done!", false) - }*/ } \ No newline at end of file diff --git a/app/src/main/java/me/hexile/odexpatcher/ktx/FilesExt.kt b/app/src/main/java/me/hexile/odexpatcher/ktx/FilesExt.kt index ce15dfc..fa3d1ed 100644 --- a/app/src/main/java/me/hexile/odexpatcher/ktx/FilesExt.kt +++ b/app/src/main/java/me/hexile/odexpatcher/ktx/FilesExt.kt @@ -17,6 +17,7 @@ package me.hexile.odexpatcher.ktx import android.content.Context +import com.topjohnwu.superuser.Shell import java.io.File import java.io.InputStream import java.io.RandomAccessFile @@ -31,14 +32,18 @@ fun File.copyInputStreamToFile(inputStream: InputStream) { } } -fun Context.getPackageBaseApk(packageName: String): String { - return this.packageManager.getPackageInfo(packageName, 0).applicationInfo.sourceDir +fun Context.getPackageApk(packageName: String): File { + return File(this.packageManager.getPackageInfo(packageName, 0).applicationInfo.sourceDir) } fun Context.getFileInFilesDir(filename: String): File { return File(this.filesDir.absolutePath, filename) } +fun Context.getFileInCacheDir(filename: String): File { + return File(this.cacheDir.absolutePath, filename) +} + fun RandomAccessFile.readIntLittleEndian(): Int { val data = ByteArray(4) this.read(data) @@ -68,4 +73,32 @@ fun RandomAccessFile.readBytes(offset: Long, amount: Int): ByteArray { fun RandomAccessFile.readBytes(offset: Int, amount: Int): ByteArray { return this.readBytes(offset.toLong(), amount) +} + +fun chown(path: String, uid: Int, gid: Int, recursive: Boolean = false): Boolean { + return Shell.sh("chown ${if (recursive) "-R" else ""} $uid:$gid $path").exec().isSuccess +} + +fun chmod(path: String, chmod: String, recursive: Boolean = false): Boolean { + return Shell.sh("chmod ${if (recursive) "-R" else ""} $chmod $path").exec().isSuccess +} + +fun restorecon(path: String, recursive: Boolean = false): Boolean { + return Shell.sh("restorecon ${if (recursive) "-R" else ""} $path").exec().isSuccess +} + +fun fixCacheFolderPermission(fileInCacheDir: File, recursive: Boolean = false): Boolean { + if (!chown(fileInCacheDir.absolutePath, selfAppUid(), cacheAppGid(selfAppUid()), recursive)) { + return false + } + + if (!chmod(fileInCacheDir.absolutePath, "600", recursive)) { + return false + } + + if (!restorecon(fileInCacheDir.absolutePath, recursive)) { + return false + } + + return true } \ No newline at end of file diff --git a/app/src/main/java/me/hexile/odexpatcher/viewmodels/MainViewModel.kt b/app/src/main/java/me/hexile/odexpatcher/viewmodels/MainViewModel.kt index 61b6c6a..08d508a 100644 --- a/app/src/main/java/me/hexile/odexpatcher/viewmodels/MainViewModel.kt +++ b/app/src/main/java/me/hexile/odexpatcher/viewmodels/MainViewModel.kt @@ -34,10 +34,10 @@ import kotlinx.coroutines.launch import me.hexile.odexpatcher.BuildConfig import me.hexile.odexpatcher.R import me.hexile.odexpatcher.art.Art +import me.hexile.odexpatcher.art.Dex2Oat import me.hexile.odexpatcher.art.oat.OatFile import me.hexile.odexpatcher.art.vdex.VdexFile import me.hexile.odexpatcher.core.App -import me.hexile.odexpatcher.core.Const import me.hexile.odexpatcher.core.SELinux import me.hexile.odexpatcher.core.utils.MediaStoreUtils import me.hexile.odexpatcher.core.utils.MediaStoreUtils.inputStream @@ -121,31 +121,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { data class SaveLogEvent(val path: String, val data: String) : Event() data class SnackBarStringRes(@StringRes val stringId: Int) : Event() data class SnackBarString(val string: String) : Event() - /*data class ShowSnackBarShare(val text: String, val data: String): Event() - data class ShowSnackBarShareUri(val text: String, val data: Uri): Event() - data class ShowSnackBar(val text: String): Event() - data class ShowToast(val text: String): Event()*/ - /*object NavigateToSettings: Event() - data class ShowSnackBar(val text: String): Event() - data class ShowSnackBarAction(val text: String, val actionText: String, val action: (View) -> Unit): Event() - data class ShowToast(val text: String): Event()*/ } private val eventChannel = Channel(BUFFERED) val eventsFlow = eventChannel.receiveAsFlow() - /*init { - viewModelScope.launch { - eventChannel.send(Event.ShowToast("Toast")) - } - }*/ - - /*fun settingsButtonClicked() { - viewModelScope.launch { - eventChannel.send(Event.NavigateToSettings) - } - }*/ - fun saveLog() = viewModelScope.launch(Dispatchers.IO) { eventChannel.send(Event.SnackBarString("Collecting data...")) @@ -209,458 +189,104 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { return@launch } - // FIXME: Move to utils + // TODO: Move all of this stuff to a use case? + + // TODO: Step 1 - check root access if (!Shell.rootAccess()) { - //viewModel.addLog("[E] ERROR: No root access! This app won't work without it.") - //viewModel.status.postValue("ERROR: " + getString(R.string.error_no_root_access)) - //viewModel.state.postValue(true) - loge("patch", "not root access") + loge("patch", "No root access!") eventChannel.send(Event.SnackBarStringRes(R.string.error_no_root_access)) return@launch } - val targetApk = context.getPackageBaseApk(targetPackage.value!!) - val baseFolder = File(targetApk).parentFile!!.absolutePath - val dexFile = targetApk.extractFilename() - val baseApk = context.getFileInFilesDir(dexFile) + val targetApk = context.getPackageApk(targetPackage.value!!) + val baseFolder = targetApk.parentFile!!.absolutePath + val dexFile = targetApk.name + val baseApk = context.getFileInCacheDir(dexFile) + // TODO: Step 2 - copy to cache folder // Copy input file to private app folder contentResolver.openInputStream(inputFileUri.value!!)?.let { baseApk.copyInputStreamToFile(it) } + // TODO: Step 3 - check if zip / check if dex // TODO(2.1.0): Add support for single dex files // Check if input file is zip try { ZipFile(baseApk) } catch (e: ZipException) { - //viewModel.addLog("[E] ERROR: " + getString(R.string.error_file_not_zip)) - //viewModel.state.postValue(true) - loge("patch", "not a zip") + loge("patch", "Input file is not a zip.") eventChannel.send(Event.SnackBarStringRes(R.string.error_file_not_zip)) return@launch } + // TODO: Step 4 - extract original checksums // Extract dex files checksums - val sourceClasses = extractClassesDex(baseApk) - val targetClasses = extractClassesDex(targetApk) - /*val targetOatChecksums = OatFile(File(Art.getOatFile(targetApk))).checksums - val targetClasses = ArrayList() - targetOatChecksums.asSequence().iterator().forEach { - targetClasses.add(it.second) - }*/ - + val sourceClasses = extractChecksums(baseApk) + val targetClasses = extractChecksums(targetApk) logd("patch", "--- Checksums ---") logd("patch", "source: ${sourceClasses.toHexString()}") - logd("patch", "target: ${sourceClasses.toHexString()}") + logd("patch", "target: ${targetClasses.toHexString()}") + // TODO: Step 5 - check same amount of checksums // TODO(2.1.0): Auto generate dummy classes.dex to match // TODO: System apps sometimes are stripped of classes.dex, so this won't work // Maybe parse the .odex and .vdex files instead. // Check if same number of classes.dex in APK if (sourceClasses.size != targetClasses.size) { - //viewModel.addLog("[E] ERROR: " + getString(R.string.error_different_dex_count)) - //viewModel.state.postValue(true) loge("patch", "source.size != target.size") eventChannel.send(Event.SnackBarStringRes(R.string.error_different_dex_count)) return@launch } - // Run dex2oat on source APK - when { - Build.VERSION.SDK_INT < Build.VERSION_CODES.M -> { - // Android 4.4 - 5.1 workflow - // - copy from /data/app/packagename.apk to /data/data/me.hexile.odexpatcher/files/backup.apk - // - copy from /data/data/me.hexile.odexpatcher/files/base.apk to /data/app/packagename.apk - // - dex2oat /data/app/packagename.apk - // - copy from /data/data/me.hexile.odexpatcher/files/backup.apk to /data/app/packagename.apk - val backupApk = context.getFileInFilesDir("backup.apk").absolutePath - - //viewModel.addLog("[I] Backing up target apk…") - var shellResult = Shell.su("cp $targetApk $backupApk").exec() - if (!shellResult.isSuccess) { - //viewModel.addLog("[E] ERROR: cp exit code was ${shellResult.code}.") - //viewModel.state.postValue(true) - eventChannel.send(Event.SnackBarString("ERROR: cp exit code was ${shellResult.code}.")) - return@launch - } - //viewModel.addLog(" Done!", false) - - //viewModel.addLog("[I] Copying over input file…") - shellResult = Shell.su("cp ${baseApk.absolutePath} $targetApk").exec() - if (!shellResult.isSuccess) { - //viewModel.addLog("[E] ERROR: cp exit code was ${shellResult.code}.") - //viewModel.state.postValue(true) - eventChannel.send(Event.SnackBarString("ERROR: cp exit code was ${shellResult.code}.")) - return@launch - } - //viewModel.addLog(" Done!", false) - - //viewModel.addLog("[I] Running dex2oat…") - shellResult = Shell.sh( - "dex2oat --dex-file=$targetApk --oat-file=${ - context.getFileInFilesDir(Const.BASE_ODEX_FILE_NAME).absolutePath - }" - ).exec() - if (!shellResult.isSuccess) { - //viewModel.addLog("[E] ERROR: dex2oat exit code was ${shellResult.code}.") - //viewModel.state.postValue(true) - eventChannel.send(Event.SnackBarString("ERROR: dex2oat exit code was ${shellResult.code}.")) - return@launch - } - //viewModel.addLog(" Done!", false) - - //viewModel.addLog("[I] Restoring backup…") - shellResult = Shell.su("cp $backupApk $targetApk").exec() - if (!shellResult.isSuccess) { - //viewModel.addLog("[E] ERROR: cp exit code was ${shellResult.code}.") - //viewModel.state.postValue(true) - eventChannel.send(Event.SnackBarString("ERROR: cp exit code was ${shellResult.code}.")) - return@launch - } - //viewModel.addLog(" Done!", false) - } - Build.VERSION.SDK_INT < Build.VERSION_CODES.Q -> { - // Android 6.0 - 9.0 workflow - // - cd /data/data/me.hexile.odexpatcher/files/ - // - dex2oat base.apk - //viewModel.addLog("[I] Running dex2oat…") - val shellResult = Shell.sh( - "cd ${context.getFileInFilesDir("").absolutePath} && dex2oat --dex-file=${dexFile} --dex-location=base.apk --oat-file=${ - context.getFileInFilesDir(Const.BASE_ODEX_FILE_NAME).absolutePath - }" - ).exec() - - // Fix permissions - val uid = context.packageManager.getApplicationInfo(context.packageName, 0).uid - val gid = uid //(uid % 100000) - 10000 + 50000 - Shell.su( - "chown ${uid}:${gid} ${context.getFileInFilesDir(Const.BASE_ODEX_FILE_NAME).absolutePath} && chmod 600 ${ - context.getFileInFilesDir( - Const.BASE_ODEX_FILE_NAME - ).absolutePath - }" - ).exec() - - // SELinux - Shell.su("restorecon -Rv ${context.getFileInFilesDir("").absolutePath}").exec() - - if (!shellResult.isSuccess) { - //viewModel.addLog("[E] ERROR: dex2oat exit code was ${shellResult.code}.") - //viewModel.state.postValue(true) - eventChannel.send(Event.SnackBarString("ERROR: dex2oat exit code was ${shellResult.code}.")) - return@launch - } - //viewModel.addLog(" Done!", false) - - Shell.sh( - "cp ${context.getFileInFilesDir(Const.BASE_ODEX_FILE_NAME).absolutePath} ${ - context.getFileInFilesDir( - Const.BASE_ODEX_FILE_NAME - ).absolutePath - }.bak" - ).exec() - } - else -> { - // Android 10+ workflow - // - cd /data/data/me.hexile.odexpatcher/files/ - // - su dex2oat base.apk - //viewModel.addLog("[I] Running dex2oat…") - val command = - "dex2oat --dex-file=${baseApk.absolutePath} --dex-location=base.apk --oat-file=${ - context.getFileInFilesDir(Const.BASE_ODEX_FILE_NAME).absolutePath - } --instruction-set=${Art.ISA} --instruction-set-variant=${Art.ISA_VARIANT} --instruction-set-features=${Art.ISA_FEATURES}" // Dex2Oat.command - - logd("patch", command) - - val shellResult = Shell.su(command).exec() - if (!shellResult.isSuccess) { - //viewModel.addLog("[E] ERROR: dex2oat exit code was ${shellResult.code}.") - //viewModel.state.postValue(true) - loge("patch", "dex2oat exit code was ${shellResult.code}.") - eventChannel.send(Event.SnackBarString("ERROR: dex2oat exit code was ${shellResult.code}.")) - return@launch - } - - // Fix permissions - val uid = context.packageManager.getApplicationInfo(context.packageName, 0).uid - val gid = uid //(uid % 100000) - 10000 + 50000 - Shell.su( - "chown ${uid}:${gid} ${context.getFileInFilesDir(Const.BASE_ODEX_FILE_NAME).absolutePath} && chmod 600 ${ - context.getFileInFilesDir( - Const.BASE_ODEX_FILE_NAME - ).absolutePath - }" - ).exec() - - Shell.su( - "chown ${uid}:${gid} ${context.getFileInFilesDir(Const.BASE_VDEX_FILE_NAME).absolutePath} && chmod 600 ${ - context.getFileInFilesDir( - Const.BASE_VDEX_FILE_NAME - ).absolutePath - }" - ).exec() - - // SELinux - Shell.su("restorecon -Rv ${context.getFileInFilesDir("").absolutePath}").exec() - //viewModel.addLog(" Done!", false) - } - } - - // Fix permissions - /*val appUid = (App.context.packageManager.getApplicationInfo(App.context.packageName, 0).uid % 100000) - Shell.su("chown $appUid:$appUid ${App.context.getFileInFilesDir("*")}").exec() - Shell.su("chmod 600 ${App.context.getFileInFilesDir("*")}").exec() - if (SELinux.isEnabled()) { - Shell.su("chcon u:object_r:app_data_file:s0 ${App.context.getFileInFilesDir("*")}").exec() - }*/ + // TODO: Step 6 - run dex2oat on input file + // Run dex2oat on input file + val outputOatFile = context.getFileInCacheDir("base.odex") + val outputVdexFile = context.getFileInCacheDir("base.vdex") + Dex2Oat.run(baseApk.absolutePath, dexFile) - //println(context.openFileInput(Const.BASE_ODEX_FILE_NAME).readBytes().copyOfRange(0, 4)) + // TODO: Step 7 - fix permissions + fixCacheFolderPermission(outputOatFile) + fixCacheFolderPermission(outputVdexFile) - // Patch files - //viewModel.addLog("[I] Patching oat file…") - val oatFile = OatFile(context.getFileInFilesDir(Const.BASE_ODEX_FILE_NAME)) + // TODO: Step 8.1 - patch oat checksums + // Patch oat + val oatFile = OatFile(outputOatFile) logd("patch", oatFile.toString()) oatFile.patch(targetClasses) - /*val oatFile1 = OatFile(context.getFileInFilesDir(Const.BASE_ODEX_FILE_NAME)) - println(oatFile1)*/ - /*try { - OatFile(App.context.getFileInFilesDir(Const.BASE_ODEX_FILE_NAME)).patch( - targetClasses - ) - } catch (e: Exhttps://developer.android.com/training/data-storage/app-specific?hl=el#kotlinception) { - e.printStackTrace() - viewModel.addLog("[E] ERROR: ${e.message}") - viewModel.state.postValue(true) - return@launch - }*/ - //viewModel.addLog(" Done!", false) - - // DexLocationToOdexFilename: https://cs.android.com/android/platform/superproject/+/master:art/runtime/oat_file_assistant.cc;l=532 - - // https://cs.android.com/android/platform/superproject/+/master:art/runtime/oat_file_assistant.cc;l=807 - // https://cs.android.com/android/platform/superproject/+/master:art/runtime/oat_file_assistant.cc;l=429 - // https://cs.android.com/android/platform/superproject/+/master:art/runtime/oat_file_assistant.cc;l=348 - // 2021-11-08 17:46:59.483 633-709/system_process E/system_server: Dex checksum does not match for dex: /data/app/me.hexile.sara.singletextview-fNTidlXzViTslDfBSLOZAA==/base.apk.Expected: 4175936928, actual: 1970187948 - + // TODO: Step 8.2 - patch vdex checksums (optional) + // Patch vdex if (isSdkGreaterThan(Build.VERSION_CODES.O)) { - val vdexFile = VdexFile(context.getFileInFilesDir(Const.BASE_VDEX_FILE_NAME)) + val vdexFile = VdexFile(outputVdexFile) logd("patch", vdexFile.toString()) vdexFile.patch(targetClasses) - //viewModel.addLog("[I] Patching vdex file…") - /*try { - VdexFile(App.context.getFileInFilesDir(Const.BASE_VDEX_FILE_NAME)).patch( - targetClasses - ) - } catch (e: Exception) { - viewModel.addLog("[E] ERROR: ${e.message}") - viewModel.state.postValue(true) - return@launch - }*/ - //viewModel.addLog(" Done!", false) } + // TODO: Step 9.1 - create missing folder (optional) // Create folder if non-existent - if (!File(Art.getOatFolder(targetApk)).isDirectory && isSdkGreaterThan(Build.VERSION_CODES.M)) { - // https://android.googlesource.com/platform/frameworks/native/+/refs/tags/android-12.0.0_r1/cmds/installd/InstalldNativeService.cpp#2573 - /* - if (fs_prepare_dir(oat_dir, S_IRWXU | S_IRWXG | S_IXOTH, AID_SYSTEM, AID_INSTALL)) { - return error("Failed to prepare " + oatDir); - } - */ - Shell.su("mkdir -p ${Art.getOatFolder(targetApk)} && chown -R system:install ${baseFolder}/oat && chmod -R 771 ${baseFolder}/oat") - .exec() + if (!File(Art.getOatFolder(targetApk.absolutePath)).isDirectory && isSdkGreaterThan(Build.VERSION_CODES.M)) { + Shell.sh("mkdir -p ${Art.getOatFolder(targetApk.absolutePath)}").exec() } - // Replace original files with patched ones - //viewModel.addLog("[I] Replacing odex file…") - val command = "cp ${context.getFileInFilesDir(Const.BASE_ODEX_FILE_NAME).absolutePath} ${ - Art.getOatFile(targetApk) - }" - logd("patch", command) - var shellResult = Shell.su(command).exec() - if (!shellResult.isSuccess) { - //viewModel.addLog("[E] ERROR: cp exit code was ${shellResult.code}.") - //viewModel.state.postValue(true) - loge("patch", "cp exit code was ${shellResult.code}.") - eventChannel.send(Event.SnackBarString("ERROR: PATCH cp exit code was ${shellResult.code}.")) - return@launch - } - //viewModel.addLog(" Done!", false) + // TODO: Step 9.2 - replace original oat file + Shell.sh("cp $outputOatFile ${Art.getOatFile(targetApk.absolutePath)}").exec() + // TODO: Step 9.3 - replace original vdex file (optional) if (isSdkGreaterThan(Build.VERSION_CODES.O)) { - //viewModel.addLog("[I] Replacing vdex file…") - shellResult = - Shell.su( - "cp ${context.getFileInFilesDir(Const.BASE_VDEX_FILE_NAME).absolutePath} ${ - Art.getVdexFile( - targetApk - ) - }" - ).exec() - if (!shellResult.isSuccess) { - //viewModel.addLog("[E] ERROR: cp exit code was ${shellResult.code}.") - //viewModel.state.postValue(true) - loge("patch", "cp exit code was ${shellResult.code}.") - eventChannel.send(Event.SnackBarString("ERROR: cp exit code was ${shellResult.code}.")) - return@launch - } - //viewModel.addLog(" Done!", false) - } - - eventChannel.send(Event.SnackBarString("DONE!")) - - // Fix permissions - /*if (isSdkGreaterThan(Build.VERSION_CODES.M)) { - val appUid = App.context.packageManager.getApplicationInfo(targetPackage.value!!, 0).uid - // https://cs.android.com/android/platform/superproject/+/master:system/core/libcutils/include/private/android_filesystem_config.h - // AID_USER_OFFSET = 100000 - // AID_APP_START = 10000 - // AID_SHARED_GID_START = 50000 - val gid = (appUid % 100000) - 10000 + 50000 - // https://cs.android.com/android/platform/superproject/+/master:frameworks/native/cmds/installd/dexopt.cpp;l=816 - //Shell.su("chown system:${appUid} ${Art.getOatFolder(targetApk)}* && chmod 644 ${Art.getOatFolder(targetApk)}*").exec() - }*/ - - /*try { - val bytes = requireContext().contentResolver.openInputStream(viewModel.inputFileUri.value!!)?.readBytes() - if (bytes != null) { - println(bytes.copyOfRange(0, 4).toHexString()) - } - } catch (fnfe: FileNotFoundException) { - // ContentResolver.openInputStream() throws FileNotFoundException - binding.root.showSnackbar("Cannot read input file!", Snackbar.LENGTH_SHORT) - viewModel.inputFile.value = null - viewModel.inputFileUri.value = null - }*/ - } - -// fun saveLog() = withExternalRW { -// viewModelScope.launch(Dispatchers.IO) { -// val filename = "odexpatcher_%s.txt".format(now.toTime(timeFormatStandard)) -// logd("OP_saveLog", filename) -// println(cr) -// -// //val displayName = filename -// -// //val values = ContentValues() -// //values.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) -// //values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) -// -// //val uri = cr.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) ?: throw IOException("Failed to insert $displayName.") -// -// /*val uri: Uri = getContentResolver().insert( -// MediaStore.Files.getContentUri("external"), -// values -// ) //important!*/ -// -// MediaStoreUtils.getFile(filename).outputStream().bufferedWriter().use { file -> -// file.write("---System Properties---\n\n") -// ProcessBuilder("getprop").start() -// .inputStream.reader().use { it.copyTo(file) } -// -// file.write("\n---Logcat---\n") -// file.write("${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})\n\n") -// ProcessBuilder("logcat", "-d", "--pid=${android.os.Process.myPid()}", "-s", "OdexPatcher").start() -// .inputStream.reader().use { it.copyTo(file) } -// } -// -// /*val cursor = cr.query(uri, null, null, null, null) -// DatabaseUtils.dumpCursor(cursor)*/ -// -// //val logfile = getFile(filename, true) -// /*val logFile = MediaStoreUtils.getFile(filename, true) -// logFile.uri.outputStream().bufferedWriter().use { file -> -// file.write("---Detected Device Info---\n\n") -// file.write("isAB=${Info.isAB}\n") -// file.write("isSAR=${Info.isSAR}\n") -// file.write("ramdisk=${Info.ramdisk}\n") -// -// file.write("\n\n---System Properties---\n\n") -// ProcessBuilder("getprop").start() -// .inputStream.reader().use { it.copyTo(file) } -// -// file.write("\n\n---System MountInfo---\n\n") -// FileInputStream("/proc/self/mountinfo").reader().use { it.copyTo(file) } -// -// file.write("\n---Magisk Logs---\n") -// file.write("${Info.env.magiskVersionString} (${Info.env.magiskVersionCode})\n\n") -// file.write(consoleText) -// -// file.write("\n---Manager Logs---\n") -// file.write("${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})\n\n") -// ProcessBuilder("logcat", "-d").start() -// .inputStream.reader().use { it.copyTo(file) } -// } -// SnackbarEvent(logFile.toString()).publish()*/ -// } -// } - - /*val baseFile = MutableLiveData() - val baseFileUri = MutableLiveData() - val targetPackage = MutableLiveData() - val state = MutableLiveData() - val installedApps = MutableLiveData>() - val logs = MutableLiveData() - val status = MutableLiveData()*/ - - /*fun setBaseFile(file: String) { - baseFile.value = file - } - - fun getBaseFile(): LiveData { - return baseFile - } - - fun addToInstalledApps(appInfo: AppInfo) { - installedApps.value?.add(appInfo) - installedApps.value = installedApps.value - } - - fun refreshInstalledApps() { - val apps: ArrayList = ArrayList() - - val packageManager = App.context.packageManager - val packages = packageManager.getInstalledPackages(PackageManager.GET_META_DATA) - - for (packageInfo in packages) { - // Skip same package - if (packageInfo.packageName == App.context.packageName) { - continue - } - - // TODO: Add filter to show system apps - if (isUserApp(packageInfo)) { - apps.add( - AppInfo( - name = packageManager.getApplicationLabel(packageInfo.applicationInfo) - .toString(), - packageName = packageInfo.packageName, - versionCode = PackageInfoCompat.getLongVersionCode(packageInfo), - versionName = packageInfo.versionName, - icon = App.context.packageManager.getApplicationIcon(packageInfo.applicationInfo) - ) - ) - } + Shell.sh("cp $outputVdexFile ${Art.getVdexFile(targetApk.absolutePath)}").exec() } - installedApps.value = apps - } + // TODO: Step 10 - fix permissions + chown("$baseFolder/oat", 1000, 1012, true) + chmod("$baseFolder/oat", "771", true) + chown(Art.getOatFile(targetApk.absolutePath), 1000, multiuserSharedAppGid(targetPackage.value!!)) + chmod(Art.getOatFile(targetApk.absolutePath), "644") + chown(Art.getVdexFile(targetApk.absolutePath), 1000, multiuserSharedAppGid(targetPackage.value!!)) + chmod(Art.getVdexFile(targetApk.absolutePath), "644") + restorecon("$baseFolder/oat", true) + restorecon("$baseFolder/oat", true) - fun addLog(line: String, newLine: Boolean = true) { - val n = if (newLine) { "\n" } else { "" } - logs.postValue(logs.value + "$n$line") + eventChannel.send(Event.SnackBarString("DONE!")) } - - init { - state.value = true - - installedApps.value = ArrayList() - - viewModelScope.launch { - refreshInstalledApps() - } - }*/ - } \ No newline at end of file