diff --git a/android/app/build.gradle b/android/app/build.gradle index ba45320a..ade5350c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -44,11 +44,10 @@ android { defaultConfig { applicationId "app.revanced.manager" - minSdkVersion flutter.minSdkVersion + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName - multiDexEnabled true } buildTypes { @@ -64,8 +63,11 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "com.android.support:multidex:1.0.3" // ReVanced implementation "app.revanced:revanced-patcher:3.3.1" + + // Signing & aligning + implementation("org.bouncycastle:bcpkix-jdk15on:1.70") + implementation("com.android.tools.build:apksig:7.2.1") } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c50efe30..4ae965b5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,10 @@ + + + + + + + + diff --git a/android/app/src/main/jniLibs/arm64-v8a/aapt.so b/android/app/src/main/jniLibs/arm64-v8a/aapt.so new file mode 100644 index 00000000..4014ef05 Binary files /dev/null and b/android/app/src/main/jniLibs/arm64-v8a/aapt.so differ diff --git a/android/app/src/main/jniLibs/armeabi-v7a/to_be_removed.so b/android/app/src/main/jniLibs/armeabi-v7a/to_be_removed.so new file mode 100644 index 00000000..797d416c Binary files /dev/null and b/android/app/src/main/jniLibs/armeabi-v7a/to_be_removed.so differ diff --git a/android/app/src/main/jniLibs/x86/aapt2.so b/android/app/src/main/jniLibs/x86/aapt2.so new file mode 100644 index 00000000..097b817e Binary files /dev/null and b/android/app/src/main/jniLibs/x86/aapt2.so differ diff --git a/android/app/src/main/jniLibs/x86_64/to_be_removed.so b/android/app/src/main/jniLibs/x86_64/to_be_removed.so new file mode 100644 index 00000000..b86c3970 Binary files /dev/null and b/android/app/src/main/jniLibs/x86_64/to_be_removed.so differ diff --git a/android/app/src/main/kotlin/app/revanced/manager/MainActivity.kt b/android/app/src/main/kotlin/app/revanced/manager/MainActivity.kt index 1ea8e03d..d8714fcf 100644 --- a/android/app/src/main/kotlin/app/revanced/manager/MainActivity.kt +++ b/android/app/src/main/kotlin/app/revanced/manager/MainActivity.kt @@ -1,6 +1,13 @@ package app.revanced.manager import androidx.annotation.NonNull +import app.revanced.manager.utils.Aapt +import app.revanced.manager.utils.aligning.ZipAligner +import app.revanced.manager.utils.signing.Signer +import app.revanced.manager.utils.zip.ZipFile +import app.revanced.manager.utils.zip.structures.ZipEntry +import app.revanced.patcher.Patcher +import app.revanced.patcher.PatcherOptions import app.revanced.patcher.data.Data import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages import app.revanced.patcher.extensions.PatchExtensions.description @@ -12,20 +19,26 @@ import dalvik.system.DexClassLoader import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption class MainActivity : FlutterActivity() { - private val CHANNEL = "app.revanced/patcher" + private val CHANNEL = "app.revanced.manager/patcher" private var patches = mutableListOf>>() + private val tag = "Patcher" + private lateinit var methodChannel: MethodChannel + private lateinit var patcher: Patcher override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> + methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + methodChannel.setMethodCallHandler { call, result -> when (call.method) { "loadPatches" -> { val pathBundlesPaths = call.argument>("pathBundlesPaths") if (pathBundlesPaths != null) { - loadPatches(pathBundlesPaths) - result.success("OK") + result.success(loadPatches(pathBundlesPaths)) } else { result.notImplemented() } @@ -36,7 +49,61 @@ class MainActivity : FlutterActivity() { val targetVersion = call.argument("targetVersion") val ignoreVersion = call.argument("ignoreVersion") if (targetPackage != null && targetVersion != null && ignoreVersion != null) { - result.success(getFilteredPatches(targetPackage, targetVersion, ignoreVersion)) + result.success( + getFilteredPatches(targetPackage, targetVersion, ignoreVersion) + ) + } else { + result.notImplemented() + } + } + "copyInputFile" -> { + val originalFilePath = call.argument("originalFilePath") + val inputFilePath = call.argument("inputFilePath") + if (originalFilePath != null && inputFilePath != null) { + result.success(copyInputFile(originalFilePath, inputFilePath)) + } else { + result.notImplemented() + } + } + "createPatcher" -> { + val inputFilePath = call.argument("inputFilePath") + val cacheDirPath = call.argument("cacheDirPath") + if (inputFilePath != null && cacheDirPath != null) { + result.success(createPatcher(inputFilePath, cacheDirPath)) + } else { + result.notImplemented() + } + } + "mergeIntegrations" -> { + val integrationsPath = call.argument("integrationsPath") + if (integrationsPath != null) { + result.success(mergeIntegrations(integrationsPath)) + } else { + result.notImplemented() + } + } + "applyPatches" -> { + val selectedPatches = call.argument>("selectedPatches") + if (selectedPatches != null) { + result.success(applyPatches(selectedPatches)) + } else { + result.notImplemented() + } + } + "repackPatchedFile" -> { + val inputFilePath = call.argument("inputFilePath") + val patchedFilePath = call.argument("patchedFilePath") + if (inputFilePath != null && patchedFilePath != null) { + result.success(repackPatchedFile(inputFilePath, patchedFilePath)) + } else { + result.notImplemented() + } + } + "signPatchedFile" -> { + val patchedFilePath = call.argument("patchedFilePath") + val outFilePath = call.argument("outFilePath") + if (patchedFilePath != null && outFilePath != null) { + result.success(signPatchedFile(patchedFilePath, outFilePath)) } else { result.notImplemented() } @@ -46,42 +113,126 @@ class MainActivity : FlutterActivity() { } } - fun loadPatches(pathBundlesPaths: List) { - pathBundlesPaths.forEach { path -> - patches.addAll(DexPatchBundle( - path, DexClassLoader( - path, - context.cacheDir.path, - null, - javaClass.classLoader + fun loadPatches(pathBundlesPaths: List): Boolean { + try { + pathBundlesPaths.forEach { path -> + patches.addAll( + DexPatchBundle( + path, + DexClassLoader( + path, + context.cacheDir.path, + null, + javaClass.classLoader + ) + ) + .loadPatches() ) - ).loadPatches()) + } + } catch (e: Exception) { + return false } + return true } fun getCompatiblePackages(): List { val filteredPackages = mutableListOf() patches.forEach patch@{ patch -> - patch.compatiblePackages?.forEach { pkg -> - filteredPackages.add(pkg.name) - } + patch.compatiblePackages?.forEach { pkg -> filteredPackages.add(pkg.name) } } return filteredPackages.distinct() } - fun getFilteredPatches(targetPackage: String, targetVersion: String, ignoreVersion: Boolean): List> { + fun getFilteredPatches( + targetPackage: String, + targetVersion: String, + ignoreVersion: Boolean + ): List> { val filteredPatches = mutableListOf>() patches.forEach patch@{ patch -> patch.compatiblePackages?.forEach { pkg -> - if (pkg.name == targetPackage && (ignoreVersion || pkg.versions.isNotEmpty() || pkg.versions.contains(targetVersion))) { - var p = mutableMapOf(); - p.put("name", patch.patchName); - p.put("version", patch.version); - p.put("description", patch.description); + if (pkg.name == targetPackage && + (ignoreVersion || + pkg.versions.isNotEmpty() || + pkg.versions.contains(targetVersion)) + ) { + var p = mutableMapOf() + p.put("name", patch.patchName) + p.put("version", patch.version) + p.put("description", patch.description) filteredPatches.add(p) } } } return filteredPatches } + + private fun findPatchesByIds(ids: Iterable): List>> { + return patches.filter { patch -> ids.any { it == patch.patchName } } + } + + fun copyInputFile(originalFilePath: String, inputFilePath: String): Boolean { + val originalFile = File(originalFilePath) + val inputFile = File(inputFilePath) + Files.copy(originalFile.toPath(), inputFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + return true + } + + fun createPatcher(inputFilePath: String, cacheDirPath: String): Boolean { + val inputFile = File(inputFilePath) + val aaptPath = Aapt.binary(context).absolutePath + patcher = Patcher(PatcherOptions(inputFile, cacheDirPath, true, aaptPath, cacheDirPath)) + return true + } + + fun mergeIntegrations(integrationsPath: String): Boolean { + val integrations = File(integrationsPath) + if (patcher == null) return false + patcher.addFiles(listOf(integrations)) {} + return true + } + + fun applyPatches(selectedPatches: List): Boolean { + val patches = findPatchesByIds(selectedPatches) + if (patches.isEmpty()) return false + if (patcher == null) return false + patcher.addPatches(patches) + patcher.applyPatches().forEach { (patch, result) -> + if (result.isSuccess) { + val msg = "[success] $patch" + methodChannel.invokeMethod("updateInstallerLog", msg) + return@forEach + } + val msg = "[error] $patch:" + result.exceptionOrNull()!! + methodChannel.invokeMethod("updateInstallerLog", msg) + } + return true + } + + fun repackPatchedFile(inputFilePath: String, patchedFilePath: String): Boolean { + val inputFile = File(inputFilePath) + val patchedFile = File(patchedFilePath) + if (patcher == null) return false + val result = patcher.save() + ZipFile(patchedFile).use { file -> + result.dexFiles.forEach { + file.addEntryCompressData( + ZipEntry.createWithName(it.name), + it.dexFileInputStream.readBytes() + ) + } + result.resourceFile?.let { + file.copyEntriesFromFileAligned(ZipFile(it), ZipAligner::getEntryAlignment) + } + file.copyEntriesFromFileAligned(ZipFile(inputFile), ZipAligner::getEntryAlignment) + } + return true + } + + fun signPatchedFile(patchedFilePath: String, outFilePath: String): Boolean { + val patchedFile = File(patchedFilePath) + val outFile = File(outFilePath) + Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile) + return true + } } diff --git a/android/app/src/main/kotlin/app/revanced/manager/utils/Aapt.kt b/android/app/src/main/kotlin/app/revanced/manager/utils/Aapt.kt new file mode 100644 index 00000000..caa7500b --- /dev/null +++ b/android/app/src/main/kotlin/app/revanced/manager/utils/Aapt.kt @@ -0,0 +1,12 @@ +package app.revanced.manager.utils + +import android.content.Context +import java.io.File + +object Aapt { + fun binary(context: Context): File { + return File(context.applicationInfo.nativeLibraryDir).resolveAapt() + } +} + +private fun File.resolveAapt() = resolve(list { _, f -> !File(f).isDirectory }!!.first()) \ No newline at end of file diff --git a/android/app/src/main/kotlin/app/revanced/manager/utils/aligning/ZipAligner.kt b/android/app/src/main/kotlin/app/revanced/manager/utils/aligning/ZipAligner.kt new file mode 100644 index 00000000..af4e51da --- /dev/null +++ b/android/app/src/main/kotlin/app/revanced/manager/utils/aligning/ZipAligner.kt @@ -0,0 +1,11 @@ +package app.revanced.manager.utils.aligning + +import app.revanced.manager.utils.zip.structures.ZipEntry + +internal object ZipAligner { + private const val DEFAULT_ALIGNMENT = 4 + private const val LIBRARY_ALIGNMENT = 4096 + + fun getEntryAlignment(entry: ZipEntry): Int? = + if (entry.compression.toUInt() != 0u) null else if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT else DEFAULT_ALIGNMENT +} diff --git a/android/app/src/main/kotlin/app/revanced/manager/utils/signing/Signer.kt b/android/app/src/main/kotlin/app/revanced/manager/utils/signing/Signer.kt new file mode 100644 index 00000000..35e31d2d --- /dev/null +++ b/android/app/src/main/kotlin/app/revanced/manager/utils/signing/Signer.kt @@ -0,0 +1,75 @@ +package app.revanced.manager.utils.signing + +import com.android.apksig.ApkSigner +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.operator.ContentSigner +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.math.BigInteger +import java.security.* +import java.security.cert.X509Certificate +import java.util.* + +internal class Signer( + private val cn: String, password: String +) { + private val passwordCharArray = password.toCharArray() + private fun newKeystore(out: File) { + val (publicKey, privateKey) = createKey() + val privateKS = KeyStore.getInstance("BKS", "BC") + privateKS.load(null, passwordCharArray) + privateKS.setKeyEntry("alias", privateKey, passwordCharArray, arrayOf(publicKey)) + privateKS.store(FileOutputStream(out), passwordCharArray) + } + + private fun createKey(): Pair { + val gen = KeyPairGenerator.getInstance("RSA") + gen.initialize(2048) + val pair = gen.generateKeyPair() + var serialNumber: BigInteger + do serialNumber = + BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO) + val x500Name = X500Name("CN=$cn") + val builder = X509v3CertificateBuilder( + x500Name, + serialNumber, + Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L), + Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L), + Locale.ENGLISH, + x500Name, + SubjectPublicKeyInfo.getInstance(pair.public.encoded) + ) + val signer: ContentSigner = JcaContentSignerBuilder("SHA256withRSA").build(pair.private) + return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private + } + + fun signApk(input: File, output: File) { + Security.addProvider(BouncyCastleProvider()) + + val ks = File(input.parent, "revanced-cli.keystore") + if (!ks.exists()) newKeystore(ks) + + val keyStore = KeyStore.getInstance("BKS", "BC") + FileInputStream(ks).use { fis -> keyStore.load(fis, null) } + val alias = keyStore.aliases().nextElement() + + val config = ApkSigner.SignerConfig.Builder( + cn, + keyStore.getKey(alias, passwordCharArray) as PrivateKey, + listOf(keyStore.getCertificate(alias) as X509Certificate) + ).build() + + val signer = ApkSigner.Builder(listOf(config)) + signer.setCreatedBy(cn) + signer.setInputApk(input) + signer.setOutputApk(output) + + signer.build().sign() + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/app/revanced/manager/utils/zip/Extensions.kt b/android/app/src/main/kotlin/app/revanced/manager/utils/zip/Extensions.kt new file mode 100644 index 00000000..8be66822 --- /dev/null +++ b/android/app/src/main/kotlin/app/revanced/manager/utils/zip/Extensions.kt @@ -0,0 +1,33 @@ +package app.revanced.manager.utils.zip + +import java.io.DataInput +import java.io.DataOutput +import java.nio.ByteBuffer + +fun UInt.toLittleEndian() = + (((this.toInt() and 0xff000000.toInt()) shr 24) or ((this.toInt() and 0x00ff0000) shr 8) or ((this.toInt() and 0x0000ff00) shl 8) or (this.toInt() shl 24)).toUInt() + +fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort() + +fun UInt.toBigEndian() = (((this.toInt() and 0xff) shl 24) or ((this.toInt() and 0xff00) shl 8) + or ((this.toInt() and 0x00ff0000) ushr 8) or (this.toInt() ushr 24)).toUInt() + +fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort() + +fun ByteBuffer.getUShort() = this.short.toUShort() +fun ByteBuffer.getUInt() = this.int.toUInt() + +fun ByteBuffer.putUShort(ushort: UShort) = this.putShort(ushort.toShort()) +fun ByteBuffer.putUInt(uint: UInt) = this.putInt(uint.toInt()) + +fun DataInput.readUShort() = this.readShort().toUShort() +fun DataInput.readUInt() = this.readInt().toUInt() + +fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt()) +fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt()) + +fun DataInput.readUShortLE() = this.readUShort().toBigEndian() +fun DataInput.readUIntLE() = this.readUInt().toBigEndian() + +fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian()) +fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian()) diff --git a/android/app/src/main/kotlin/app/revanced/manager/utils/zip/ZipFile.kt b/android/app/src/main/kotlin/app/revanced/manager/utils/zip/ZipFile.kt new file mode 100644 index 00000000..5b8c5f8e --- /dev/null +++ b/android/app/src/main/kotlin/app/revanced/manager/utils/zip/ZipFile.kt @@ -0,0 +1,176 @@ +package app.revanced.manager.utils.zip + +import app.revanced.manager.utils.zip.structures.ZipEndRecord +import app.revanced.manager.utils.zip.structures.ZipEntry +import java.io.Closeable +import java.io.File +import java.io.RandomAccessFile +import java.nio.ByteBuffer +import java.nio.channels.FileChannel +import java.util.zip.CRC32 +import java.util.zip.Deflater + +class ZipFile(val file: File) : Closeable { + var entries: MutableList = mutableListOf() + + private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw") + private var CDNeedsRewrite = false + + private val compressionLevel = 5 + + init { + //if file isn't empty try to load entries + if (file.length() > 0) { + val endRecord = findEndRecord() + + if (endRecord.diskNumber > 0u || endRecord.totalEntries != endRecord.diskEntries) + throw IllegalArgumentException("Multi-file archives are not supported") + + entries = readEntries(endRecord).toMutableList() + } + + //seek back to start for writing + filePointer.seek(0) + } + + private fun findEndRecord(): ZipEndRecord { + //look from end to start since end record is at the end + for (i in filePointer.length() - 1 downTo 0) { + filePointer.seek(i) + //possible beginning of signature + if (filePointer.readByte() == 0x50.toByte()) { + //seek back to get the full int + filePointer.seek(i) + val possibleSignature = filePointer.readUIntLE() + if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) { + filePointer.seek(i) + return ZipEndRecord.fromECD(filePointer) + } + } + } + + throw Exception("Couldn't find end record") + } + + private fun readEntries(endRecord: ZipEndRecord): List { + filePointer.seek(endRecord.centralDirectoryStartOffset.toLong()) + + val numberOfEntries = endRecord.diskEntries.toInt() + + return buildList(numberOfEntries) { + for (i in 1..numberOfEntries) { + add( + ZipEntry.fromCDE(filePointer).also + { + //for some reason the local extra field can be different from the central one + it.readLocalExtra( + filePointer.channel.map( + FileChannel.MapMode.READ_ONLY, + it.localHeaderOffset.toLong() + 28, + 2 + ) + ) + }) + } + } + } + + private fun writeCD() { + val CDStart = filePointer.channel.position().toUInt() + + entries.forEach { + filePointer.channel.write(it.toCDE()) + } + + val entriesCount = entries.size.toUShort() + + val endRecord = ZipEndRecord( + 0u, + 0u, + entriesCount, + entriesCount, + filePointer.channel.position().toUInt() - CDStart, + CDStart, + "" + ) + + filePointer.channel.write(endRecord.toECD()) + } + + private fun addEntry(entry: ZipEntry, data: ByteBuffer) { + CDNeedsRewrite = true + + entry.localHeaderOffset = filePointer.channel.position().toUInt() + + filePointer.channel.write(entry.toLFH()) + filePointer.channel.write(data) + + entries.add(entry) + } + + fun addEntryCompressData(entry: ZipEntry, data: ByteArray) { + val compressor = Deflater(compressionLevel, true) + compressor.setInput(data) + compressor.finish() + + val uncompressedSize = data.size + val compressedData = + ByteArray(uncompressedSize) //i'm guessing compression won't make the data bigger + + val compressedDataLength = compressor.deflate(compressedData) + val compressedBuffer = + ByteBuffer.wrap(compressedData.take(compressedDataLength).toByteArray()) + + compressor.end() + + val crc = CRC32() + crc.update(data) + + entry.compression = 8u //deflate compression + entry.uncompressedSize = uncompressedSize.toUInt() + entry.compressedSize = compressedDataLength.toUInt() + entry.crc32 = crc.value.toUInt() + + addEntry(entry, compressedBuffer) + } + + fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) { + alignment?.let { alignment -> + //calculate where data would end up + val dataOffset = filePointer.filePointer + entry.LFHSize + + val mod = dataOffset % alignment + + //wrong alignment + if (mod != 0L) { + //add padding at end of extra field + entry.localExtraField = + entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt()) + } + } + + addEntry(entry, data) + } + + fun getDataForEntry(entry: ZipEntry): ByteBuffer { + return filePointer.channel.map( + FileChannel.MapMode.READ_ONLY, + entry.dataOffset.toLong(), + entry.compressedSize.toLong() + ) + } + + fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) { + for (entry in file.entries) { + if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates + + val data = file.getDataForEntry(entry) + addEntryCopyData(entry, data, entryAlignment(entry)) + } + } + + override fun close() { + if (CDNeedsRewrite) writeCD() + filePointer.close() + } +} diff --git a/android/app/src/main/kotlin/app/revanced/manager/utils/zip/structures/ZipEndRecord.kt b/android/app/src/main/kotlin/app/revanced/manager/utils/zip/structures/ZipEndRecord.kt new file mode 100644 index 00000000..9e4ff65d --- /dev/null +++ b/android/app/src/main/kotlin/app/revanced/manager/utils/zip/structures/ZipEndRecord.kt @@ -0,0 +1,78 @@ +package app.revanced.manager.utils.zip.structures + +import app.revanced.manager.utils.zip.putUInt +import app.revanced.manager.utils.zip.putUShort +import app.revanced.manager.utils.zip.readUIntLE +import app.revanced.manager.utils.zip.readUShortLE +import java.io.DataInput +import java.nio.ByteBuffer +import java.nio.ByteOrder + +data class ZipEndRecord( + val diskNumber: UShort, + val startingDiskNumber: UShort, + val diskEntries: UShort, + val totalEntries: UShort, + val centralDirectorySize: UInt, + val centralDirectoryStartOffset: UInt, + val fileComment: String, +) { + + companion object { + const val ECD_HEADER_SIZE = 22 + const val ECD_SIGNATURE = 0x06054b50u + + fun fromECD(input: DataInput): ZipEndRecord { + val signature = input.readUIntLE() + + if (signature != ECD_SIGNATURE) + throw IllegalArgumentException("Input doesn't start with end record signature") + + val diskNumber = input.readUShortLE() + val startingDiskNumber = input.readUShortLE() + val diskEntries = input.readUShortLE() + val totalEntries = input.readUShortLE() + val centralDirectorySize = input.readUIntLE() + val centralDirectoryStartOffset = input.readUIntLE() + val fileCommentLength = input.readUShortLE() + var fileComment = "" + + if (fileCommentLength > 0u) { + val fileCommentBytes = ByteArray(fileCommentLength.toInt()) + input.readFully(fileCommentBytes) + fileComment = fileCommentBytes.toString(Charsets.UTF_8) + } + + return ZipEndRecord( + diskNumber, + startingDiskNumber, + diskEntries, + totalEntries, + centralDirectorySize, + centralDirectoryStartOffset, + fileComment + ) + } + } + + fun toECD(): ByteBuffer { + val commentBytes = fileComment.toByteArray(Charsets.UTF_8) + + val buffer = ByteBuffer.allocate(ECD_HEADER_SIZE + commentBytes.size) + .also { it.order(ByteOrder.LITTLE_ENDIAN) } + + buffer.putUInt(ECD_SIGNATURE) + buffer.putUShort(diskNumber) + buffer.putUShort(startingDiskNumber) + buffer.putUShort(diskEntries) + buffer.putUShort(totalEntries) + buffer.putUInt(centralDirectorySize) + buffer.putUInt(centralDirectoryStartOffset) + buffer.putUShort(commentBytes.size.toUShort()) + + buffer.put(commentBytes) + + buffer.flip() + return buffer + } +} diff --git a/android/app/src/main/kotlin/app/revanced/manager/utils/zip/structures/ZipEntry.kt b/android/app/src/main/kotlin/app/revanced/manager/utils/zip/structures/ZipEntry.kt new file mode 100644 index 00000000..51a24ebf --- /dev/null +++ b/android/app/src/main/kotlin/app/revanced/manager/utils/zip/structures/ZipEntry.kt @@ -0,0 +1,190 @@ +package app.revanced.manager.utils.zip.structures + +import app.revanced.manager.utils.zip.* +import java.io.DataInput +import java.nio.ByteBuffer +import java.nio.ByteOrder + +data class ZipEntry( + val version: UShort, + val versionNeeded: UShort, + val flags: UShort, + var compression: UShort, + val modificationTime: UShort, + val modificationDate: UShort, + var crc32: UInt, + var compressedSize: UInt, + var uncompressedSize: UInt, + val diskNumber: UShort, + val internalAttributes: UShort, + val externalAttributes: UInt, + var localHeaderOffset: UInt, + val fileName: String, + val extraField: ByteArray, + val fileComment: String, + var localExtraField: ByteArray = ByteArray(0), //separate for alignment +) { + val LFHSize: Int + get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size + + val dataOffset: UInt + get() = localHeaderOffset + LFHSize.toUInt() + + companion object { + const val CDE_HEADER_SIZE = 46 + const val CDE_SIGNATURE = 0x02014b50u + + const val LFH_HEADER_SIZE = 30 + const val LFH_SIGNATURE = 0x04034b50u + + fun createWithName(fileName: String): ZipEntry { + return ZipEntry( + 0x1403u, //made by unix, version 20 + 0u, + 0u, + 0u, + 0x0821u, //seems to be static time google uses, no idea + 0x0221u, //same as above + 0u, + 0u, + 0u, + 0u, + 0u, + 0u, + 0u, + fileName, + ByteArray(0), + "" + ) + } + + fun fromCDE(input: DataInput): ZipEntry { + val signature = input.readUIntLE() + + if (signature != CDE_SIGNATURE) + throw IllegalArgumentException("Input doesn't start with central directory entry signature") + + val version = input.readUShortLE() + val versionNeeded = input.readUShortLE() + var flags = input.readUShortLE() + val compression = input.readUShortLE() + val modificationTime = input.readUShortLE() + val modificationDate = input.readUShortLE() + val crc32 = input.readUIntLE() + val compressedSize = input.readUIntLE() + val uncompressedSize = input.readUIntLE() + val fileNameLength = input.readUShortLE() + var fileName = "" + val extraFieldLength = input.readUShortLE() + var extraField = ByteArray(extraFieldLength.toInt()) + val fileCommentLength = input.readUShortLE() + var fileComment = "" + val diskNumber = input.readUShortLE() + val internalAttributes = input.readUShortLE() + val externalAttributes = input.readUIntLE() + val localHeaderOffset = input.readUIntLE() + + val variableFieldsLength = + fileNameLength.toInt() + extraFieldLength.toInt() + fileCommentLength.toInt() + + if (variableFieldsLength > 0) { + val fileNameBytes = ByteArray(fileNameLength.toInt()) + input.readFully(fileNameBytes) + fileName = fileNameBytes.toString(Charsets.UTF_8) + + input.readFully(extraField) + + val fileCommentBytes = ByteArray(fileCommentLength.toInt()) + input.readFully(fileCommentBytes) + fileComment = fileCommentBytes.toString(Charsets.UTF_8) + } + + flags = (flags and 0b1000u.inv() + .toUShort()) //disable data descriptor flag as they are not used + + return ZipEntry( + version, + versionNeeded, + flags, + compression, + modificationTime, + modificationDate, + crc32, + compressedSize, + uncompressedSize, + diskNumber, + internalAttributes, + externalAttributes, + localHeaderOffset, + fileName, + extraField, + fileComment, + ) + } + } + + fun readLocalExtra(buffer: ByteBuffer) { + buffer.order(ByteOrder.LITTLE_ENDIAN) + localExtraField = ByteArray(buffer.getUShort().toInt()) + } + + fun toLFH(): ByteBuffer { + val nameBytes = fileName.toByteArray(Charsets.UTF_8) + + val buffer = ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size) + .also { it.order(ByteOrder.LITTLE_ENDIAN) } + + buffer.putUInt(LFH_SIGNATURE) + buffer.putUShort(versionNeeded) + buffer.putUShort(flags) + buffer.putUShort(compression) + buffer.putUShort(modificationTime) + buffer.putUShort(modificationDate) + buffer.putUInt(crc32) + buffer.putUInt(compressedSize) + buffer.putUInt(uncompressedSize) + buffer.putUShort(nameBytes.size.toUShort()) + buffer.putUShort(localExtraField.size.toUShort()) + + buffer.put(nameBytes) + buffer.put(localExtraField) + + buffer.flip() + return buffer + } + + fun toCDE(): ByteBuffer { + val nameBytes = fileName.toByteArray(Charsets.UTF_8) + val commentBytes = fileComment.toByteArray(Charsets.UTF_8) + + val buffer = + ByteBuffer.allocate(CDE_HEADER_SIZE + nameBytes.size + extraField.size + commentBytes.size) + .also { it.order(ByteOrder.LITTLE_ENDIAN) } + + buffer.putUInt(CDE_SIGNATURE) + buffer.putUShort(version) + buffer.putUShort(versionNeeded) + buffer.putUShort(flags) + buffer.putUShort(compression) + buffer.putUShort(modificationTime) + buffer.putUShort(modificationDate) + buffer.putUInt(crc32) + buffer.putUInt(compressedSize) + buffer.putUInt(uncompressedSize) + buffer.putUShort(nameBytes.size.toUShort()) + buffer.putUShort(extraField.size.toUShort()) + buffer.putUShort(commentBytes.size.toUShort()) + buffer.putUShort(diskNumber) + buffer.putUShort(internalAttributes) + buffer.putUInt(externalAttributes) + buffer.putUInt(localHeaderOffset) + + buffer.put(nameBytes) + buffer.put(extraField) + buffer.put(commentBytes) + + buffer.flip() + return buffer + } +} + diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 00000000..d2e2c8de --- /dev/null +++ b/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,4 @@ + + + + diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 5ada2a36..45cc0b79 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -43,12 +43,19 @@ "widgetThirdSubtitle": "{selected} patch(es) selected." }, "appSelectorView": { - "searchBarHint": "Search applications" + "searchBarHint": "Search applications", + "fabButton": "Storage", + "errorMessage": "Unable to use selected application." }, "patchesSelectorView": { "searchBarHint": "Search patches", "fabButton": "Done" }, + "installerView": { + "widgetTitle": "Installer", + "installButton": "Install", + "shareButton": "Share" + }, "settingsView": { "widgetTitle": "Settings", "languageLabel": "Language", diff --git a/lib/app/app.dart b/lib/app/app.dart index 454af0e4..f94544a1 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -2,8 +2,8 @@ import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/ui/views/app_selector/app_selector_view.dart'; import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.dart'; import 'package:revanced_manager/ui/views/contributors/contributors_view.dart'; -import 'package:revanced_manager/ui/views/home/home_view.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_view.dart'; +import 'package:revanced_manager/ui/views/installer/installer_view.dart'; +import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view.dart'; import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart'; @@ -14,10 +14,9 @@ import 'package:stacked_themes/stacked_themes.dart'; @StackedApp( routes: [ - MaterialRoute(page: HomeView), MaterialRoute(page: AppSelectorView), - MaterialRoute(page: PatcherView), MaterialRoute(page: PatchesSelectorView), + MaterialRoute(page: InstallerView), MaterialRoute(page: SettingsView), MaterialRoute(page: ContributorsView) ], @@ -27,6 +26,7 @@ import 'package:stacked_themes/stacked_themes.dart'; LazySingleton(classType: PatcherViewModel), LazySingleton(classType: AppSelectorViewModel), LazySingleton(classType: PatchesSelectorViewModel), + LazySingleton(classType: InstallerViewModel), LazySingleton( classType: ThemeService, resolveUsing: ThemeService.getInstance), ], diff --git a/lib/app/app.locator.dart b/lib/app/app.locator.dart index 421b6440..6a44d2ab 100644 --- a/lib/app/app.locator.dart +++ b/lib/app/app.locator.dart @@ -4,7 +4,7 @@ // StackedLocatorGenerator // ************************************************************************** -// ignore_for_file: public_member_api_docs +// ignore_for_file: public_member_api_docs, depend_on_referenced_packages, implementation_imports import 'package:stacked_core/stacked_core.dart'; import 'package:stacked_services/src/navigation/navigation_service.dart'; @@ -12,6 +12,7 @@ import 'package:stacked_themes/src/theme_service.dart'; import '../services/patcher_api.dart'; import '../ui/views/app_selector/app_selector_viewmodel.dart'; +import '../ui/views/installer/installer_viewmodel.dart'; import '../ui/views/patcher/patcher_viewmodel.dart'; import '../ui/views/patches_selector/patches_selector_viewmodel.dart'; @@ -29,5 +30,6 @@ Future setupLocator( locator.registerLazySingleton(() => PatcherViewModel()); locator.registerLazySingleton(() => AppSelectorViewModel()); locator.registerLazySingleton(() => PatchesSelectorViewModel()); + locator.registerLazySingleton(() => InstallerViewModel()); locator.registerLazySingleton(() => ThemeService.getInstance()); } diff --git a/lib/app/app.router.dart b/lib/app/app.router.dart index 58a90262..3a618c20 100644 --- a/lib/app/app.router.dart +++ b/lib/app/app.router.dart @@ -4,36 +4,33 @@ // StackedRouterGenerator // ************************************************************************** -// ignore_for_file: no_leading_underscores_for_library_prefixes +// ignore_for_file: no_leading_underscores_for_library_prefixes, implementation_imports import 'package:flutter/material.dart'; +import 'package:flutter/src/foundation/key.dart' as _i7; import 'package:stacked/stacked.dart' as _i1; import 'package:stacked_services/stacked_services.dart' as _i8; -import '../ui/views/app_selector/app_selector_view.dart' as _i3; -import '../ui/views/contributors/contributors_view.dart' as _i7; -import '../ui/views/home/home_view.dart' as _i2; -import '../ui/views/patcher/patcher_view.dart' as _i4; -import '../ui/views/patches_selector/patches_selector_view.dart' as _i5; -import '../ui/views/settings/settings_view.dart' as _i6; +import '../ui/views/app_selector/app_selector_view.dart' as _i2; +import '../ui/views/contributors/contributors_view.dart' as _i6; +import '../ui/views/installer/installer_view.dart' as _i4; +import '../ui/views/patches_selector/patches_selector_view.dart' as _i3; +import '../ui/views/settings/settings_view.dart' as _i5; class Routes { - static const homeView = '/home-view'; - static const appSelectorView = '/app-selector-view'; - static const patcherView = '/patcher-view'; - static const patchesSelectorView = '/patches-selector-view'; + static const installerView = '/installer-view'; + static const settingsView = '/settings-view'; static const contributorsView = '/contributors-view'; static const all = { - homeView, appSelectorView, - patcherView, patchesSelectorView, + installerView, settingsView, contributorsView }; @@ -41,48 +38,44 @@ class Routes { class StackedRouter extends _i1.RouterBase { final _routes = <_i1.RouteDef>[ - _i1.RouteDef(Routes.homeView, page: _i2.HomeView), - _i1.RouteDef(Routes.appSelectorView, page: _i3.AppSelectorView), - _i1.RouteDef(Routes.patcherView, page: _i4.PatcherView), - _i1.RouteDef(Routes.patchesSelectorView, page: _i5.PatchesSelectorView), - _i1.RouteDef(Routes.settingsView, page: _i6.SettingsView), - _i1.RouteDef(Routes.contributorsView, page: _i7.ContributorsView) + _i1.RouteDef(Routes.appSelectorView, page: _i2.AppSelectorView), + _i1.RouteDef(Routes.patchesSelectorView, page: _i3.PatchesSelectorView), + _i1.RouteDef(Routes.installerView, page: _i4.InstallerView), + _i1.RouteDef(Routes.settingsView, page: _i5.SettingsView), + _i1.RouteDef(Routes.contributorsView, page: _i6.ContributorsView) ]; final _pagesMap = { - _i2.HomeView: (data) { + _i2.AppSelectorView: (data) { return MaterialPageRoute( - builder: (context) => const _i2.HomeView(), + builder: (context) => const _i2.AppSelectorView(), settings: data, ); }, - _i3.AppSelectorView: (data) { + _i3.PatchesSelectorView: (data) { return MaterialPageRoute( - builder: (context) => const _i3.AppSelectorView(), + builder: (context) => const _i3.PatchesSelectorView(), settings: data, ); }, - _i4.PatcherView: (data) { + _i4.InstallerView: (data) { + final args = data.getArgs( + orElse: () => const InstallerViewArguments(), + ); return MaterialPageRoute( - builder: (context) => const _i4.PatcherView(), + builder: (context) => _i4.InstallerView(key: args.key), settings: data, ); }, - _i5.PatchesSelectorView: (data) { + _i5.SettingsView: (data) { return MaterialPageRoute( - builder: (context) => const _i5.PatchesSelectorView(), + builder: (context) => const _i5.SettingsView(), settings: data, ); }, - _i6.SettingsView: (data) { + _i6.ContributorsView: (data) { return MaterialPageRoute( - builder: (context) => const _i6.SettingsView(), - settings: data, - ); - }, - _i7.ContributorsView: (data) { - return MaterialPageRoute( - builder: (context) => const _i7.ContributorsView(), + builder: (context) => const _i6.ContributorsView(), settings: data, ); } @@ -94,21 +87,13 @@ class StackedRouter extends _i1.RouterBase { Map get pagesMap => _pagesMap; } -extension NavigatorStateExtension on _i8.NavigationService { - Future navigateToHomeView( - [int? routerId, - bool preventDuplicates = true, - Map? parameters, - Widget Function( - BuildContext, Animation, Animation, Widget)? - transition]) async { - navigateTo(Routes.homeView, - id: routerId, - preventDuplicates: preventDuplicates, - parameters: parameters, - transition: transition); - } +class InstallerViewArguments { + const InstallerViewArguments({this.key}); + final _i7.Key? key; +} + +extension NavigatorStateExtension on _i8.NavigationService { Future navigateToAppSelectorView( [int? routerId, bool preventDuplicates = true, @@ -123,20 +108,6 @@ extension NavigatorStateExtension on _i8.NavigationService { transition: transition); } - Future navigateToPatcherView( - [int? routerId, - bool preventDuplicates = true, - Map? parameters, - Widget Function( - BuildContext, Animation, Animation, Widget)? - transition]) async { - navigateTo(Routes.patcherView, - id: routerId, - preventDuplicates: preventDuplicates, - parameters: parameters, - transition: transition); - } - Future navigateToPatchesSelectorView( [int? routerId, bool preventDuplicates = true, @@ -151,6 +122,22 @@ extension NavigatorStateExtension on _i8.NavigationService { transition: transition); } + Future navigateToInstallerView( + {_i7.Key? key, + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function( + BuildContext, Animation, Animation, Widget)? + transition}) async { + navigateTo(Routes.installerView, + arguments: InstallerViewArguments(key: key), + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + Future navigateToSettingsView( [int? routerId, bool preventDuplicates = true, diff --git a/lib/main.dart b/lib/main.dart index c8be5e38..af039fdf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; - // ignore: depend_on_referenced_packages import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:revanced_manager/app/app.locator.dart'; diff --git a/lib/models/application_info.dart b/lib/models/application_info.dart new file mode 100644 index 00000000..a332fd98 --- /dev/null +++ b/lib/models/application_info.dart @@ -0,0 +1,13 @@ +class ApplicationInfo { + final String name; + final String packageName; + final String version; + final String apkFilePath; + + ApplicationInfo({ + required this.name, + required this.packageName, + required this.version, + required this.apkFilePath, + }); +} diff --git a/lib/services/patcher_api.dart b/lib/services/patcher_api.dart index a37b068b..b722e183 100644 --- a/lib/services/patcher_api.dart +++ b/lib/services/patcher_api.dart @@ -1,22 +1,45 @@ import 'dart:io'; +import 'package:app_installer/app_installer.dart'; +import 'package:device_apps/device_apps.dart'; import 'package:flutter/services.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:injectable/injectable.dart'; -import 'package:installed_apps/app_info.dart'; -import 'package:installed_apps/installed_apps.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:revanced_manager/app/app.locator.dart'; +import 'package:revanced_manager/models/application_info.dart'; import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/services/github_api.dart'; +import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart'; import 'package:revanced_manager/utils/string.dart'; +import 'package:share_extend/share_extend.dart'; @lazySingleton class PatcherAPI { + static const platform = MethodChannel('app.revanced.manager/patcher'); final GithubAPI githubAPI = GithubAPI(); - final List _filteredPackages = []; + final List _filteredPackages = []; final Map> _filteredPatches = >{}; + bool isRoot = false; + Directory? _workDir; + Directory? _cacheDir; File? _patchBundleFile; - static const platform = MethodChannel('app.revanced/patcher'); + File? _integrations; + File? _inputFile; + File? _patchedFile; + File? _outFile; - Future loadPatches() async { + Future handlePlatformChannelMethods() async { + platform.setMethodCallHandler((call) async { + switch (call.method) { + case 'updateInstallerLog': + var message = call.arguments('message'); + locator().addLog(message); + return 'OK'; + } + }); + } + + Future loadPatches() async { if (_patchBundleFile == null) { String? dexFileUrl = await githubAPI.latestRelease('revanced', 'revanced-patches'); @@ -24,7 +47,7 @@ class PatcherAPI { _patchBundleFile = await DefaultCacheManager().getSingleFile(dexFileUrl); try { - await platform.invokeMethod( + return await platform.invokeMethod( 'loadPatches', { 'pathBundlesPaths': [_patchBundleFile!.absolute.path], @@ -32,12 +55,15 @@ class PatcherAPI { ); } on PlatformException { _patchBundleFile = null; + return false; } } + return false; } + return true; } - Future> getFilteredInstalledApps() async { + Future> getFilteredInstalledApps() async { if (_patchBundleFile != null && _filteredPackages.isEmpty) { try { List? patchesPackages = @@ -45,8 +71,11 @@ class PatcherAPI { if (patchesPackages != null) { for (String package in patchesPackages) { try { - AppInfo app = await InstalledApps.getAppInfo(package); - _filteredPackages.add(app); + ApplicationWithIcon? app = await DeviceApps.getApp(package, true) + as ApplicationWithIcon?; + if (app != null) { + _filteredPackages.add(app); + } } catch (e) { continue; } @@ -60,25 +89,25 @@ class PatcherAPI { return _filteredPackages; } - Future?> getFilteredPatches(AppInfo? targetApp) async { - if (_patchBundleFile != null && targetApp != null) { - if (_filteredPatches[targetApp.packageName] == null || - _filteredPatches[targetApp.packageName]!.isEmpty) { - _filteredPatches[targetApp.packageName!] = []; + Future?> getFilteredPatches(ApplicationInfo? selectedApp) async { + if (_patchBundleFile != null && selectedApp != null) { + if (_filteredPatches[selectedApp.packageName] == null || + _filteredPatches[selectedApp.packageName]!.isEmpty) { + _filteredPatches[selectedApp.packageName] = []; try { var patches = await platform.invokeListMethod>( 'getFilteredPatches', { - 'targetPackage': targetApp.packageName, - 'targetVersion': targetApp.versionName, + 'targetPackage': selectedApp.packageName, + 'targetVersion': selectedApp.version, 'ignoreVersion': true, }, ); if (patches != null) { for (var patch in patches) { - if (!_filteredPatches[targetApp.packageName]! + if (!_filteredPatches[selectedApp.packageName]! .any((element) => element.name == patch['name'])) { - _filteredPatches[targetApp.packageName]!.add( + _filteredPatches[selectedApp.packageName]!.add( Patch( name: patch['name'], simpleName: (patch['name'] as String) @@ -94,13 +123,168 @@ class PatcherAPI { } } } on PlatformException { - _filteredPatches[targetApp.packageName]!.clear(); + _filteredPatches[selectedApp.packageName]!.clear(); return List.empty(); } } } else { return List.empty(); } - return _filteredPatches[targetApp.packageName]; + return _filteredPatches[selectedApp.packageName]; + } + + Future downloadIntegrations() async { + String? apkFileUrl = + await githubAPI.latestRelease('revanced', 'revanced-integrations'); + if (apkFileUrl != null && apkFileUrl.isNotEmpty) { + return await DefaultCacheManager().getSingleFile(apkFileUrl); + } + return null; + } + + Future initPatcher() async { + try { + _integrations = await downloadIntegrations(); + if (_integrations != null) { + Directory tmpDir = await getTemporaryDirectory(); + _workDir = tmpDir.createTempSync('tmp-'); + _inputFile = File('${_workDir!.path}/base.apk'); + _patchedFile = File('${_workDir!.path}/patched.apk'); + _outFile = File('${_workDir!.path}/out.apk'); + _cacheDir = Directory('${_workDir!.path}/cache'); + _cacheDir!.createSync(); + return true; + } + } on Exception { + return false; + } + return false; + } + + Future copyInputFile(String originalFilePath) async { + if (_inputFile != null) { + try { + return await platform.invokeMethod( + 'copyInputFile', + { + 'originalFilePath': originalFilePath, + 'inputFilePath': _inputFile!.path, + }, + ); + } on PlatformException { + return false; + } + } + return false; + } + + Future createPatcher() async { + if (_inputFile != null && _cacheDir != null) { + try { + return await platform.invokeMethod( + 'createPatcher', + { + 'inputFilePath': _inputFile!.path, + 'cacheDirPath': _cacheDir!.path, + }, + ); + } on PlatformException { + return false; + } + } + return false; + } + + Future mergeIntegrations() async { + try { + return await platform.invokeMethod( + 'mergeIntegrations', + { + 'integrationsPath': _integrations!.path, + }, + ); + } on PlatformException { + return false; + } + } + + Future applyPatches(List selectedPatches) async { + try { + return await platform.invokeMethod( + 'applyPatches', + { + 'selectedPatches': selectedPatches.map((e) => e.name).toList(), + }, + ); + } on PlatformException { + return false; + } + } + + Future repackPatchedFile() async { + if (_inputFile != null && _patchedFile != null) { + try { + return await platform.invokeMethod( + 'repackPatchedFile', + { + 'inputFilePath': _inputFile!.path, + 'patchedFilePath': _patchedFile!.path, + }, + ); + } on PlatformException { + return false; + } + } + return false; + } + + Future signPatchedFile() async { + if (_patchedFile != null && _outFile != null) { + try { + return await platform.invokeMethod( + 'signPatchedFile', + { + 'patchedFilePath': _patchedFile!.path, + 'outFilePath': _outFile!.path, + }, + ); + } on PlatformException { + return false; + } + } + return false; + } + + Future installPatchedFile() async { + if (_outFile != null) { + try { + if (isRoot) { + // TBD + } else { + await AppInstaller.installApk(_outFile!.path); + } + return true; + } on Exception { + return false; + } + } + return false; + } + + void cleanPatcher() { + if (_workDir != null) { + _workDir!.deleteSync(recursive: true); + } + } + + bool sharePatchedFile(String packageName) { + if (_outFile != null) { + String sharePath = '${_outFile!.parent.path}/$packageName.revanced.apk'; + File share = _outFile!.copySync(sharePath); + ShareExtend.share(share.path, "file"); + return true; + } else { + return false; + } } } diff --git a/lib/ui/views/app_selector/app_selector_view.dart b/lib/ui/views/app_selector/app_selector_view.dart index d990c5b4..7b15a4ee 100644 --- a/lib/ui/views/app_selector/app_selector_view.dart +++ b/lib/ui/views/app_selector/app_selector_view.dart @@ -21,9 +21,19 @@ class _AppSelectorViewState extends State { Widget build(BuildContext context) { return ViewModelBuilder.reactive( disposeViewModel: false, - onModelReady: (model) => model.initialise(), + onModelReady: (model) => model.initialize(), viewModelBuilder: () => locator(), builder: (context, model, child) => Scaffold( + floatingActionButton: FloatingActionButton.extended( + onPressed: () { + model.selectAppFromStorage(context); + Navigator.of(context).pop(); + }, + label: I18nText('appSelectorView.fabButton'), + icon: const Icon(Icons.sd_storage), + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: Colors.white, + ), body: SafeArea( child: Padding( padding: @@ -71,16 +81,16 @@ class _AppSelectorViewState extends State { child: ListView.builder( itemCount: model.apps.length, itemBuilder: (context, index) { - model.apps.sort((a, b) => a.name!.compareTo(b.name!)); + model.apps.sort((a, b) => a.appName.compareTo(b.appName)); return InkWell( onTap: () { model.selectApp(model.apps[index]); Navigator.of(context).pop(); }, child: InstalledAppItem( - name: model.apps[index].name!, - pkgName: model.apps[index].packageName!, - icon: model.apps[index].icon!, + name: model.apps[index].appName, + pkgName: model.apps[index].packageName, + icon: model.apps[index].icon, ), ); }, @@ -93,8 +103,8 @@ class _AppSelectorViewState extends State { child: ListView.builder( itemCount: model.apps.length, itemBuilder: (context, index) { - model.apps.sort((a, b) => a.name!.compareTo(b.name!)); - if (model.apps[index].name!.toLowerCase().contains( + model.apps.sort((a, b) => a.appName.compareTo(b.appName)); + if (model.apps[index].appName.toLowerCase().contains( query.toLowerCase(), )) { return InkWell( @@ -103,9 +113,9 @@ class _AppSelectorViewState extends State { Navigator.of(context).pop(); }, child: InstalledAppItem( - name: model.apps[index].name!, - pkgName: model.apps[index].packageName!, - icon: model.apps[index].icon!, + name: model.apps[index].appName, + pkgName: model.apps[index].packageName, + icon: model.apps[index].icon, ), ); } else { diff --git a/lib/ui/views/app_selector/app_selector_viewmodel.dart b/lib/ui/views/app_selector/app_selector_viewmodel.dart index 433bba48..fdd1c4f7 100644 --- a/lib/ui/views/app_selector/app_selector_viewmodel.dart +++ b/lib/ui/views/app_selector/app_selector_viewmodel.dart @@ -1,15 +1,22 @@ -import 'package:installed_apps/app_info.dart'; +import 'dart:io'; +import 'package:device_apps/device_apps.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:package_archive_info/package_archive_info.dart'; import 'package:revanced_manager/app/app.locator.dart'; +import 'package:revanced_manager/models/application_info.dart'; import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:stacked/stacked.dart'; class AppSelectorViewModel extends BaseViewModel { final PatcherAPI patcherAPI = locator(); - List apps = []; - AppInfo? selectedApp; + List apps = []; + ApplicationInfo? selectedApp; - Future initialise() async { + Future initialize() async { await getApps(); notifyListeners(); } @@ -19,9 +26,47 @@ class AppSelectorViewModel extends BaseViewModel { apps = await patcherAPI.getFilteredInstalledApps(); } - void selectApp(AppInfo appInfo) { - locator().selectedApp = appInfo; + void selectApp(ApplicationWithIcon application) { + ApplicationInfo app = ApplicationInfo( + name: application.appName, + packageName: application.packageName, + version: application.versionName!, + apkFilePath: application.apkFilePath, + ); + locator().selectedApp = app; locator().dimPatchCard = false; locator().notifyListeners(); } + + Future selectAppFromStorage(BuildContext context) async { + try { + FilePickerResult? result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['apk'], + ); + if (result != null && result.files.single.path != null) { + File apkFile = File(result.files.single.path!); + PackageArchiveInfo? packageArchiveInfo = + await PackageArchiveInfo.fromPath(apkFile.path); + ApplicationInfo app = ApplicationInfo( + name: packageArchiveInfo.appName, + packageName: packageArchiveInfo.packageName, + version: packageArchiveInfo.version, + apkFilePath: result.files.single.path!, + ); + locator().selectedApp = app; + locator().dimPatchCard = false; + locator().notifyListeners(); + } + } on Exception { + Fluttertoast.showToast( + msg: FlutterI18n.translate( + context, + 'appSelectorView.errorMessage', + ), + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + ); + } + } } diff --git a/lib/ui/views/home/home_view.dart b/lib/ui/views/home/home_view.dart index 7b94e495..dfe5db15 100644 --- a/lib/ui/views/home/home_view.dart +++ b/lib/ui/views/home/home_view.dart @@ -26,7 +26,7 @@ class HomeView extends StatelessWidget { Align( alignment: Alignment.topRight, child: IconButton( - onPressed: () {}, + onPressed: () => {}, icon: const Icon( Icons.more_vert, ), diff --git a/lib/ui/views/installer/installer_view.dart b/lib/ui/views/installer/installer_view.dart new file mode 100644 index 00000000..5543b386 --- /dev/null +++ b/lib/ui/views/installer/installer_view.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:revanced_manager/app/app.locator.dart'; +import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart'; +import 'package:stacked/stacked.dart'; + +class InstallerView extends StatelessWidget { + InstallerView({Key? key}) : super(key: key); + final ScrollController _controller = ScrollController(); + + @override + Widget build(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => _controller.jumpTo(_controller.position.maxScrollExtent), + ); + return ViewModelBuilder.reactive( + disposeViewModel: false, + onModelReady: (model) => model.initialize(), + viewModelBuilder: () => locator(), + builder: (context, model, child) => WillStartForegroundTask( + onWillStart: () async => model.isPatching, + androidNotificationOptions: AndroidNotificationOptions( + channelId: 'revanced-patcher-patching', + channelName: 'Patching', + channelDescription: 'This notification appears when the patching ' + 'foreground service is running.', + channelImportance: NotificationChannelImportance.LOW, + priority: NotificationPriority.LOW, + ), + notificationTitle: 'Patching', + notificationText: 'ReVanced Manager is patching', + callback: () => {}, + child: WillPopScope( + child: Scaffold( + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 12), + controller: _controller, + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: constraints.maxWidth, + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + I18nText( + 'installerView.widgetTitle', + child: Text( + '', + style: Theme.of(context).textTheme.headline5, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 4.0, + ), + child: LinearProgressIndicator( + color: Theme.of(context).colorScheme.secondary, + backgroundColor: Colors.white, + value: model.progress, + ), + ), + Container( + padding: const EdgeInsets.all(12.0), + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(8), + ), + child: SelectableText( + model.logs, + style: const TextStyle( + fontFamily: 'monospace', fontSize: 15), + ), + ), + const Spacer(), + Visibility( + visible: model.showButtons, + child: Row( + children: [ + Expanded( + child: MaterialButton( + textColor: Colors.white, + color: + Theme.of(context).colorScheme.secondary, + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + onPressed: () => model.installResult(), + child: I18nText( + 'installerView.installButton', + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: MaterialButton( + textColor: Colors.white, + color: + Theme.of(context).colorScheme.secondary, + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + onPressed: () => model.shareResult(), + child: I18nText( + 'installerView.shareButton', + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + onWillPop: () async { + if (!model.isPatching) { + Navigator.of(context).pop(); + } + return false; + }, + ), + ), + ); + } +} diff --git a/lib/ui/views/installer/installer_viewmodel.dart b/lib/ui/views/installer/installer_viewmodel.dart new file mode 100644 index 00000000..32728f06 --- /dev/null +++ b/lib/ui/views/installer/installer_viewmodel.dart @@ -0,0 +1,114 @@ +import 'package:revanced_manager/app/app.locator.dart'; +import 'package:revanced_manager/models/application_info.dart'; +import 'package:revanced_manager/models/patch.dart'; +import 'package:revanced_manager/services/patcher_api.dart'; +import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.dart'; +import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart'; +import 'package:stacked/stacked.dart'; + +class InstallerViewModel extends BaseViewModel { + double? progress = 0.2; + String logs = ''; + bool isPatching = false; + bool showButtons = false; + + Future initialize() async { + await locator().handlePlatformChannelMethods(); + runPatcher(); + } + + void addLog(String message) { + if (logs.isNotEmpty) { + logs += '\n'; + } + logs += message; + notifyListeners(); + } + + void updateProgress(double value) { + progress = value; + isPatching = progress == 1.0 ? false : true; + showButtons = progress == 1.0 ? true : false; + if (progress == 0.0) { + logs = ''; + } + notifyListeners(); + } + + Future runPatcher() async { + updateProgress(0.0); + ApplicationInfo? selectedApp = locator().selectedApp; + if (selectedApp != null) { + String apkFilePath = selectedApp.apkFilePath; + List selectedPatches = + locator().selectedPatches; + if (selectedPatches.isNotEmpty) { + addLog('Initializing patcher...'); + bool? isSuccess = await locator().initPatcher(); + if (isSuccess != null && isSuccess) { + addLog('Done'); + updateProgress(0.1); + addLog('Copying original apk...'); + isSuccess = await locator().copyInputFile(apkFilePath); + if (isSuccess != null && isSuccess) { + addLog('Done'); + updateProgress(0.2); + addLog('Creating patcher...'); + isSuccess = await locator().createPatcher(); + if (isSuccess != null && isSuccess) { + if (selectedApp.packageName == 'com.google.android.youtube') { + addLog('Done'); + updateProgress(0.3); + addLog('Merging integrations...'); + isSuccess = await locator().mergeIntegrations(); + } + if (isSuccess != null && isSuccess) { + addLog('Done'); + updateProgress(0.5); + addLog('Applying patches...'); + isSuccess = + await locator().applyPatches(selectedPatches); + if (isSuccess != null && isSuccess) { + addLog('Done'); + updateProgress(0.7); + addLog('Repacking patched apk...'); + isSuccess = await locator().repackPatchedFile(); + if (isSuccess != null && isSuccess) { + addLog('Done'); + updateProgress(0.9); + addLog('Signing patched apk...'); + isSuccess = await locator().signPatchedFile(); + if (isSuccess != null && isSuccess) { + addLog('Done'); + showButtons = true; + updateProgress(1.0); + } + } + } + } + } + } + } + if (isSuccess == null || !isSuccess) { + addLog('An error occurred! Aborting...'); + } + } else { + addLog('No patches selected! Aborting...'); + } + } else { + addLog('No app selected! Aborting...'); + } + isPatching = false; + } + + void installResult() async { + await locator().installPatchedFile(); + } + + void shareResult() { + ApplicationInfo? selectedApp = locator().selectedApp; + if (selectedApp != null) { + locator().sharePatchedFile(selectedApp.packageName); + } + } +} diff --git a/lib/ui/views/patcher/patcher_view.dart b/lib/ui/views/patcher/patcher_view.dart index 18187b81..5ace4a71 100644 --- a/lib/ui/views/patcher/patcher_view.dart +++ b/lib/ui/views/patcher/patcher_view.dart @@ -3,12 +3,11 @@ import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/theme.dart'; +import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/widgets/app_selector_card.dart'; import 'package:revanced_manager/ui/widgets/patch_selector_card.dart'; import 'package:stacked/stacked.dart'; -import 'patcher_viewmodel.dart'; - class PatcherView extends StatelessWidget { const PatcherView({Key? key}) : super(key: key); @@ -21,7 +20,7 @@ class PatcherView extends StatelessWidget { floatingActionButton: Visibility( visible: locator().showFabButton, child: FloatingActionButton.extended( - onPressed: () => {}, + onPressed: () => model.navigateToInstaller(), label: I18nText('patcherView.fabButton'), icon: const Icon(Icons.build), backgroundColor: Theme.of(context).colorScheme.secondary, diff --git a/lib/ui/views/patcher/patcher_viewmodel.dart b/lib/ui/views/patcher/patcher_viewmodel.dart index 959010ec..4f8a6460 100644 --- a/lib/ui/views/patcher/patcher_viewmodel.dart +++ b/lib/ui/views/patcher/patcher_viewmodel.dart @@ -15,4 +15,8 @@ class PatcherViewModel extends BaseViewModel { void navigateToPatchesSelector() { _navigationService.navigateTo(Routes.patchesSelectorView); } + + void navigateToInstaller() { + _navigationService.navigateTo(Routes.installerView); + } } diff --git a/lib/ui/views/patches_selector/patches_selector_view.dart b/lib/ui/views/patches_selector/patches_selector_view.dart index 88aeaef9..6cd100de 100644 --- a/lib/ui/views/patches_selector/patches_selector_view.dart +++ b/lib/ui/views/patches_selector/patches_selector_view.dart @@ -22,7 +22,7 @@ class _PatchesSelectorViewState extends State { Widget build(BuildContext context) { return ViewModelBuilder.reactive( disposeViewModel: false, - onModelReady: (model) => model.initialise(), + onModelReady: (model) => model.initialize(), viewModelBuilder: () => locator(), builder: (context, model, child) => Scaffold( body: SafeArea( @@ -52,7 +52,7 @@ class _PatchesSelectorViewState extends State { : _getFilteredResults(model), MaterialButton( textColor: Colors.white, - color: const Color(0x957792BA), + color: Theme.of(context).colorScheme.secondary, minWidth: double.infinity, padding: const EdgeInsets.symmetric( vertical: 12, diff --git a/lib/ui/views/patches_selector/patches_selector_viewmodel.dart b/lib/ui/views/patches_selector/patches_selector_viewmodel.dart index 1bb00dad..25986c40 100644 --- a/lib/ui/views/patches_selector/patches_selector_viewmodel.dart +++ b/lib/ui/views/patches_selector/patches_selector_viewmodel.dart @@ -1,5 +1,5 @@ -import 'package:installed_apps/app_info.dart'; import 'package:revanced_manager/app/app.locator.dart'; +import 'package:revanced_manager/models/application_info.dart'; import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.dart'; @@ -12,14 +12,14 @@ class PatchesSelectorViewModel extends BaseViewModel { List? patches = []; List selectedPatches = []; - Future initialise() async { + Future initialize() async { await getPatches(); notifyListeners(); } Future getPatches() async { - AppInfo? appInfo = locator().selectedApp; - patches = await patcherAPI.getFilteredPatches(appInfo); + ApplicationInfo? app = locator().selectedApp; + patches = await patcherAPI.getFilteredPatches(app); } void selectPatches(List patchItems) { diff --git a/lib/ui/widgets/app_selector_card.dart b/lib/ui/widgets/app_selector_card.dart index 4cae7a40..21ef4d1b 100644 --- a/lib/ui/widgets/app_selector_card.dart +++ b/lib/ui/widgets/app_selector_card.dart @@ -45,7 +45,7 @@ class AppSelectorCard extends StatelessWidget { const SizedBox(height: 10), locator().selectedApp != null ? Text( - locator().selectedApp!.packageName!, + locator().selectedApp!.packageName, style: robotoTextStyle, ) : I18nText( diff --git a/lib/ui/widgets/installed_app_item.dart b/lib/ui/widgets/installed_app_item.dart index e0985400..0ecce773 100644 --- a/lib/ui/widgets/installed_app_item.dart +++ b/lib/ui/widgets/installed_app_item.dart @@ -1,5 +1,4 @@ import 'dart:typed_data'; - import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:revanced_manager/constants.dart'; diff --git a/lib/ui/widgets/latest_commit_card.dart b/lib/ui/widgets/latest_commit_card.dart index 89de3df8..2e1e5158 100644 --- a/lib/ui/widgets/latest_commit_card.dart +++ b/lib/ui/widgets/latest_commit_card.dart @@ -33,59 +33,52 @@ class _LatestCommitCardState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - I18nText( - 'latestCommitCard.patcherLabel', - child: Text( - '', - style: GoogleFonts.roboto( - fontWeight: FontWeight.w700, - ), - ), + I18nText( + 'latestCommitCard.patcherLabel', + child: Text( + '', + style: GoogleFonts.roboto( + fontWeight: FontWeight.w700, ), - FutureBuilder( - future: githubAPI.latestCommitTime( - 'revanced', - 'revanced-patcher', - ), - initialData: FlutterI18n.translate( - context, - 'latestCommitCard.loadingLabel', - ), - builder: (context, snapshot) => Text( - snapshot.data!, - style: robotoTextStyle, - ), - ), - ], + ), ), - Row( - children: [ - I18nText( - 'latestCommitCard.managerLabel', - child: Text( - '', - style: GoogleFonts.roboto( - fontWeight: FontWeight.w700, - ), - ), + FutureBuilder( + future: githubAPI.latestCommitTime( + 'revanced', + 'revanced-patcher', + ), + initialData: FlutterI18n.translate( + context, + 'latestCommitCard.loadingLabel', + ), + builder: (context, snapshot) => Text( + snapshot.data!, + style: robotoTextStyle, + ), + ), + const SizedBox(height: 8), + I18nText( + 'latestCommitCard.managerLabel', + child: Text( + '', + style: GoogleFonts.roboto( + fontWeight: FontWeight.w700, ), - FutureBuilder( - future: githubAPI.latestCommitTime( - 'revanced', - 'revanced-patcher', - ), - initialData: FlutterI18n.translate( - context, - 'latestCommitCard.loadingLabel', - ), - builder: (context, snapshot) => Text( - snapshot.data!, - style: robotoTextStyle, - ), - ), - ], + ), + ), + FutureBuilder( + future: githubAPI.latestCommitTime( + 'revanced', + 'revanced-patcher', + ), + initialData: FlutterI18n.translate( + context, + 'latestCommitCard.loadingLabel', + ), + builder: (context, snapshot) => Text( + snapshot.data!, + style: robotoTextStyle, + ), ), ], ), diff --git a/lib/ui/widgets/patch_item.dart b/lib/ui/widgets/patch_item.dart index 66bad385..0fc2966e 100644 --- a/lib/ui/widgets/patch_item.dart +++ b/lib/ui/widgets/patch_item.dart @@ -25,64 +25,69 @@ class PatchItem extends StatefulWidget { class _PatchItemState extends State { @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), - margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - widget.simpleName, - style: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.w600, + return InkWell( + onTap: () => setState(() { + widget.isSelected = !widget.isSelected; + }), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), + margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + widget.simpleName, + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w600, + ), ), - ), - const SizedBox(width: 4), - Text(widget.version) - ], - ), - const SizedBox(height: 4), - Text( - widget.description, - softWrap: true, - maxLines: 3, - overflow: TextOverflow.visible, - style: GoogleFonts.roboto( - fontSize: 14, + const SizedBox(width: 4), + Text(widget.version) + ], ), - ), - ], + const SizedBox(height: 4), + Text( + widget.description, + softWrap: true, + maxLines: 3, + overflow: TextOverflow.visible, + style: GoogleFonts.roboto( + fontSize: 14, + ), + ), + ], + ), ), - ), - Transform.scale( - scale: 1.2, - child: Checkbox( - value: widget.isSelected, - activeColor: Colors.blueGrey[500], - onChanged: (newValue) { - setState(() { - widget.isSelected = newValue!; - }); - }, - ), - ) - ], - ) - ], + Transform.scale( + scale: 1.2, + child: Checkbox( + value: widget.isSelected, + activeColor: Colors.blueGrey[500], + onChanged: (newValue) { + setState(() { + widget.isSelected = newValue!; + }); + }, + ), + ) + ], + ) + ], + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index e147ebb0..3ec64753 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -15,6 +15,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.3.1" + app_installer: + dependency: "direct main" + description: + name: app_installer + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" args: dependency: transitive description: @@ -162,6 +169,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.3" + device_apps: + dependency: "direct main" + description: + name: device_apps + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" dio: dependency: "direct main" description: @@ -190,6 +204,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.2" + file_picker: + dependency: "direct main" + description: + name: file_picker + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.1" fixnum: dependency: transitive description: @@ -209,6 +230,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.3.0" + flutter_foreground_task: + dependency: "direct main" + description: + name: flutter_foreground_task + url: "https://pub.dartlang.org" + source: hosted + version: "3.8.1" flutter_i18n: dependency: "direct main" description: @@ -228,6 +256,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" flutter_statusbarcolor_ns: dependency: transitive description: @@ -252,6 +287,13 @@ packages: description: flutter source: sdk version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + url: "https://pub.dartlang.org" + source: hosted + version: "8.0.9" frontend_server_client: dependency: transitive description: @@ -336,13 +378,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.4" - installed_apps: - dependency: "direct main" - description: - name: installed_apps - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" intl: dependency: transitive description: @@ -434,6 +469,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + package_archive_info: + dependency: "direct main" + description: + name: package_archive_info + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" package_config: dependency: transitive description: @@ -441,6 +483,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + package_info: + dependency: transitive + description: + name: package_info + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" path: dependency: transitive description: @@ -581,6 +630,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.0" + root: + dependency: "direct main" + description: + name: root + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" rxdart: dependency: transitive description: @@ -588,6 +644,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.27.5" + share_extend: + dependency: "direct main" + description: + name: share_extend + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" shared_preferences: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 67b94ee7..7841562e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,21 +10,28 @@ environment: sdk: ">=2.17.5 <3.0.0" dependencies: + app_installer: ^1.1.0 cupertino_icons: ^1.0.2 + device_apps: ^2.2.0 dio: ^4.0.6 + file_picker: ^5.0.1 flutter: sdk: flutter flutter_cache_manager: ^3.3.0 + flutter_foreground_task: ^3.8.1 flutter_i18n: ^0.32.4 flutter_svg: ^1.1.1+1 + fluttertoast: ^8.0.9 get_it: ^7.2.0 github: ^9.4.0 google_fonts: ^3.0.1 http: ^0.13.4 injectable: ^1.5.3 - installed_apps: ^1.3.1 json_annotation: ^4.6.0 + package_archive_info: ^0.1.0 path_provider: ^2.0.11 + root: ^2.0.2 + share_extend: ^2.0.0 stacked: ^2.3.15 stacked_generator: ^0.7.14 stacked_services: ^0.9.3