Fix saving content (1/2)
This commit is contained in:
parent
a23553e8ff
commit
0bd40ca4dd
|
|
@ -1,14 +1,21 @@
|
||||||
package org.lsposed.lspatch
|
package org.lsposed.lspatch
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.lsposed.patch.LSPatch
|
import org.lsposed.patch.LSPatch
|
||||||
import org.lsposed.patch.util.Logger
|
import org.lsposed.patch.util.Logger
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Files
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
|
||||||
object Patcher {
|
object Patcher {
|
||||||
class Options(
|
class Options(
|
||||||
private val apkPaths: Array<String>,
|
private val apkPaths: Array<String>,
|
||||||
private val outputPath: String,
|
|
||||||
private val debuggable: Boolean,
|
private val debuggable: Boolean,
|
||||||
private val sigbypassLevel: Int,
|
private val sigbypassLevel: Int,
|
||||||
private val v1: Boolean,
|
private val v1: Boolean,
|
||||||
|
|
@ -19,9 +26,10 @@ object Patcher {
|
||||||
private val verbose: Boolean,
|
private val verbose: Boolean,
|
||||||
private val embeddedModules: List<String>
|
private val embeddedModules: List<String>
|
||||||
) {
|
) {
|
||||||
|
lateinit var outputPath: String
|
||||||
|
|
||||||
fun toStringArray(): Array<String> {
|
fun toStringArray(): Array<String> {
|
||||||
return arrayListOf<String>().run {
|
return arrayListOf<String>().run {
|
||||||
add("-f")
|
|
||||||
addAll(apkPaths)
|
addAll(apkPaths)
|
||||||
add("-o"); add(outputPath)
|
add("-o"); add(outputPath)
|
||||||
if (debuggable) add("-d")
|
if (debuggable) add("-d")
|
||||||
|
|
@ -41,9 +49,31 @@ object Patcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun patch(logger: Logger, options: Options) {
|
suspend fun patch(context: Context, logger: Logger, options: Options) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
val download = "${Environment.DIRECTORY_DOWNLOADS}/LSPatch"
|
||||||
|
options.outputPath = Files.createTempDirectory("patch").absolutePathString()
|
||||||
LSPatch(logger, *options.toStringArray()).doCommandLine()
|
LSPatch(logger, *options.toStringArray()).doCommandLine()
|
||||||
|
File(options.outputPath)
|
||||||
|
.walk()
|
||||||
|
.filter { it.isFile }
|
||||||
|
.forEach {
|
||||||
|
//FIXME: Android 9
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val contentDetails = ContentValues().apply {
|
||||||
|
put(MediaStore.Downloads.DISPLAY_NAME, it.name)
|
||||||
|
put(MediaStore.Downloads.RELATIVE_PATH, download)
|
||||||
|
}
|
||||||
|
val uri = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentDetails)
|
||||||
|
?: throw IllegalStateException("Failed to save files to Download")
|
||||||
|
it.inputStream().use { input ->
|
||||||
|
context.contentResolver.openOutputStream(uri)!!.use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.i("Patched files are saved to $download")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package org.lsposed.lspatch.ui.page
|
package org.lsposed.lspatch.ui.page
|
||||||
|
|
||||||
import android.os.Environment
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.Spring
|
import androidx.compose.animation.core.Spring
|
||||||
|
|
@ -25,6 +24,7 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -43,9 +43,10 @@ import org.lsposed.lspatch.ui.util.lastItemIndex
|
||||||
import org.lsposed.lspatch.ui.util.observeState
|
import org.lsposed.lspatch.ui.util.observeState
|
||||||
import org.lsposed.lspatch.ui.viewmodel.AppInfo
|
import org.lsposed.lspatch.ui.viewmodel.AppInfo
|
||||||
import org.lsposed.patch.util.Logger
|
import org.lsposed.patch.util.Logger
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
enum class PatchState {
|
enum class PatchState {
|
||||||
SELECTING, CONFIGURING, SUBMITTING, PATCHING, FINISHED
|
SELECTING, CONFIGURING, SUBMITTING, PATCHING, FINISHED, ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
class NewPatchPageViewModel : ViewModel() {
|
class NewPatchPageViewModel : ViewModel() {
|
||||||
|
|
@ -76,7 +77,7 @@ fun NewPatchPage() {
|
||||||
when (viewModel.patchState) {
|
when (viewModel.patchState) {
|
||||||
PatchState.SELECTING -> navController.navigate(PageList.SelectApps.name + "/false")
|
PatchState.SELECTING -> navController.navigate(PageList.SelectApps.name + "/false")
|
||||||
PatchState.CONFIGURING, PatchState.SUBMITTING -> PatchOptionsPage(patchApp!!)
|
PatchState.CONFIGURING, PatchState.SUBMITTING -> PatchOptionsPage(patchApp!!)
|
||||||
PatchState.PATCHING, PatchState.FINISHED -> DoPatchPage(viewModel.patchOptions!!)
|
PatchState.PATCHING, PatchState.FINISHED, PatchState.ERROR -> DoPatchPage(viewModel.patchOptions!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,11 +96,9 @@ private fun PatchOptionsPage(patchApp: AppInfo) {
|
||||||
.savedStateHandle.getLiveData<SnapshotStateList<AppInfo>>("selected", SnapshotStateList())
|
.savedStateHandle.getLiveData<SnapshotStateList<AppInfo>>("selected", SnapshotStateList())
|
||||||
|
|
||||||
if (viewModel.patchState == PatchState.SUBMITTING) LaunchedEffect(patchApp) {
|
if (viewModel.patchState == PatchState.SUBMITTING) LaunchedEffect(patchApp) {
|
||||||
val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path
|
|
||||||
if (useManager) embeddedModules.value?.clear()
|
if (useManager) embeddedModules.value?.clear()
|
||||||
viewModel.patchOptions = Patcher.Options(
|
viewModel.patchOptions = Patcher.Options(
|
||||||
apkPaths = arrayOf(patchApp.app.sourceDir), // TODO: Split Apk
|
apkPaths = arrayOf(patchApp.app.sourceDir), // TODO: Split Apk
|
||||||
outputPath = downloadDir,
|
|
||||||
debuggable = debuggable,
|
debuggable = debuggable,
|
||||||
sigbypassLevel = sigBypassLevel,
|
sigbypassLevel = sigBypassLevel,
|
||||||
v1 = v1, v2 = v2, v3 = v3,
|
v1 = v1, v2 = v2, v3 = v3,
|
||||||
|
|
@ -194,6 +193,7 @@ private fun PatchOptionsPage(patchApp: AppInfo) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DoPatchPage(patcherOptions: Patcher.Options) {
|
private fun DoPatchPage(patcherOptions: Patcher.Options) {
|
||||||
|
val context = LocalContext.current
|
||||||
val viewModel = viewModel<NewPatchPageViewModel>()
|
val viewModel = viewModel<NewPatchPageViewModel>()
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val logs = remember { mutableStateListOf<Pair<Int, String>>() }
|
val logs = remember { mutableStateListOf<Pair<Int, String>>() }
|
||||||
|
|
@ -219,8 +219,16 @@ private fun DoPatchPage(patcherOptions: Patcher.Options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(patcherOptions) {
|
LaunchedEffect(patcherOptions) {
|
||||||
Patcher.patch(logger, patcherOptions)
|
try {
|
||||||
|
Patcher.patch(context, logger, patcherOptions)
|
||||||
viewModel.patchState = PatchState.FINISHED
|
viewModel.patchState = PatchState.FINISHED
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
logger.e(t.message.orEmpty())
|
||||||
|
logger.e(t.stackTraceToString())
|
||||||
|
viewModel.patchState = PatchState.ERROR
|
||||||
|
} finally {
|
||||||
|
File(patcherOptions.outputPath).deleteRecursively()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -230,8 +238,7 @@ private fun DoPatchPage(patcherOptions: Patcher.Options) {
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.animateContentSize(spring(stiffness = Spring.StiffnessLow))
|
.animateContentSize(spring(stiffness = Spring.StiffnessLow))
|
||||||
) {
|
) {
|
||||||
val patching by remember { derivedStateOf { viewModel.patchState == PatchState.PATCHING } }
|
ShimmerAnimation(enabled = viewModel.patchState == PatchState.PATCHING) {
|
||||||
ShimmerAnimation(enabled = patching) {
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTextStyle provides MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace)
|
LocalTextStyle provides MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace)
|
||||||
) {
|
) {
|
||||||
|
|
@ -262,7 +269,7 @@ private fun DoPatchPage(patcherOptions: Patcher.Options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!patching) {
|
if (viewModel.patchState == PatchState.FINISHED) {
|
||||||
Row(Modifier.padding(top = 12.dp)) {
|
Row(Modifier.padding(top = 12.dp)) {
|
||||||
Button(
|
Button(
|
||||||
onClick = { navController.popBackStack() },
|
onClick = { navController.popBackStack() },
|
||||||
|
|
|
||||||
|
|
@ -173,14 +173,13 @@ public class LSPatch {
|
||||||
if (!srcApkFile.exists())
|
if (!srcApkFile.exists())
|
||||||
throw new PatchError("The source apk file does not exit. Please provide a correct path.");
|
throw new PatchError("The source apk file does not exit. Please provide a correct path.");
|
||||||
|
|
||||||
File tmpApk = Files.createTempFile(srcApkFile.getName(), "unsigned").toFile();
|
outputFile.delete();
|
||||||
tmpApk.delete();
|
|
||||||
|
|
||||||
logger.d("apk path: " + srcApkFile);
|
logger.d("apk path: " + srcApkFile);
|
||||||
|
|
||||||
logger.i("Parsing original apk...");
|
logger.i("Parsing original apk...");
|
||||||
|
|
||||||
try (var dstZFile = ZFile.openReadWrite(tmpApk, Z_FILE_OPTIONS);
|
try (var dstZFile = ZFile.openReadWrite(outputFile, Z_FILE_OPTIONS);
|
||||||
var srcZFile = dstZFile.addNestedZip((ignore) -> ORIGINAL_APK_ASSET_PATH, srcApkFile, false)) {
|
var srcZFile = dstZFile.addNestedZip((ignore) -> ORIGINAL_APK_ASSET_PATH, srcApkFile, false)) {
|
||||||
|
|
||||||
// sign apk
|
// sign apk
|
||||||
|
|
@ -313,15 +312,8 @@ public class LSPatch {
|
||||||
dstZFile.realign();
|
dstZFile.realign();
|
||||||
|
|
||||||
logger.i("Writing apk...");
|
logger.i("Writing apk...");
|
||||||
} finally {
|
}
|
||||||
try {
|
|
||||||
outputFile.delete();
|
|
||||||
FileUtils.moveFile(tmpApk, outputFile);
|
|
||||||
logger.i("Done. Output APK: " + outputFile.getAbsolutePath());
|
logger.i("Done. Output APK: " + outputFile.getAbsolutePath());
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new PatchError("Error writing apk", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void embedModules(ZFile zFile) {
|
private void embedModules(ZFile zFile) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue