├── docs ├── screenshots │ ├── search.png │ ├── resources.png │ ├── dark_theme.png │ ├── decompiler.png │ ├── smali_editor.png │ └── sign_and_deploy.png └── logo.svg ├── composeApp ├── desktopAppIcons │ ├── logo.ico │ ├── logo.png │ └── logo.icns ├── src │ ├── commonMain │ │ ├── composeResources │ │ │ ├── font │ │ │ │ ├── MonaspaceNeonFrozen-Bold.ttf │ │ │ │ ├── MonaspaceNeonFrozen-Light.ttf │ │ │ │ ├── MonaspaceNeonFrozen-Italic.ttf │ │ │ │ ├── MonaspaceNeonFrozen-Medium.ttf │ │ │ │ ├── MonaspaceNeonFrozen-Regular.ttf │ │ │ │ ├── MonaspaceNeonFrozen-SemiBold.ttf │ │ │ │ ├── MonaspaceNeonFrozen-BoldItalic.ttf │ │ │ │ ├── MonaspaceNeonFrozen-ExtraBold.ttf │ │ │ │ ├── MonaspaceNeonFrozen-ExtraLight.ttf │ │ │ │ ├── MonaspaceNeonFrozen-LightItalic.ttf │ │ │ │ ├── MonaspaceNeonFrozen-MediumItalic.ttf │ │ │ │ ├── MonaspaceNeonFrozen-ExtraBoldItalic.ttf │ │ │ │ ├── MonaspaceNeonFrozen-SemiBoldItalic.ttf │ │ │ │ └── MonaspaceNeonFrozen-ExtraLightItalic.ttf │ │ │ ├── values-de │ │ │ │ └── opcodes.xml │ │ │ ├── values-hi │ │ │ │ └── opcodes.xml │ │ │ └── drawable │ │ │ │ ├── ic_dark_mode.xml │ │ │ │ ├── ic_rotate_right.xml │ │ │ │ ├── ic_cyclone.xml │ │ │ │ └── ic_light_mode.xml │ │ └── kotlin │ │ │ └── me │ │ │ └── lkl │ │ │ └── dalvikus │ │ │ ├── decompiler │ │ │ ├── Decompiler.kt │ │ │ ├── DecompilerContentProvider.kt │ │ │ ├── JADXDecompiler.kt │ │ │ ├── JavaDecompiler.kt │ │ │ └── CFRDecompiler.kt │ │ │ ├── lexer │ │ │ ├── JSONLexerHighlight.kt │ │ │ ├── JavaLexerHighlight.kt │ │ │ ├── XmlLexerHighlight.kt │ │ │ └── SmaliLexerHighlight.kt │ │ │ ├── settings │ │ │ ├── Shortcuts.kt │ │ │ └── DalvikusBaksmaliOptions.kt │ │ │ ├── ui │ │ │ ├── nav │ │ │ │ └── NavItem.kt │ │ │ ├── editor │ │ │ │ ├── suggestions │ │ │ │ │ ├── AssistSuggestion.kt │ │ │ │ │ └── EditorAnnotationPopup.kt │ │ │ │ ├── highlight │ │ │ │ │ └── Highlight.kt │ │ │ │ └── LineNumbers.kt │ │ │ ├── tabs │ │ │ │ ├── TabContentRenderer.kt │ │ │ │ └── TabManager.kt │ │ │ ├── RightPanel.kt │ │ │ ├── tree │ │ │ │ ├── TreeDragAndDropTarget.kt │ │ │ │ └── FileSelector.kt │ │ │ ├── packaging │ │ │ │ └── PackagingViewModel.kt │ │ │ ├── snackbar │ │ │ │ └── Snackbar.kt │ │ │ ├── settings │ │ │ │ └── SettingsView.kt │ │ │ ├── image │ │ │ │ └── ImageView.kt │ │ │ └── LeftPanel.kt │ │ │ ├── util │ │ │ ├── Throwables.kt │ │ │ ├── Hex.kt │ │ │ ├── SearchOptions.kt │ │ │ ├── HazeDefaults.kt │ │ │ ├── ApkTool.kt │ │ │ ├── Modifiers.kt │ │ │ ├── GitHub.kt │ │ │ ├── IOCommons.kt │ │ │ └── UICommons.kt │ │ │ ├── tree │ │ │ ├── filesystem │ │ │ │ ├── FileSystemFileNode.kt │ │ │ │ └── FileSystemFolderNode.kt │ │ │ ├── archive │ │ │ │ ├── ZipEntryFileNode.kt │ │ │ │ ├── ZipEntryImageNode.kt │ │ │ │ ├── ZipEntryFolderNode.kt │ │ │ │ ├── ApkNode.kt │ │ │ │ ├── ApkEntryXmlNode.kt │ │ │ │ └── ZipNode.kt │ │ │ ├── NodeFactory.kt │ │ │ ├── dex │ │ │ │ ├── DexEntryPackageNode.kt │ │ │ │ ├── DexFileNode.kt │ │ │ │ └── DexEntryClassNode.kt │ │ │ ├── backing │ │ │ │ ├── Backing.kt │ │ │ │ └── BackingFileNode.kt │ │ │ ├── root │ │ │ │ └── HiddenRoot.kt │ │ │ └── Node.kt │ │ │ ├── tabs │ │ │ ├── contentprovider │ │ │ │ ├── NopContentProvider.kt │ │ │ │ ├── ContentProvider.kt │ │ │ │ └── DualContentProvider.kt │ │ │ ├── TabElement.kt │ │ │ ├── CodeTab.kt │ │ │ ├── ImageTab.kt │ │ │ ├── WelcomeTab.kt │ │ │ └── SmaliTab.kt │ │ │ ├── smali │ │ │ ├── SoloDexFileWrapper.kt │ │ │ └── ErrorHandlingSmaliParser.kt │ │ │ ├── theme │ │ │ ├── CodeHighlightColors.kt │ │ │ ├── Theme.kt │ │ │ ├── Typography.kt │ │ │ └── FileTypeMeta.kt │ │ │ ├── icons │ │ │ ├── DeployedCode.kt │ │ │ ├── MatchCase.kt │ │ │ ├── RegularExpression.kt │ │ │ ├── FamilyHistory.kt │ │ │ └── ThreadUnread.kt │ │ │ └── tools │ │ │ ├── Keytool.kt │ │ │ ├── AdbDeployer.kt │ │ │ ├── SdkLocator.kt │ │ │ └── ApkSigner.kt │ └── jvmMain │ │ ├── kotlin │ │ ├── me │ │ │ └── lkl │ │ │ │ └── dalvikus │ │ │ │ ├── theme │ │ │ │ └── Theme.jvm.kt │ │ │ │ └── lexer │ │ │ │ ├── JSONLexerHighlight.jvm.kt │ │ │ │ ├── XmlLexerHighlight.jvm.kt │ │ │ │ └── JavaHighlightParserVisitor.kt │ │ └── main.kt │ │ └── java │ │ └── me │ │ └── lkl │ │ └── dalvikus │ │ └── lexer │ │ └── java │ │ └── JavaParserBase.java ├── proguard-rules.pro └── build.gradle.kts ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .gitignore ├── gradle.properties ├── settings.gradle.kts ├── .github └── workflows │ └── release.yml ├── gradlew.bat └── README.MD /docs/screenshots/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/docs/screenshots/search.png -------------------------------------------------------------------------------- /docs/screenshots/resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/docs/screenshots/resources.png -------------------------------------------------------------------------------- /docs/screenshots/dark_theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/docs/screenshots/dark_theme.png -------------------------------------------------------------------------------- /docs/screenshots/decompiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/docs/screenshots/decompiler.png -------------------------------------------------------------------------------- /composeApp/desktopAppIcons/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/desktopAppIcons/logo.ico -------------------------------------------------------------------------------- /composeApp/desktopAppIcons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/desktopAppIcons/logo.png -------------------------------------------------------------------------------- /docs/screenshots/smali_editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/docs/screenshots/smali_editor.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /composeApp/desktopAppIcons/logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/desktopAppIcons/logo.icns -------------------------------------------------------------------------------- /docs/screenshots/sign_and_deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/docs/screenshots/sign_and_deploy.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-Bold.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-Light.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/values-de/opcodes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/values-hi/opcodes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-Italic.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-Medium.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-Regular.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-SemiBold.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-BoldItalic.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-ExtraBold.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-ExtraLight.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-LightItalic.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-MediumItalic.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loerting/dalvikus/HEAD/composeApp/src/commonMain/composeResources/font/MonaspaceNeonFrozen-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /composeApp/src/jvmMain/kotlin/me/lkl/dalvikus/theme/Theme.jvm.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.theme 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | internal actual fun SystemAppearance(isDark: Boolean) { 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.iml 3 | .gradle 4 | .idea 5 | .kotlin 6 | .DS_Store 7 | build 8 | */build 9 | captures 10 | .externalNativeBuild 11 | .cxx 12 | local.properties 13 | xcuserdata/ 14 | Pods/ 15 | *.jks 16 | *.gpg 17 | *yarn.lock 18 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/decompiler/Decompiler.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.decompiler 2 | 3 | import com.android.tools.smali.dexlib2.iface.ClassDef 4 | 5 | interface Decompiler { 6 | suspend fun decompile(classDef: ClassDef): String 7 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/lexer/JSONLexerHighlight.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.lexer 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | import me.lkl.dalvikus.theme.CodeHighlightColors 5 | 6 | expect fun highlightJsonCode(code: String, colors: CodeHighlightColors): AnnotatedString -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/lexer/JavaLexerHighlight.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.lexer 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | import me.lkl.dalvikus.theme.CodeHighlightColors 5 | 6 | expect fun highlightJavaCode(code: String, colors: CodeHighlightColors): AnnotatedString -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/lexer/XmlLexerHighlight.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.lexer 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | import me.lkl.dalvikus.theme.CodeHighlightColors 5 | 6 | expect fun highlightXmlCode(code: String, colors: CodeHighlightColors): AnnotatedString -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/settings/Shortcuts.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.settings 2 | 3 | import androidx.compose.ui.input.key.* 4 | 5 | internal val shortcutSave = Key.S 6 | internal val shortcutFind = Key.F 7 | internal val shortcutToggleEditorDecompiler = Key.D 8 | internal val shortcutTreeAdd = Key.N 9 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/nav/NavItem.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.ui.nav 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | import org.jetbrains.compose.resources.StringResource 5 | 6 | data class NavItem( 7 | val key: String, 8 | val icon: ImageVector, 9 | val labelRes: StringResource 10 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/util/Throwables.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.util 2 | 3 | fun Throwable.toOneLiner(maxLength: Int = 100): String { 4 | return this.message?.let { message -> 5 | if (message.length > maxLength) { 6 | message.take(maxLength) + "..." 7 | } else { 8 | message 9 | } 10 | } ?: this.javaClass.simpleName 11 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx4G 3 | org.gradle.caching=true 4 | org.gradle.configuration-cache=true 5 | org.gradle.daemon=true 6 | org.gradle.parallel=true 7 | 8 | #Kotlin 9 | kotlin.code.style=official 10 | kotlin.daemon.jvmargs=-Xmx4G 11 | kotlin.native.binary.gc=cms 12 | kotlin.incremental.wasm=true 13 | 14 | #Android 15 | android.useAndroidX=true 16 | android.nonTransitiveRClass=true 17 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/filesystem/FileSystemFileNode.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree.filesystem 2 | 3 | import me.lkl.dalvikus.tree.ContainerNode 4 | import me.lkl.dalvikus.tree.backing.BackingFileNode 5 | import me.lkl.dalvikus.tree.backing.FileBacking 6 | import java.io.File 7 | 8 | class FileSystemFileNode( 9 | val file: File, 10 | parent: ContainerNode? 11 | ) : BackingFileNode( 12 | file.name, 13 | FileBacking(file), 14 | parent 15 | ) -------------------------------------------------------------------------------- /composeApp/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -ignorewarnings 2 | -dontwarn kotlinx.coroutines.** 3 | -dontwarn kotlin.** 4 | -dontwarn java.awt.** 5 | -dontwarn javax.swing.** 6 | 7 | -dontwarn com.ibm.icu.** 8 | -dontwarn sun.misc.** 9 | 10 | -keep class com.ibm.icu.** { *; } 11 | -keepattributes SourceFile,LineNumberTable 12 | 13 | # keep smali as reflection is used for highlighting 14 | -keep class com.android.tools.smali.** { *; } 15 | 16 | # keep JADX as it crashes if not 17 | -keep class jadx.** { *; } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tabs/contentprovider/NopContentProvider.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tabs.contentprovider 2 | 3 | class NopContentProvider : ContentProvider() { 4 | override suspend fun loadContent() {} 5 | override fun getFileType(): String = "txt" 6 | 7 | override fun getSourcePath(): String? = null 8 | override fun getSizeEstimate(): Long = 0L 9 | 10 | override fun isDisplayable(): Boolean = false 11 | override fun isEditable(): Boolean = false 12 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/archive/ZipEntryFileNode.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree.archive 2 | 3 | import me.lkl.dalvikus.tree.ContainerNode 4 | import me.lkl.dalvikus.tree.backing.BackingFileNode 5 | import me.lkl.dalvikus.tree.backing.ZipBacking 6 | 7 | open class ZipEntryFileNode( 8 | name: String, 9 | path: String, 10 | zipRoot: ZipNode, 11 | parent: ContainerNode? 12 | ) : BackingFileNode( 13 | name, 14 | ZipBacking(path, zipRoot), 15 | parent 16 | ) -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/util/Hex.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.util 2 | 3 | import org.stringtemplate.v4.ST 4 | 5 | fun String.is0xHex(): Boolean { 6 | return this.startsWith("0x") && this.drop(2).all { it.isDigit() || (it in 'a'..'f') || (it in 'A'..'F') } 7 | } 8 | 9 | fun Long.to0xHex(): String { 10 | val sign = if (this < 0) "-" else "" 11 | return "${sign}0x${this.toString(16).lowercase()}" 12 | } 13 | 14 | fun String.base10To0xOrNull(): String? { 15 | return this.toLongOrNull()?.to0xHex() 16 | } 17 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_dark_mode.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/smali/SoloDexFileWrapper.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.smali 2 | 3 | import com.android.tools.smali.dexlib2.Opcodes 4 | import com.android.tools.smali.dexlib2.iface.ClassDef 5 | import com.android.tools.smali.dexlib2.iface.DexFile 6 | import me.lkl.dalvikus.dalvikusSettings 7 | 8 | class SoloDexFileWrapper( 9 | private val classDef: ClassDef 10 | ) : DexFile { 11 | override fun getClasses(): Set = setOf(classDef) 12 | override fun getOpcodes(): Opcodes = Opcodes.forApi(dalvikusSettings["api_level"] as Int) 13 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/editor/suggestions/AssistSuggestion.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.ui.editor.suggestions 2 | 3 | import androidx.compose.ui.text.SpanStyle 4 | import org.jetbrains.compose.resources.StringResource 5 | 6 | enum class SuggestionType { 7 | Instruction, 8 | Directive, 9 | Register, 10 | Access, 11 | LabelOrType, 12 | } 13 | 14 | data class AssistSuggestion( 15 | val text: String, 16 | val description: StringResource? = null, 17 | val type: SuggestionType, 18 | val spanStyle: SpanStyle? = null 19 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tabs/TabElement.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tabs 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.MutableState 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | import androidx.compose.ui.text.TextRange 8 | import me.lkl.dalvikus.tabs.contentprovider.ContentProvider 9 | import me.lkl.dalvikus.ui.editor.EditorViewModel 10 | 11 | interface TabElement { 12 | val tabId: String 13 | 14 | @Composable 15 | fun tabName(): String 16 | 17 | val tabIcon: ImageVector 18 | 19 | var hasUnsavedChanges: MutableState 20 | 21 | val contentProvider: ContentProvider 22 | 23 | var editorViewModel: EditorViewModel? 24 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_rotate_right.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tabs/CodeTab.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tabs 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.MutableState 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | import me.lkl.dalvikus.tree.FileNode 8 | import me.lkl.dalvikus.ui.editor.EditorViewModel 9 | 10 | class CodeTab( 11 | private val tabName: String, 12 | override val tabId: String, 13 | override val tabIcon: ImageVector, 14 | override val contentProvider: FileNode, 15 | ) : TabElement { 16 | override var hasUnsavedChanges: MutableState = mutableStateOf(false) 17 | 18 | override var editorViewModel: EditorViewModel? = null 19 | 20 | @Composable 21 | override fun tabName(): String = tabName 22 | 23 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/util/SearchOptions.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.util 2 | 3 | import co.touchlab.kermit.Logger 4 | 5 | data class SearchOptions( 6 | val caseSensitive: Boolean = false, 7 | val useRegex: Boolean = false 8 | ) 9 | 10 | fun createSearchMatcher(query: String, options: SearchOptions): ((String) -> Boolean) { 11 | return if (options.useRegex) { 12 | try { 13 | val regex = if (options.caseSensitive) { 14 | Regex(query) 15 | } else { 16 | Regex(query, RegexOption.IGNORE_CASE) 17 | } 18 | { input -> regex.containsMatchIn(input) } 19 | } catch (e: Exception) { 20 | { input: String -> false } 21 | } 22 | } else { 23 | { input -> input.contains(query, ignoreCase = !options.caseSensitive) } 24 | } 25 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/NodeFactory.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree 2 | 3 | import me.lkl.dalvikus.tree.archive.ApkNode 4 | import me.lkl.dalvikus.tree.archive.ZipNode 5 | import me.lkl.dalvikus.tree.backing.BackingFileNode 6 | import me.lkl.dalvikus.tree.backing.FileBacking 7 | import me.lkl.dalvikus.tree.dex.DexFileNode 8 | import me.lkl.dalvikus.tree.filesystem.FileSystemFileNode 9 | import java.io.File 10 | 11 | object NodeFactory { 12 | fun createNode(file: File, parent: ContainerNode): Node { 13 | return when (file.extension.lowercase()) { 14 | "apk"-> ApkNode(file.name, FileBacking(file), parent) 15 | "aab", "jar", "zip", "xapk", "apks" -> ZipNode(file.name, FileBacking(file), parent) 16 | "dex", "odex" -> DexFileNode(file.name, FileBacking(file), parent) 17 | else -> FileSystemFileNode(file, parent) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/editor/highlight/Highlight.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.ui.editor.highlight 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | import me.lkl.dalvikus.lexer.highlightJavaCode 5 | import me.lkl.dalvikus.lexer.highlightJsonCode 6 | import me.lkl.dalvikus.lexer.highlightSmaliCode 7 | import me.lkl.dalvikus.lexer.highlightXmlCode 8 | import me.lkl.dalvikus.theme.CodeHighlightColors 9 | 10 | fun highlightCode(code: String, codeType: String, colors: CodeHighlightColors): AnnotatedString { 11 | if (code.trim().isEmpty()) return AnnotatedString(code) 12 | return when (codeType.lowercase()) { 13 | "json" -> highlightJsonCode(code, colors) 14 | "xml", "html" -> highlightXmlCode(code, colors) 15 | "java" -> highlightJavaCode(code, colors) 16 | "smali" -> highlightSmaliCode(code, colors) 17 | else -> AnnotatedString(code) 18 | } 19 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tabs/contentprovider/ContentProvider.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tabs.contentprovider 2 | 3 | import kotlinx.coroutines.flow.MutableStateFlow 4 | import kotlinx.coroutines.flow.StateFlow 5 | import kotlinx.coroutines.flow.asStateFlow 6 | 7 | abstract class ContentProvider { 8 | protected val _contentFlow = MutableStateFlow(ByteArray(0)) 9 | open val contentFlow: StateFlow = _contentFlow.asStateFlow() 10 | 11 | abstract suspend fun loadContent() 12 | 13 | open suspend fun updateContent(newContent: ByteArray) { 14 | if(!isEditable()) throw IllegalArgumentException("ContentProvider is not editable.") 15 | _contentFlow.value = newContent 16 | } 17 | 18 | abstract fun getFileType(): String 19 | 20 | abstract fun getSourcePath(): String? 21 | 22 | abstract fun getSizeEstimate(): Long 23 | abstract fun isDisplayable(): Boolean 24 | abstract fun isEditable(): Boolean 25 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/decompiler/DecompilerContentProvider.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.decompiler 2 | 3 | import me.lkl.dalvikus.tabs.contentprovider.ContentProvider 4 | import me.lkl.dalvikus.tree.dex.DexEntryClassNode 5 | 6 | class DecompilerContentProvider(private val dexEntryClassNode: DexEntryClassNode, private val decompilerProvider: () -> Decompiler) : ContentProvider() { 7 | override suspend fun loadContent() { 8 | _contentFlow.value = decompilerProvider().decompile(dexEntryClassNode.getClassDef()).toByteArray() 9 | } 10 | 11 | override fun getFileType(): String = "java" 12 | 13 | override fun getSourcePath(): String? = dexEntryClassNode.getSourcePath() 14 | override fun getSizeEstimate(): Long { 15 | return maxOf( 16 | contentFlow.value.size.toLong(), 17 | 16 * 1024 // 16 kB 18 | ) 19 | } 20 | 21 | override fun isDisplayable(): Boolean = true 22 | override fun isEditable(): Boolean = false 23 | } 24 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/util/HazeDefaults.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.util 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.luminance 7 | import androidx.compose.ui.unit.dp 8 | import dev.chrisbanes.haze.HazeStyle 9 | import dev.chrisbanes.haze.HazeTint 10 | 11 | @Composable 12 | fun defaultHazeStyle( 13 | containerColor: Color = MaterialTheme.colorScheme.surface, 14 | ): HazeStyle = hazeMaterial( 15 | containerColor = containerColor, 16 | lightAlpha = 0.2f, 17 | darkAlpha = 0.4f, 18 | ) 19 | 20 | private fun hazeMaterial( 21 | containerColor: Color, 22 | lightAlpha: Float, 23 | darkAlpha: Float, 24 | ): HazeStyle = HazeStyle( 25 | blurRadius = 10.dp, 26 | backgroundColor = containerColor, 27 | tint = HazeTint( 28 | containerColor.copy(alpha = if (containerColor.luminance() >= 0.5) lightAlpha else darkAlpha), 29 | ), 30 | ) -------------------------------------------------------------------------------- /composeApp/src/jvmMain/java/me/lkl/dalvikus/lexer/java/JavaParserBase.java: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.lexer.java; 2 | 3 | import org.antlr.v4.runtime.*; 4 | import java.io.Writer; 5 | import java.util.List; 6 | 7 | public abstract class JavaParserBase extends Parser { 8 | 9 | public JavaParserBase(TokenStream input){ 10 | super(input); 11 | } 12 | 13 | public boolean DoLastRecordComponent() { 14 | ParserRuleContext ctx = this.getContext(); 15 | if (!(ctx instanceof JavaParser.RecordComponentListContext)) { 16 | return true; // or throw if this is an unexpected state 17 | } 18 | 19 | JavaParser.RecordComponentListContext tctx = (JavaParser.RecordComponentListContext) ctx; 20 | List rcs = tctx.recordComponent(); 21 | if (rcs.isEmpty()) return true; 22 | 23 | int count = rcs.size(); 24 | for (int c = 0; c < count; ++c) { 25 | JavaParser.RecordComponentContext rc = rcs.get(c); 26 | if (rc.ELLIPSIS() != null && c + 1 < count) 27 | return false; 28 | } 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_cyclone.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/util/ApkTool.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.util 2 | 3 | import brut.androlib.Config 4 | import brut.androlib.exceptions.AndrolibException 5 | import brut.xmlpull.MXSerializer 6 | import org.xmlpull.v1.XmlSerializer 7 | 8 | 9 | fun getApkToolConfig(): Config { 10 | val config = Config() 11 | config.isKeepBrokenResources = true 12 | return config 13 | } 14 | 15 | @Throws(AndrolibException::class) 16 | fun newXmlSerializer(): XmlSerializer { 17 | try { 18 | val serial: XmlSerializer = MXSerializer() 19 | serial.setFeature(MXSerializer.FEATURE_ATTR_VALUE_NO_ESCAPE, true) 20 | serial.setProperty(MXSerializer.PROPERTY_DEFAULT_ENCODING, "utf-8") 21 | serial.setProperty(MXSerializer.PROPERTY_INDENTATION, " ") 22 | serial.setProperty(MXSerializer.PROPERTY_LINE_SEPARATOR, System.lineSeparator()) 23 | return serial 24 | } catch (ex: IllegalArgumentException) { 25 | throw AndrolibException(ex) 26 | } catch (ex: IllegalStateException) { 27 | throw AndrolibException(ex) 28 | } 29 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tabs/ImageTab.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tabs 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Image 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.MutableState 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import me.lkl.dalvikus.tree.archive.ZipEntryImageNode 10 | import me.lkl.dalvikus.ui.editor.EditorViewModel 11 | 12 | class ImageTab( 13 | val tabName: String, 14 | override val tabId: String, 15 | override val contentProvider: ZipEntryImageNode, 16 | ) : TabElement { 17 | override var hasUnsavedChanges: MutableState = mutableStateOf(false) 18 | 19 | override var editorViewModel: EditorViewModel? = null 20 | set(value) { 21 | throw IllegalStateException("ImageTab does not support editor view model") 22 | } 23 | 24 | @Composable 25 | override fun tabName(): String = tabName 26 | override val tabIcon: ImageVector = Icons.Filled.Image 27 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/util/Modifiers.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.util 2 | 3 | import androidx.compose.ui.Modifier 4 | import androidx.compose.ui.input.key.* 5 | import androidx.compose.ui.layout.layout 6 | 7 | fun Modifier.handleFocusedCtrlShortcuts( 8 | enabled: Boolean = true, 9 | keyActionMap: Map Unit> 10 | ): Modifier = this.then( 11 | if (enabled) Modifier.onPreviewKeyEvent(ctrlShortcuts(keyActionMap)) else Modifier 12 | ) 13 | 14 | fun ctrlShortcuts( 15 | keyActionMap: Map Unit> 16 | ): (KeyEvent) -> Boolean = { event -> 17 | if ( 18 | event.type == KeyEventType.KeyDown && 19 | (event.isCtrlPressed || event.isMetaPressed) 20 | ) { 21 | keyActionMap[event.key]?.let { 22 | it.invoke() 23 | true 24 | } 25 | } 26 | false 27 | } 28 | 29 | 30 | fun Modifier.withoutWidthConstraints() = layout { measurable, constraints -> 31 | val placeable = measurable.measure(constraints.copy(maxWidth = Int.MAX_VALUE)) 32 | layout(constraints.maxWidth, placeable.height) { 33 | placeable.place(0, 0) 34 | } 35 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/tabs/TabContentRenderer.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.ui.tabs 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.BugReport 6 | import androidx.compose.material3.* 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | import me.lkl.dalvikus.tabs.CodeTab 11 | import me.lkl.dalvikus.tabs.ImageTab 12 | import me.lkl.dalvikus.tabs.SmaliTab 13 | import me.lkl.dalvikus.tabs.TabElement 14 | import me.lkl.dalvikus.tabs.WelcomeTab 15 | import me.lkl.dalvikus.ui.editor.EditorView 16 | import me.lkl.dalvikus.ui.image.ImageView 17 | 18 | @OptIn(ExperimentalMaterial3Api::class) 19 | @Composable 20 | fun TabContentRenderer(tab: TabElement) { 21 | when (tab) { 22 | is WelcomeTab -> WelcomeView() 23 | is CodeTab -> EditorView(tab) 24 | is SmaliTab -> EditorView(tab) 25 | is ImageTab -> ImageView(tab) 26 | else -> throw IllegalArgumentException("Unsupported tab type: ${tab::class.simpleName}") 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "dalvikus" 2 | 3 | pluginManagement { 4 | repositories { 5 | google { 6 | content { 7 | includeGroupByRegex("com\\.android.*") 8 | includeGroupByRegex("com\\.google.*") 9 | includeGroupByRegex("androidx.*") 10 | includeGroupByRegex("android.*") 11 | } 12 | } 13 | gradlePluginPortal() 14 | mavenCentral() 15 | } 16 | } 17 | 18 | dependencyResolutionManagement { 19 | repositories { 20 | google { 21 | content { 22 | includeGroupByRegex("com\\.android.*") 23 | includeGroupByRegex("com\\.google.*") 24 | includeGroupByRegex("androidx.*") 25 | includeGroupByRegex("android.*") 26 | } 27 | } 28 | mavenCentral() 29 | //maven { url = uri("https://jitpack.io") } 30 | } 31 | } 32 | plugins { 33 | //https://github.com/JetBrains/compose-hot-reload?tab=readme-ov-file#set-up-automatic-provisioning-of-the-jetbrains-runtime-jbr-via-gradle 34 | id("org.gradle.toolchains.foojay-resolver-convention").version("0.10.0") 35 | } 36 | 37 | include(":composeApp") 38 | 39 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/dex/DexEntryPackageNode.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree.dex 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.FolderSpecial 5 | import me.lkl.dalvikus.tree.ContainerNode 6 | import me.lkl.dalvikus.tree.Node 7 | import me.lkl.dalvikus.tree.buildChildNodes 8 | 9 | open class DexEntryPackageNode( 10 | override val name: String, 11 | private val packagePath: String, // e.g., com/example/ 12 | private val root: DexFileNode, 13 | override val parent: ContainerNode? 14 | ) : ContainerNode() { 15 | 16 | override val icon = Icons.Default.FolderSpecial 17 | override val changesWithChildren = false 18 | 19 | override suspend fun loadChildrenInternal(): List { 20 | return buildChildNodes( 21 | entries = root.entries, 22 | prefix = packagePath, 23 | onFolder = { name, path -> 24 | DexEntryPackageNode(name, path, root, this) 25 | }, 26 | onFile = { name, path, classDef -> 27 | DexEntryClassNode(name, path, root, this) 28 | } 29 | ) 30 | } 31 | 32 | 33 | override suspend fun rebuild() { 34 | // No-op; DexFileNode handles writing 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_light_mode.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/RightPanel.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.ui 2 | 3 | import SettingsView 4 | import androidx.compose.animation.AnimatedContent 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import me.lkl.dalvikus.selectedNavItem 10 | import me.lkl.dalvikus.ui.packaging.PackagingView 11 | import me.lkl.dalvikus.ui.packaging.PackagingViewModel 12 | import me.lkl.dalvikus.ui.resources.ResourcesView 13 | import me.lkl.dalvikus.ui.tabs.TabView 14 | import me.lkl.dalvikus.util.DefaultCard 15 | 16 | @Composable 17 | internal fun RightPanelContent() { 18 | Box( 19 | modifier = Modifier.fillMaxSize() 20 | ) { 21 | AnimatedContent(targetState = selectedNavItem, label = "NavItem Animation") { targetTab -> 22 | when (targetTab) { 23 | "Editor", "Decompiler" -> DefaultCard { TabView() } 24 | "Resources" -> ResourcesView() 25 | "Packaging" -> PackagingView() 26 | "Settings" -> SettingsView() 27 | else -> throw IllegalArgumentException( 28 | "Unsupported selectedNavItem: $selectedNavItem. " 29 | ) 30 | } 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/archive/ZipEntryImageNode.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree.archive 2 | 3 | import brut.androlib.res.decoder.AXmlResourceParser 4 | import brut.androlib.res.decoder.AndroidManifestPullStreamDecoder 5 | import brut.androlib.res.decoder.AndroidManifestResourceParser 6 | import brut.androlib.res.decoder.ResStreamDecoder 7 | import brut.androlib.res.decoder.ResXmlPullStreamDecoder 8 | import me.lkl.dalvikus.tabs.CodeTab 9 | import me.lkl.dalvikus.tabs.ImageTab 10 | import me.lkl.dalvikus.tabs.TabElement 11 | import me.lkl.dalvikus.tree.ContainerNode 12 | import me.lkl.dalvikus.util.guessIfEditableTextually 13 | import me.lkl.dalvikus.util.newXmlSerializer 14 | import java.io.ByteArrayOutputStream 15 | import kotlin.properties.Delegates 16 | 17 | 18 | class ZipEntryImageNode( 19 | override val name: String, 20 | private val fullPath: String, 21 | private val zipRoot: ZipNode, 22 | override val parent: ContainerNode? 23 | ) : ZipEntryFileNode(name, fullPath, zipRoot, parent) { 24 | 25 | override fun isDisplayable(): Boolean = true 26 | override fun isEditable(): Boolean = false 27 | 28 | override suspend fun createTab(): TabElement { 29 | return ImageTab( 30 | tabName = name, 31 | tabId = fullPath, 32 | contentProvider = this 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/theme/CodeHighlightColors.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.theme 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import com.materialkolor.ktx.harmonize 7 | 8 | data class CodeHighlightColors( 9 | val onSurface: Color, 10 | val primary: Color, 11 | val secondary: Color, 12 | val tertiary: Color, 13 | val quaternary: Color, 14 | val quinary: Color, 15 | val senary: Color, 16 | val septenary: Color, 17 | val error: Color 18 | ) 19 | 20 | @Composable 21 | fun defaultCodeHighlightColors(darkTheme: Boolean): CodeHighlightColors { 22 | return CodeHighlightColors( 23 | onSurface = MaterialTheme.colorScheme.onSurface, 24 | primary = MaterialTheme.colorScheme.primary, 25 | secondary = MaterialTheme.colorScheme.secondary, 26 | tertiary = MaterialTheme.colorScheme.tertiary, 27 | quaternary = Color(0xFF7B6FB2).harmonize(MaterialTheme.colorScheme.primary), 28 | quinary = Color(0xFF9EAD6F).harmonize(MaterialTheme.colorScheme.primary), 29 | senary = Color(0xFFBD875B).harmonize(MaterialTheme.colorScheme.primary), 30 | septenary = Color(0xFF5BAEA0).harmonize(MaterialTheme.colorScheme.primary), 31 | error = MaterialTheme.colorScheme.error 32 | ) 33 | } -------------------------------------------------------------------------------- /composeApp/src/jvmMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.mutableStateOf 2 | import androidx.compose.runtime.remember 3 | import androidx.compose.ui.unit.dp 4 | import androidx.compose.ui.window.Window 5 | import androidx.compose.ui.window.application 6 | import androidx.compose.ui.window.rememberWindowState 7 | import me.lkl.dalvikus.App 8 | import me.lkl.dalvikus.keyActionMap 9 | import me.lkl.dalvikus.errorreport.handleUncaughtExceptions 10 | import me.lkl.dalvikus.util.ctrlShortcuts 11 | import java.awt.Dimension 12 | import javax.swing.UIManager 13 | 14 | fun main() = application { 15 | try { 16 | System.setProperty("compose.interop.blending", "true") 17 | UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()) 18 | } catch (e: Exception) { 19 | e.printStackTrace() 20 | } 21 | handleUncaughtExceptions() 22 | 23 | val showExitDialog = remember { mutableStateOf(false) } 24 | 25 | Window( 26 | title = "dalvikus", 27 | state = rememberWindowState(width = 1200.dp, height = 800.dp), 28 | onCloseRequest = { 29 | showExitDialog.value = true 30 | }, 31 | onKeyEvent = ctrlShortcuts(keyActionMap), 32 | ) { 33 | window.minimumSize = Dimension(350, 600) 34 | App(showExitDialog, onExitConfirmed = { 35 | exitApplication() 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/settings/DalvikusBaksmaliOptions.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.settings 2 | 3 | import com.android.tools.smali.baksmali.BaksmaliOptions 4 | 5 | class DalvikusBaksmaliOptions(private val dalvikusSettings: DalvikusSettings) : BaksmaliOptions() { 6 | 7 | init { 8 | loadSettings() 9 | } 10 | 11 | fun loadSettings() { 12 | apiLevel = dalvikusSettings["api_level"] as Int 13 | parameterRegisters = dalvikusSettings["smali_parameter_registers"] as Boolean 14 | localsDirective = dalvikusSettings["smali_locals_directive"] as Boolean 15 | sequentialLabels = dalvikusSettings["smali_sequential_labels"] as Boolean 16 | debugInfo = dalvikusSettings["smali_debug_info"] as Boolean 17 | codeOffsets = dalvikusSettings["smali_code_offsets"] as Boolean 18 | accessorComments = dalvikusSettings["smali_accessor_comments"] as Boolean 19 | allowOdex = dalvikusSettings["smali_allow_odex"] as Boolean 20 | deodex = dalvikusSettings["smali_deodex"] as Boolean 21 | implicitReferences = dalvikusSettings["smali_implicit_references"] as Boolean 22 | normalizeVirtualMethods = dalvikusSettings["smali_normalize_virtual_methods"] as Boolean 23 | //registerInfo = ... TODO maybe implement this. 24 | } 25 | 26 | fun reloadSettings() { 27 | loadSettings() 28 | } 29 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tabs/WelcomeTab.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tabs 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.EmojiPeople 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.MutableState 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import dalvikus.composeapp.generated.resources.Res 10 | import dalvikus.composeapp.generated.resources.welcome_tab_name 11 | import me.lkl.dalvikus.tabs.contentprovider.ContentProvider 12 | import me.lkl.dalvikus.tabs.contentprovider.NopContentProvider 13 | import me.lkl.dalvikus.ui.editor.EditorViewModel 14 | import org.jetbrains.compose.resources.stringResource 15 | 16 | data class WelcomeTab( 17 | override val tabId: String = "welcome", 18 | override val tabIcon: ImageVector = Icons.Default.EmojiPeople 19 | ) : TabElement { 20 | override var hasUnsavedChanges: MutableState = mutableStateOf(false) 21 | override val contentProvider: ContentProvider = NopContentProvider() 22 | override var editorViewModel: EditorViewModel? = null 23 | set(value) { 24 | throw IllegalStateException("WelcomeTab does not support editor view model") 25 | } 26 | @Composable 27 | override fun tabName(): String = stringResource(Res.string.welcome_tab_name) 28 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/filesystem/FileSystemFolderNode.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree.filesystem 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Folder 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import me.lkl.dalvikus.tree.ContainerNode 7 | import me.lkl.dalvikus.tree.Metadata 8 | import me.lkl.dalvikus.tree.Node 9 | import java.io.File 10 | 11 | class FileSystemFolderNode( 12 | override val name: String, 13 | private val file: File, 14 | override val parent: ContainerNode? 15 | ) : ContainerNode() { 16 | 17 | override val icon: ImageVector 18 | get() = Icons.Filled.Folder // Use appropriate icon for folder 19 | override val changesWithChildren: Boolean = false 20 | 21 | override suspend fun loadChildrenInternal(): List { 22 | return file.listFiles()?.map { 23 | if (it.isDirectory) { 24 | FileSystemFolderNode(it.name, it, this) 25 | } else { 26 | FileSystemFileNode(it, this) 27 | } 28 | } ?: emptyList() 29 | } 30 | 31 | override suspend fun rebuild() { 32 | // Folders usually don't need rebuilding — NOP 33 | } 34 | 35 | override fun getMetadata(): Set> { 36 | return setOf( 37 | Metadata.LAST_EDITED to file.lastModified(), 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/util/GitHub.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.util 2 | 3 | import co.touchlab.kermit.Logger 4 | import java.io.BufferedReader 5 | import java.io.InputStreamReader 6 | import java.net.HttpURLConnection 7 | import java.net.URI 8 | 9 | fun getLatestReleaseTag(owner: String, repo: String): String? { 10 | val url = URI("https://api.github.com/repos/$owner/$repo/releases/latest").toURL() 11 | val connection = url.openConnection() as HttpURLConnection 12 | 13 | connection.requestMethod = "GET" 14 | connection.setRequestProperty("Accept", "application/vnd.github+json") 15 | connection.setRequestProperty("User-Agent", "KotlinApp") // Required by GitHub 16 | 17 | return try { 18 | if (connection.responseCode == 200) { 19 | val reader = BufferedReader(InputStreamReader(connection.inputStream)) 20 | val response = reader.readText() 21 | reader.close() 22 | 23 | // Extract tag_name manually (primitive parsing) 24 | val regex = Regex("\"tag_name\"\\s*:\\s*\"([^\"]+)\"") 25 | val match = regex.find(response) 26 | match?.groupValues?.get(1) 27 | } else { 28 | Logger.e("GitHub API error: HTTP ${connection.responseCode}") 29 | null 30 | } 31 | } catch (e: Exception) { 32 | Logger.e("Error fetching latest release tag: ${e.message}", e) 33 | null 34 | } finally { 35 | connection.disconnect() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/backing/Backing.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree.backing 2 | 3 | import me.lkl.dalvikus.tree.archive.ZipNode 4 | import java.io.File 5 | import java.io.InputStream 6 | 7 | interface Backing { 8 | suspend fun read(): ByteArray 9 | suspend fun write(data: ByteArray) 10 | suspend fun getFileOrCreateTemp(suffix: String): File 11 | fun inputStream(): InputStream 12 | fun size(): Long 13 | } 14 | 15 | class FileBacking(val file: File) : Backing { 16 | override suspend fun read(): ByteArray = file.readBytes() 17 | override suspend fun write(data: ByteArray) = file.writeBytes(data) 18 | override suspend fun getFileOrCreateTemp(suffix: String): File = file 19 | override fun inputStream(): InputStream = file.inputStream() 20 | override fun size(): Long = file.length() 21 | } 22 | 23 | class ZipBacking( 24 | private val entryPath: String, 25 | private val zipNode: ZipNode 26 | ) : Backing { 27 | override suspend fun read(): ByteArray = zipNode.readEntry(entryPath) 28 | override suspend fun write(data: ByteArray) = zipNode.updateEntry(entryPath, data) 29 | override suspend fun getFileOrCreateTemp(suffix: String): File { 30 | val tempFile = File.createTempFile("dalvikus_temp_", suffix) 31 | tempFile.writeBytes(read()) 32 | return tempFile 33 | } 34 | 35 | override fun inputStream(): InputStream = zipNode.readEntry(entryPath).inputStream() 36 | override fun size(): Long = zipNode.readEntry(entryPath).size.toLong() 37 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/root/HiddenRoot.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree.root 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.FolderSpecial 5 | import androidx.compose.runtime.mutableStateListOf 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | import me.lkl.dalvikus.tree.ContainerNode 8 | import me.lkl.dalvikus.tree.Node 9 | import me.lkl.dalvikus.tree.sortedTree 10 | 11 | class HiddenRoot( 12 | private val _backingChildren: MutableList = mutableStateListOf() 13 | ) : ContainerNode() { 14 | 15 | constructor(vararg children: Node) : this(mutableStateListOf(*children)) 16 | 17 | init { 18 | _childrenFlow.value = _backingChildren.sortedTree() 19 | } 20 | 21 | override val parent: ContainerNode? = null 22 | override val name: String = "root" 23 | override val icon: ImageVector = Icons.Outlined.FolderSpecial 24 | 25 | override val changesWithChildren: Boolean = false 26 | override val isRoot: Boolean = true 27 | 28 | override suspend fun loadChildrenInternal(): List = _backingChildren 29 | 30 | override suspend fun rebuild() { 31 | // No-op; HiddenRoot does not handle writing 32 | } 33 | 34 | fun addChild(node: Node) { 35 | _backingChildren.add(node) 36 | _childrenFlow.value = _backingChildren.sortedTree() 37 | } 38 | 39 | fun removeChild(node: Node) { 40 | _backingChildren.remove(node) 41 | _childrenFlow.value = _backingChildren.sortedTree() 42 | } 43 | 44 | fun clear() { 45 | _backingChildren.clear() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.* 5 | import androidx.compose.runtime.* 6 | import androidx.compose.ui.graphics.Color 7 | import com.materialkolor.DynamicMaterialExpressiveTheme 8 | import com.materialkolor.PaletteStyle 9 | import me.lkl.dalvikus.dalvikusSettings 10 | 11 | internal val SeedColor = Color(0xFF924A92) 12 | 13 | internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) } 14 | 15 | @OptIn(ExperimentalMaterial3ExpressiveApi::class) 16 | @Composable 17 | internal fun AppTheme( 18 | content: @Composable () -> Unit 19 | ) { 20 | val systemIsDark = isSystemInDarkTheme() 21 | val isDarkState = remember(systemIsDark) { mutableStateOf(systemIsDark) } 22 | 23 | val selectedPaletteStyle = dalvikusSettings["theme_palette_style"] as String 24 | 25 | CompositionLocalProvider( 26 | LocalThemeIsDark provides isDarkState 27 | ) { 28 | val isDark by isDarkState 29 | SystemAppearance(!isDark) 30 | DynamicMaterialExpressiveTheme( 31 | typography = Typography().withFontFamily(Monaspace()), 32 | seedColor = SeedColor, 33 | style = PaletteStyle.entries.firstOrNull { it.name == selectedPaletteStyle } 34 | ?: PaletteStyle.TonalSpot, 35 | motionScheme = MotionScheme.expressive(), 36 | isDark = isDark, 37 | animate = true, 38 | content = { Surface(content = content) }, 39 | ) 40 | } 41 | } 42 | 43 | @Composable 44 | internal expect fun SystemAppearance(isDark: Boolean) 45 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tabs/contentprovider/DualContentProvider.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tabs.contentprovider 2 | 3 | open class DualContentProvider(val contentProvider: ContentProvider, val contentProvider2: ContentProvider) : ContentProvider() { 4 | override val contentFlow 5 | get() = if (firstContentProvider) contentProvider.contentFlow else contentProvider2.contentFlow 6 | open var firstContentProvider: Boolean = true 7 | 8 | override suspend fun loadContent() { 9 | if(firstContentProvider) 10 | contentProvider.loadContent() 11 | else 12 | contentProvider2.loadContent() 13 | } 14 | 15 | override suspend fun updateContent(newContent: ByteArray) { 16 | if(firstContentProvider) 17 | contentProvider.updateContent(newContent) 18 | else 19 | contentProvider2.updateContent(newContent) 20 | } 21 | 22 | override fun getFileType(): String = if(firstContentProvider) contentProvider.getFileType() else contentProvider2.getFileType() 23 | 24 | override fun getSourcePath(): String? = if(firstContentProvider) contentProvider.getSourcePath() else contentProvider2.getSourcePath() 25 | override fun getSizeEstimate(): Long { 26 | return if(firstContentProvider) contentProvider.getSizeEstimate() else contentProvider2.getSizeEstimate() 27 | } 28 | 29 | override fun isDisplayable(): Boolean { 30 | return if(firstContentProvider) contentProvider.isDisplayable() else contentProvider2.isDisplayable() 31 | } 32 | 33 | override fun isEditable(): Boolean { 34 | return if(firstContentProvider) contentProvider.isEditable() else contentProvider2.isEditable() 35 | } 36 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/archive/ZipEntryFolderNode.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree.archive 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Folder 5 | import me.lkl.dalvikus.theme.readableImageFormats 6 | import me.lkl.dalvikus.tree.ContainerNode 7 | import me.lkl.dalvikus.tree.Node 8 | import me.lkl.dalvikus.tree.backing.ZipBacking 9 | import me.lkl.dalvikus.tree.buildChildNodes 10 | import me.lkl.dalvikus.tree.dex.DexFileNode 11 | 12 | class ZipEntryFolderNode( 13 | override val name: String, 14 | private val folderPath: String, // ends with "/" 15 | private val zipRoot: ZipNode, 16 | override val parent: ContainerNode? 17 | ) : ContainerNode() { 18 | 19 | override val icon 20 | get() = Icons.Default.Folder 21 | override val changesWithChildren = false 22 | 23 | override suspend fun loadChildrenInternal(): List { 24 | return buildChildNodes( 25 | entries = zipRoot.entries, 26 | prefix = folderPath, 27 | onFolder = { name, path -> 28 | ZipEntryFolderNode(name, path, zipRoot, this) 29 | }, 30 | onFile = { name, path, bytes -> 31 | // TODO this is code duplication with ZipNode, refactor to avoid it 32 | when { 33 | name.endsWith(".dex") -> DexFileNode(name, ZipBacking(path, zipRoot), this) 34 | name.endsWith(".xml") && zipRoot is ApkNode -> ApkEntryXmlNode(name, path, zipRoot, this) 35 | name.substringAfterLast(".").lowercase() in readableImageFormats -> ZipEntryImageNode(name, path, zipRoot, this) 36 | else -> ZipEntryFileNode(name, path, zipRoot, this) 37 | } 38 | } 39 | ) 40 | } 41 | 42 | 43 | override suspend fun rebuild() { 44 | // folders don’t rebuild directly - handled by ZipNode 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /composeApp/src/jvmMain/kotlin/me/lkl/dalvikus/lexer/JSONLexerHighlight.jvm.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.lexer 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | import androidx.compose.ui.text.SpanStyle 5 | import androidx.compose.ui.text.buildAnnotatedString 6 | import org.antlr.v4.runtime.CharStreams 7 | import org.antlr.v4.runtime.CommonTokenStream 8 | import me.lkl.dalvikus.theme.CodeHighlightColors 9 | 10 | actual fun highlightJsonCode(code: String, colors: CodeHighlightColors): AnnotatedString { 11 | val lexer = JSONLexer(CharStreams.fromString(code)) 12 | val tokens = CommonTokenStream(lexer) 13 | tokens.fill() 14 | 15 | return buildAnnotatedString { 16 | append(code) 17 | 18 | for (token in tokens.tokens) { 19 | if (token.type == -1 || token.text.isNullOrBlank()) continue 20 | 21 | getJsonTokenStyle(token.type, colors)?.let { 22 | addStyle(it, token.startIndex, token.stopIndex + 1) 23 | } 24 | } 25 | } 26 | } 27 | 28 | private fun getJsonTokenStyle(tokenType: Int, colors: CodeHighlightColors): SpanStyle? { 29 | return when (tokenType) { 30 | 31 | // === Punctuation: { } [ ] , : === 32 | JSONLexer.T__0, // { 33 | JSONLexer.T__1, // } 34 | JSONLexer.T__2, // [ 35 | JSONLexer.T__3, // ] 36 | JSONLexer.T__4, // : 37 | JSONLexer.T__5 // , 38 | -> SpanStyle(color = colors.onSurface.copy(alpha = 0.6f)) 39 | 40 | // === Keywords: true, false, null === 41 | JSONLexer.T__6, // true 42 | JSONLexer.T__7, // false 43 | JSONLexer.T__8 // null 44 | -> SpanStyle(color = colors.secondary) 45 | 46 | // === Strings === 47 | JSONLexer.STRING -> SpanStyle(color = colors.primary) 48 | 49 | // === Numbers === 50 | JSONLexer.NUMBER -> SpanStyle(color = colors.tertiary) 51 | 52 | // === Whitespace (ignored) === 53 | JSONLexer.WS -> null 54 | 55 | else -> null 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/tabs/TabManager.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.ui.tabs 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateListOf 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import me.lkl.dalvikus.tabs.TabElement 8 | import me.lkl.dalvikus.tabs.WelcomeTab 9 | 10 | class TabManager(initialTabs: List) { 11 | private val _tabs = mutableStateListOf().apply { addAll(initialTabs) } 12 | val tabs: List get() = _tabs 13 | val currentTab: TabElement 14 | get() = _tabs.getOrNull(selectedTabIndex) ?: WelcomeTab() 15 | 16 | var selectedTabIndex by mutableStateOf(0) 17 | 18 | fun selectTab(index: Int) { 19 | if (index in _tabs.indices) { 20 | selectedTabIndex = index 21 | } 22 | } 23 | 24 | fun closeTab(tab: TabElement) { 25 | val index = _tabs.indexOf(tab) 26 | if (index != -1) { 27 | _tabs.removeAt(index) 28 | if (selectedTabIndex >= _tabs.size) { 29 | selectedTabIndex = maxOf(0, _tabs.lastIndex) 30 | } 31 | } 32 | } 33 | 34 | fun closeTabs(tabsToClose: List) { 35 | val currentTab = _tabs.getOrNull(selectedTabIndex) 36 | 37 | tabsToClose.forEach { tab -> 38 | _tabs.remove(tab) 39 | } 40 | 41 | // Adjust selected tab index 42 | if (_tabs.isEmpty()) { 43 | selectedTabIndex = 0 44 | } else if (currentTab != null && currentTab in _tabs) { 45 | selectedTabIndex = _tabs.indexOf(currentTab) 46 | } else { 47 | selectedTabIndex = minOf(selectedTabIndex, _tabs.lastIndex) 48 | } 49 | } 50 | 51 | fun addTab(tab: TabElement) { 52 | _tabs.add(tab) 53 | selectedTabIndex = _tabs.lastIndex 54 | } 55 | 56 | fun addOrSelectTab(tab: TabElement) { 57 | val existingIndex = _tabs.indexOfFirst { it.tabId == tab.tabId } 58 | if (existingIndex != -1) { 59 | selectTab(existingIndex) 60 | } else { 61 | addTab(tab) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/decompiler/JADXDecompiler.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.decompiler 2 | import com.android.tools.smali.dexlib2.iface.ClassDef 3 | import com.android.tools.smali.dexlib2.writer.io.MemoryDataStore 4 | import com.android.tools.smali.dexlib2.writer.pool.DexPool 5 | import jadx.api.CommentsLevel 6 | import jadx.api.JadxArgs 7 | import jadx.api.JadxDecompiler 8 | import jadx.api.impl.NoOpCodeCache 9 | import me.lkl.dalvikus.dalvikusSettings 10 | import me.lkl.dalvikus.smali.SoloDexFileWrapper 11 | import java.io.File 12 | import java.nio.file.Files 13 | 14 | class JADXDecompiler : Decompiler { 15 | override suspend fun decompile(classDef: ClassDef): String { 16 | return try { 17 | // Create a temporary DexFile that contains only this classDef 18 | val tempDex = SoloDexFileWrapper(classDef) 19 | 20 | // Write DexFile to memory 21 | val dataStore = MemoryDataStore().also { 22 | DexPool.writeTo(it, tempDex) 23 | } 24 | 25 | // Write to a temporary file 26 | val tempDexFile = File.createTempFile("dalvikus", ".dex").apply { 27 | writeBytes(dataStore.data) 28 | deleteOnExit() 29 | } 30 | // Setup Jadx arguments 31 | val jadxArgs = JadxArgs().apply { 32 | inputFiles = listOf(tempDexFile) 33 | commentsLevel = 34 | if (dalvikusSettings["decompiler_verbose"]) CommentsLevel.DEBUG else CommentsLevel.WARN 35 | isShowInconsistentCode = true 36 | isDebugInfo = false 37 | codeCache = NoOpCodeCache() 38 | } 39 | 40 | JadxDecompiler(jadxArgs).use { jadx -> 41 | jadx.load() 42 | // Return the decompiled source of the first class found 43 | val decompiled = jadx.classes.firstOrNull()?.code 44 | decompiled ?: "Error: No classes found during decompilation." 45 | } 46 | } catch (e: Exception) { 47 | // Handle exceptions, could wrap with a translation call or simple error message 48 | "Decompilation error: ${e.message}" 49 | } 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/tree/TreeDragAndDropTarget.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.ui.tree 2 | 3 | import androidx.compose.ui.ExperimentalComposeUiApi 4 | import androidx.compose.ui.draganddrop.DragAndDropEvent 5 | import androidx.compose.ui.draganddrop.DragAndDropTarget 6 | import androidx.compose.ui.draganddrop.awtTransferable 7 | import me.lkl.dalvikus.ui.addFileToTree 8 | import me.lkl.dalvikus.ui.snackbar.SnackbarManager 9 | import java.io.File 10 | import java.net.URI 11 | 12 | class TreeDragAndDropTarget(val snackbarManager: SnackbarManager, val unsupportedFileText: String) : DragAndDropTarget { 13 | @OptIn(ExperimentalComposeUiApi::class) 14 | override fun onDrop(event: DragAndDropEvent): Boolean { 15 | val flavors = event.awtTransferable.transferDataFlavors 16 | 17 | for (flavor in flavors) { 18 | if (flavor.isMimeTypeEqual("application/x-java-file-list")) { 19 | val files = event.awtTransferable.getTransferData(flavor) as? List<*> 20 | files?.filterIsInstance()?.forEach { file -> 21 | addFileToTree(file, snackbarManager, unsupportedFileText) 22 | } 23 | return true 24 | } 25 | 26 | if (flavor.isMimeTypeEqual("text/uri-list") || flavor.isMimeTypeEqual("text/plain")) { 27 | val data = event.awtTransferable.getTransferData(flavor) as? String ?: continue 28 | 29 | // Split into lines, ignore comments 30 | val uris = data.lines() 31 | .mapNotNull { line -> line.trim().takeIf { it.isNotBlank() && !it.startsWith("#") } } 32 | .mapNotNull { uri -> 33 | try { 34 | File(URI(uri)) 35 | } catch (e: Exception) { 36 | null 37 | } 38 | } 39 | 40 | uris.forEach { file -> 41 | if (file.exists()) { 42 | addFileToTree(file, snackbarManager, unsupportedFileText) 43 | } 44 | } 45 | 46 | return uris.isNotEmpty() 47 | } 48 | } 49 | 50 | return false 51 | } 52 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/packaging/PackagingViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.ui.packaging 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import me.lkl.dalvikus.dalvikusSettings 7 | import me.lkl.dalvikus.tools.AdbDeployer 8 | import me.lkl.dalvikus.tools.ApkSigner 9 | import me.lkl.dalvikus.tools.Keytool 10 | import me.lkl.dalvikus.ui.snackbar.SnackbarManager 11 | import java.io.File 12 | 13 | class PackagingViewModel(snackbarManager: SnackbarManager) { 14 | val adbDeployer = AdbDeployer(snackbarManager) 15 | val apkSigner = ApkSigner(snackbarManager) 16 | val keytool = Keytool(snackbarManager) 17 | } 18 | 19 | 20 | var keystorePasswordField by mutableStateOf(CharArray(0)) 21 | 22 | fun getKeystoreInfo(): KeystoreInfo { 23 | val keystoreFile = dalvikusSettings["keystore_file"] as File 24 | val keyAlias = dalvikusSettings["key_alias"] as String 25 | 26 | return KeystoreInfo( 27 | keystoreFile = keystoreFile, 28 | keyAlias = keyAlias, 29 | keystorePassword = keystorePasswordField 30 | ) 31 | } 32 | 33 | data class KeystoreInfo( 34 | val keystoreFile: File, 35 | val keyAlias: String, 36 | val keystorePassword: CharArray 37 | ) { 38 | fun seemsValid(): Boolean { 39 | return keystoreFile.exists() && keystoreFile.canRead() && 40 | keyAlias.isNotBlank() 41 | } 42 | 43 | fun isPasswordFilled(): Boolean { 44 | return keystorePassword.size >= 6 45 | } 46 | 47 | override fun equals(other: Any?): Boolean { 48 | if (this === other) return true 49 | if (javaClass != other?.javaClass) return false 50 | 51 | other as KeystoreInfo 52 | 53 | if (keystoreFile != other.keystoreFile) return false 54 | if (keyAlias != other.keyAlias) return false 55 | if (!keystorePassword.contentEquals(other.keystorePassword)) return false 56 | 57 | return true 58 | } 59 | 60 | override fun hashCode(): Int { 61 | var result = keystoreFile.hashCode() 62 | result = 31 * result + keyAlias.hashCode() 63 | result = 31 * result + keystorePassword.contentHashCode() 64 | return result 65 | } 66 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/backing/BackingFileNode.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree.backing 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | import me.lkl.dalvikus.tabs.CodeTab 5 | import me.lkl.dalvikus.tabs.TabElement 6 | import me.lkl.dalvikus.tree.ContainerNode 7 | import me.lkl.dalvikus.tree.FileNode 8 | import me.lkl.dalvikus.theme.getFileExtensionMeta 9 | import me.lkl.dalvikus.tree.Metadata 10 | import me.lkl.dalvikus.util.guessIfEditableTextually 11 | 12 | open class BackingFileNode( 13 | override val name: String, 14 | private val backing: Backing, 15 | override val parent: ContainerNode? 16 | ) : FileNode() { 17 | 18 | override val icon: ImageVector 19 | get() = getFileExtensionMeta(name).icon 20 | 21 | override suspend fun getContent(): ByteArray { 22 | return backing.read() 23 | } 24 | 25 | override suspend fun writeContent(newContent: ByteArray) { 26 | backing.write(newContent) 27 | parent?.notifyChanged() 28 | } 29 | 30 | override suspend fun createTab(): TabElement { 31 | return CodeTab( 32 | tabName = name, 33 | tabId = name + "@" + backing.hashCode(), 34 | tabIcon = icon, 35 | contentProvider = this 36 | ) 37 | } 38 | 39 | override fun getSizeEstimate(): Long { 40 | return backing.size() 41 | } 42 | 43 | override fun isDisplayable(): Boolean = isEditable() 44 | override fun isEditable(): Boolean { 45 | // For backing-based files, we'll assume they're editable if they pass the text check 46 | // This requires reading the content, which is not ideal, but necessary for the check 47 | return try { 48 | guessIfEditableTextually(backing.inputStream()) 49 | } catch (e: Exception) { 50 | false 51 | } 52 | } 53 | 54 | override fun getMetadata(): Set> { 55 | if (backing is FileBacking) { 56 | return setOf( 57 | Metadata.FILE_SIZE to backing.size(), 58 | Metadata.LAST_EDITED to backing.file.lastModified(), 59 | ) 60 | } else if (backing is ZipBacking) { 61 | return setOf( 62 | Metadata.FILE_SIZE to backing.size() 63 | ) 64 | } 65 | return emptySet() 66 | } 67 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/archive/ApkNode.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree.archive 2 | 3 | import brut.androlib.exceptions.AndrolibException 4 | import brut.androlib.meta.ApkInfo 5 | import brut.androlib.res.data.ResID 6 | import brut.androlib.res.data.ResResSpec 7 | import brut.androlib.res.data.ResTable 8 | import brut.directory.ExtFile 9 | import co.touchlab.kermit.Logger 10 | import me.lkl.dalvikus.tree.ContainerNode 11 | import me.lkl.dalvikus.tree.Node 12 | import me.lkl.dalvikus.tree.backing.Backing 13 | import me.lkl.dalvikus.util.getApkToolConfig 14 | import java.io.File 15 | 16 | class ApkNode( 17 | override val name: String, 18 | override val backing: Backing, 19 | override val parent: ContainerNode? 20 | ) : ZipNode(name, backing, parent) { 21 | 22 | private var _resTable: ResTable? = null 23 | private var apkFile: File? = null 24 | 25 | val resTable: ResTable 26 | get() = _resTable ?: throw IllegalStateException("Resources not loaded. Call loadChildrenInternal() first.") 27 | 28 | init { 29 | require(name.endsWith(".apk", ignoreCase = true)) 30 | } 31 | 32 | override suspend fun loadChildrenInternal(): List { 33 | apkFile = backing.getFileOrCreateTemp(".apk") 34 | 35 | // Initialize ResTable with the APK file 36 | _resTable = ResTable(ApkInfo(ExtFile(apkFile!!)), getApkToolConfig()) 37 | 38 | try { 39 | if(!resTable.isMainPackageLoaded && resTable.apkInfo.hasResources()) 40 | resTable.loadMainPackage() 41 | } catch (ale: AndrolibException) { 42 | Logger.e("Failed to load resources from APK: ${ale.message}", ale) 43 | } 44 | 45 | return super.loadChildrenInternal() 46 | } 47 | 48 | fun getAndroidPackageName(): String? { 49 | if (_resTable?.isMainPackageLoaded != true) return null 50 | return resTable.mainPackage.name 51 | } 52 | 53 | fun getResourceSpecs(): List? { 54 | if (_resTable?.isMainPackageLoaded != true) return null 55 | return resTable.mainPackage.listResSpecs() 56 | } 57 | 58 | fun getResourceById(unsignedValue: Int): ResResSpec? { 59 | if (_resTable?.isMainPackageLoaded != true) return null 60 | val resId = ResID(unsignedValue) 61 | val resSpec = resTable.mainPackage.getResSpec(resId) 62 | return resSpec 63 | } 64 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Dalvikus Cross-platform Release Build 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Trigger on tag push like v1.0.0 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build: 13 | name: Build on ${{ matrix.os }} 14 | timeout-minutes: 30 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest, macos-latest] 19 | 20 | steps: 21 | - name: Checkout repo 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up JDK 17 25 | uses: actions/setup-java@v4 26 | with: 27 | java-version: '17' 28 | distribution: temurin 29 | 30 | - name: Grant execute permission for Gradle wrapper 31 | run: chmod +x ./gradlew 32 | 33 | - name: Build native installer 34 | shell: bash 35 | run: | 36 | if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then 37 | ./gradlew composeApp:packageReleaseDeb --info --stacktrace 38 | elif [[ "${{ matrix.os }}" == "windows-latest" ]]; then 39 | ./gradlew composeApp:packageReleaseMsi composeApp:packageReleaseExe --info --stacktrace 40 | elif [[ "${{ matrix.os }}" == "macos-latest" ]]; then 41 | ./gradlew composeApp:packageReleaseDmg --info --stacktrace 42 | fi 43 | 44 | - name: Upload installer artifacts 45 | uses: actions/upload-artifact@v4 46 | with: 47 | name: dalvikus-${{ matrix.os }} 48 | path: | 49 | composeApp/build/compose/binaries/**/*.deb 50 | composeApp/build/compose/binaries/**/*.msi 51 | composeApp/build/compose/binaries/**/*.exe 52 | composeApp/build/compose/binaries/**/*.dmg 53 | 54 | release: 55 | name: Create GitHub Release 56 | runs-on: ubuntu-latest 57 | needs: build 58 | steps: 59 | - name: Checkout repo 60 | uses: actions/checkout@v4 61 | 62 | - name: Download all installer artifacts 63 | uses: actions/download-artifact@v4 64 | with: 65 | path: artifacts # All artifacts downloaded here 66 | 67 | - name: Create Release 68 | uses: softprops/action-gh-release@v2 69 | with: 70 | files: | 71 | artifacts/dalvikus-ubuntu-latest/**/*.deb 72 | artifacts/dalvikus-windows-latest/**/*.msi 73 | artifacts/dalvikus-windows-latest/**/*.exe 74 | artifacts/dalvikus-macos-latest/**/*.dmg 75 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/smali/ErrorHandlingSmaliParser.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.smali 2 | 3 | import com.android.tools.smali.smali.smaliParser 4 | import org.antlr.runtime.* 5 | 6 | class ErrorHandlingSmaliParser( 7 | tokens: CommonTokenStream 8 | ) : smaliParser(tokens) { 9 | 10 | val errorTokens = mutableMapOf() 11 | var errorLines: List = mutableListOf() 12 | 13 | override fun reportError(e: RecognitionException?) { 14 | try { 15 | // due to a bug in smali we have to try catch here. https://github.com/google/smali/issues/98 16 | super.reportError(e) 17 | } catch (ex: Exception) { 18 | // ignore. 19 | } 20 | val offending = e?.token 21 | if (offending is CommonToken) { 22 | errorTokens[offending] = getShortErrorMessage(e) 23 | if (offending.line !in errorLines) { 24 | errorLines += offending.line 25 | } 26 | } 27 | } 28 | 29 | fun getShortErrorMessage(e: RecognitionException): String { 30 | fun tokenName(id: Int): String = if (id == Token.EOF) "" else tokenNames[id] ?: "" 31 | val tokenText = e.token?.text ?: "" 32 | 33 | return when (e) { 34 | is UnwantedTokenException -> { 35 | val expected = tokenName(e.expecting) 36 | "Unexpected '$tokenText', expected $expected" 37 | } 38 | 39 | is MissingTokenException -> { 40 | val expected = tokenName(e.expecting) 41 | "Missing $expected before '$tokenText'" 42 | } 43 | 44 | is MismatchedTokenException -> { 45 | val expected = tokenName(e.expecting) 46 | "Expected $expected but found '$tokenText'" 47 | } 48 | 49 | is MismatchedTreeNodeException -> { 50 | val expected = tokenName(e.expecting) 51 | "Wrong tree node '${e.node}', expected $expected" 52 | } 53 | 54 | is NoViableAltException -> { 55 | "Cannot parse '$tokenText'" 56 | } 57 | 58 | is EarlyExitException -> { 59 | "Incomplete input after '$tokenText'" 60 | } 61 | 62 | is MismatchedSetException -> { 63 | "Unexpected '$tokenText'" 64 | } 65 | 66 | is FailedPredicateException -> { 67 | "Rule '${e.ruleName}' failed: ${e.predicateText}" 68 | } 69 | 70 | else -> e.message ?: "Unknown error at '$tokenText'" 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/snackbar/Snackbar.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.ui.snackbar 2 | 3 | import androidx.compose.material3.SnackbarDuration 4 | import androidx.compose.material3.SnackbarHostState 5 | import androidx.compose.material3.SnackbarResult 6 | import androidx.compose.ui.platform.ClipEntry 7 | import androidx.compose.ui.platform.Clipboard 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.launch 10 | import me.lkl.dalvikus.errorreport.crtExHandler 11 | import me.lkl.dalvikus.util.toOneLiner 12 | import java.awt.datatransfer.StringSelection 13 | import java.io.PrintWriter 14 | import java.io.StringWriter 15 | 16 | class SnackbarManager( 17 | val snackbarHostState: SnackbarHostState, 18 | private val clipboardManager: Clipboard, 19 | private val coroutineScope: CoroutineScope, 20 | private val snackbarResources: SnackbarResources 21 | ) { 22 | 23 | fun showError(throwable: Throwable) { 24 | coroutineScope.launch(crtExHandler) { 25 | val message = snackbarResources.snackFailed.format(throwable.toOneLiner()) 26 | val result = snackbarHostState.showSnackbar( 27 | message = message, 28 | actionLabel = snackbarResources.copy, 29 | duration = SnackbarDuration.Long, 30 | withDismissAction = true 31 | ) 32 | if (result == SnackbarResult.ActionPerformed) { 33 | val fullStackTrace = StringWriter().also { throwable.printStackTrace(PrintWriter(it)) }.toString() 34 | clipboardManager.setClipEntry(ClipEntry(StringSelection(fullStackTrace))) 35 | } 36 | } 37 | } 38 | 39 | fun showMessage(message: String) { 40 | coroutineScope.launch(crtExHandler) { 41 | snackbarHostState.showSnackbar( 42 | message = message, 43 | duration = SnackbarDuration.Short, 44 | withDismissAction = true 45 | ) 46 | } 47 | } 48 | 49 | fun showSuccess() = showMessage(snackbarResources.snackSuccess) 50 | fun showAssembleError(errorLines: List) { 51 | coroutineScope.launch(crtExHandler) { 52 | val message = snackbarResources.snackAssembleError.format(errorLines.joinToString(", ")) 53 | snackbarHostState.showSnackbar( 54 | message = message, 55 | duration = SnackbarDuration.Short, 56 | withDismissAction = true 57 | ) 58 | } 59 | } 60 | } 61 | 62 | data class SnackbarResources( 63 | val copy: String, 64 | val snackFailed: String, 65 | val snackSuccess: String, 66 | val snackAssembleError: String, 67 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/editor/LineNumbers.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.ui.editor 2 | 3 | import androidx.compose.foundation.ScrollState 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.text.selection.DisableSelection 6 | import androidx.compose.foundation.verticalScroll 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.alpha 13 | import androidx.compose.ui.platform.LocalDensity 14 | import androidx.compose.ui.text.TextLayoutResult 15 | import androidx.compose.ui.text.TextStyle 16 | import androidx.compose.ui.unit.dp 17 | 18 | 19 | @Composable 20 | fun LineNumberColumn( 21 | code: TextLayoutResult?, 22 | scrollState: ScrollState, 23 | textContentPadding: PaddingValues, 24 | textStyle: TextStyle, 25 | ) { 26 | // wait until the code is loaded 27 | if(code == null) return 28 | val lines = code.lineCount 29 | val lineHeightDp = with(LocalDensity.current) { 30 | textStyle.lineHeight.toDp() 31 | } 32 | val maxNumDigits = lines.toString().length 33 | 34 | // TODO we can do this lazily too. 35 | DisableSelection { 36 | Column( 37 | Modifier 38 | .verticalScroll(scrollState) 39 | .padding(top = textContentPadding.calculateTopPadding(), 40 | bottom = textContentPadding.calculateBottomPadding(), 41 | start = 8.dp, 42 | end = 8.dp) 43 | .width(IntrinsicSize.Min) 44 | ) { 45 | repeat(lines) { i -> 46 | 47 | Box(modifier = Modifier.height(lineHeightDp)) { 48 | // to account for the width of the line numbers 49 | LineNumber( 50 | number = "9".repeat(maxNumDigits), 51 | modifier = Modifier.alpha(0f), 52 | textStyle = textStyle 53 | ) 54 | LineNumber( 55 | number = "${i + 1}", 56 | modifier = Modifier.align(Alignment.CenterEnd), 57 | textStyle = textStyle, 58 | ) 59 | } 60 | 61 | } 62 | } 63 | } 64 | } 65 | 66 | @Composable 67 | private fun LineNumber(number: String, modifier: Modifier, textStyle: TextStyle) { 68 | Text( 69 | text = number, 70 | style = textStyle, 71 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f), 72 | modifier = modifier 73 | ) 74 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tabs/SmaliTab.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tabs 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.MutableState 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import io.github.composegears.valkyrie.DeployedCode 9 | import me.lkl.dalvikus.dalvikusSettings 10 | import me.lkl.dalvikus.decompiler.CFRDecompiler 11 | import me.lkl.dalvikus.decompiler.Decompiler 12 | import me.lkl.dalvikus.decompiler.DecompilerContentProvider 13 | import me.lkl.dalvikus.decompiler.JADXDecompiler 14 | import me.lkl.dalvikus.decompiler.VineflowerDecompiler 15 | import me.lkl.dalvikus.selectedNavItem 16 | import me.lkl.dalvikus.tabs.contentprovider.DualContentProvider 17 | import me.lkl.dalvikus.tree.dex.DexEntryClassNode 18 | import me.lkl.dalvikus.ui.editor.EditorViewModel 19 | 20 | class SmaliTab( 21 | override val tabId: String, 22 | val dexEntryClassNode: DexEntryClassNode 23 | ) : TabElement { 24 | override var hasUnsavedChanges: MutableState = mutableStateOf(false) 25 | override val contentProvider: DualContentProvider = object : DualContentProvider(dexEntryClassNode, 26 | DecompilerContentProvider(dexEntryClassNode) { getDecompiler() } 27 | ) { 28 | override var firstContentProvider = true 29 | get() = selectedNavItem != "Decompiler" 30 | } 31 | 32 | private fun getDecompiler(): Decompiler { 33 | return when(dalvikusSettings["decompiler_implementation"] as String) { 34 | "jadx" -> JADXDecompiler() 35 | "cfr" -> CFRDecompiler() 36 | "vineflower" -> VineflowerDecompiler() 37 | // TODO implement LLM decompiler https://huggingface.co/collections/MoxStone/smalillm-68550b87817dfb046f790cdf 38 | else -> throw IllegalArgumentException("Selected decompiler not implemented yet") 39 | } 40 | } 41 | 42 | @Composable 43 | override fun tabName(): String = dexEntryClassNode.getClassDef().type.removeSurrounding("L", ";").substringAfterLast('/') 44 | override val tabIcon: ImageVector = Icons.Filled.DeployedCode 45 | 46 | var smaliEditorViewModel : EditorViewModel? = null 47 | var decompiledEditorViewModel : EditorViewModel? = null 48 | 49 | override var editorViewModel: EditorViewModel? 50 | get() = if (selectedNavItem == "Decompiler") decompiledEditorViewModel else smaliEditorViewModel 51 | set(value) { 52 | if (selectedNavItem == "Decompiler") { 53 | decompiledEditorViewModel = value 54 | } else { 55 | smaliEditorViewModel = value 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | kotlin = "2.2.0" 4 | compose = "1.9.1" 5 | hotReload = "1.0.0-alpha09" 6 | updates = "0.52.0" 7 | kermit = "2.0.6" 8 | multiplatformSettings = "1.3.0" 9 | materialKolor = "3.0.1" 10 | kotlinxCoroutinesSwing = "1.10.2" 11 | smali = "3.0.9" 12 | jadx = "1.5.2" 13 | antlr4 = "4.13.2" 14 | apksig = "8.13.0-alpha04" 15 | ddmlib = "31.13.0-alpha04" 16 | cfr = "0.152" 17 | dex2jarNicoMexis = "2.4.32" 18 | vineflower = "1.11.1" 19 | apktoolLib = "2.12.0" 20 | haze = "1.6.9" 21 | 22 | [libraries] 23 | 24 | kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } 25 | multiplatformSettings = { module = "com.russhwolf:multiplatform-settings-no-arg", version.ref = "multiplatformSettings" } 26 | materialKolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" } 27 | splitPaneDesktop = { group = "org.jetbrains.compose.components", name = "components-splitpane-desktop", version.ref = "compose" } 28 | kotlinxCoroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutinesSwing" } 29 | smali = { module = "com.android.tools.smali:smali", version.ref = "smali" } 30 | smaliDexlib2 = { module = "com.android.tools.smali:smali-dexlib2", version.ref = "smali" } 31 | smaliBaksmali = { module = "com.android.tools.smali:smali-baksmali", version.ref = "smali" } 32 | smaliUtil = { module = "com.android.tools.smali:smali-util", version.ref = "smali" } 33 | jadx = { module = "io.github.skylot:jadx-core", version.ref = "jadx" } 34 | jadxSmaliInput = { module = "io.github.skylot:jadx-smali-input", version.ref = "jadx" } 35 | antlr4 = { module = "org.antlr:antlr4", version.ref = "antlr4" } 36 | apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" } 37 | ddmlib = { module = "com.android.tools.ddms:ddmlib", version.ref = "ddmlib" } 38 | cfr = { module = "org.benf:cfr", version.ref = "cfr" } 39 | dex2jarNicoMexis = { module = "de.femtopedia.dex2jar:dex2jar", version.ref = "dex2jarNicoMexis" } 40 | vineflower = { module = "org.vineflower:vineflower", version.ref = "vineflower" } 41 | apktoolLib = { module = "org.apktool:apktool-lib", version.ref = "apktoolLib" } 42 | haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } 43 | hazeMaterials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } 44 | 45 | [plugins] 46 | 47 | multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 48 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 49 | compose = { id = "org.jetbrains.compose", version.ref = "compose" } 50 | hotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "hotReload" } 51 | updates = { id = "com.github.ben-manes.versions", version.ref = "updates" } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/archive/ApkEntryXmlNode.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree.archive 2 | 3 | import brut.androlib.res.decoder.AXmlResourceParser 4 | import brut.androlib.res.decoder.AndroidManifestPullStreamDecoder 5 | import brut.androlib.res.decoder.AndroidManifestResourceParser 6 | import brut.androlib.res.decoder.ResStreamDecoder 7 | import brut.androlib.res.decoder.ResXmlPullStreamDecoder 8 | import com.android.apksig.internal.apk.AndroidBinXmlParser 9 | import me.lkl.dalvikus.tree.ContainerNode 10 | import me.lkl.dalvikus.util.guessIfEditableTextually 11 | import me.lkl.dalvikus.util.newXmlSerializer 12 | import org.xmlpull.v1.XmlSerializer 13 | import java.io.ByteArrayOutputStream 14 | import kotlin.properties.Delegates 15 | 16 | 17 | class ApkEntryXmlNode( 18 | override val name: String, 19 | private val fullPath: String, 20 | private val apkRoot: ApkNode, 21 | override val parent: ContainerNode? 22 | ) : ZipEntryFileNode(name, fullPath, apkRoot, parent) { 23 | 24 | private var isManifest by Delegates.notNull() 25 | private var isPlaintext by Delegates.notNull() 26 | 27 | init { 28 | require(name.endsWith(".xml", ignoreCase = true)) { "ZipEntryXmlNode must be initialized with a .xml file" } 29 | isManifest = fullPath == "AndroidManifest.xml" 30 | isPlaintext = !isManifest && guessIfEditableTextually(apkRoot.readEntry(fullPath).inputStream()) 31 | } 32 | 33 | override suspend fun getContent(): ByteArray { 34 | val readEntry = apkRoot.readEntry(fullPath) 35 | if (isPlaintext) return readEntry 36 | 37 | val parser = if (isManifest) { 38 | AndroidManifestResourceParser(apkRoot.resTable) 39 | } else { 40 | AXmlResourceParser(apkRoot.resTable) 41 | } 42 | 43 | val decoder: ResStreamDecoder = if (isManifest) { 44 | AndroidManifestPullStreamDecoder(parser, newXmlSerializer()) 45 | } else { 46 | ResXmlPullStreamDecoder(parser, newXmlSerializer()) 47 | } 48 | 49 | return ByteArrayOutputStream().use { output -> 50 | decoder.decode(readEntry.inputStream(), output) 51 | output.toByteArray() 52 | } 53 | } 54 | 55 | override suspend fun writeContent(newContent: ByteArray) { 56 | if(!isPlaintext) { 57 | throw IllegalStateException("Cannot write content to non-plaintext XML entry: $fullPath") 58 | } 59 | // TODO currently I haven't found a good way to write XML back to binary format. 60 | apkRoot.updateEntry(fullPath, newContent) 61 | parent?.notifyChanged() 62 | } 63 | 64 | override fun isDisplayable(): Boolean = true 65 | override fun isEditable(): Boolean = isPlaintext 66 | } 67 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/icons/DeployedCode.kt: -------------------------------------------------------------------------------- 1 | package io.github.composegears.valkyrie 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | import androidx.compose.ui.graphics.vector.path 8 | import androidx.compose.ui.unit.dp 9 | 10 | val Icons.Filled.DeployedCode: ImageVector 11 | get() { 12 | if (_DeployedCode != null) { 13 | return _DeployedCode!! 14 | } 15 | _DeployedCode = ImageVector.Builder( 16 | name = "Filled.DeployedCode", 17 | defaultWidth = 24.dp, 18 | defaultHeight = 24.dp, 19 | viewportWidth = 960f, 20 | viewportHeight = 960f 21 | ).apply { 22 | path(fill = SolidColor(Color(0xFFE8EAED))) { 23 | moveTo(440f, 777f) 24 | verticalLineToRelative(-274f) 25 | lineTo(200f, 364f) 26 | verticalLineToRelative(274f) 27 | lineToRelative(240f, 139f) 28 | close() 29 | moveTo(520f, 777f) 30 | lineTo(760f, 638f) 31 | verticalLineToRelative(-274f) 32 | lineTo(520f, 503f) 33 | verticalLineToRelative(274f) 34 | close() 35 | moveTo(480f, 434f) 36 | lineTo(717f, 297f) 37 | lineTo(480f, 160f) 38 | lineTo(243f, 297f) 39 | lineTo(480f, 434f) 40 | close() 41 | moveTo(160f, 708f) 42 | quadToRelative(-19f, -11f, -29.5f, -29f) 43 | reflectiveQuadTo(120f, 639f) 44 | verticalLineToRelative(-318f) 45 | quadToRelative(0f, -22f, 10.5f, -40f) 46 | reflectiveQuadToRelative(29.5f, -29f) 47 | lineToRelative(280f, -161f) 48 | quadToRelative(19f, -11f, 40f, -11f) 49 | reflectiveQuadToRelative(40f, 11f) 50 | lineToRelative(280f, 161f) 51 | quadToRelative(19f, 11f, 29.5f, 29f) 52 | reflectiveQuadToRelative(10.5f, 40f) 53 | verticalLineToRelative(318f) 54 | quadToRelative(0f, 22f, -10.5f, 40f) 55 | reflectiveQuadTo(800f, 708f) 56 | lineTo(520f, 869f) 57 | quadToRelative(-19f, 11f, -40f, 11f) 58 | reflectiveQuadToRelative(-40f, -11f) 59 | lineTo(160f, 708f) 60 | close() 61 | moveTo(480f, 480f) 62 | close() 63 | } 64 | }.build() 65 | 66 | return _DeployedCode!! 67 | } 68 | 69 | @Suppress("ObjectPropertyName") 70 | private var _DeployedCode: ImageVector? = null 71 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/theme/Typography.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontStyle 7 | import androidx.compose.ui.text.font.FontWeight 8 | import dalvikus.composeapp.generated.resources.* 9 | import org.jetbrains.compose.resources.Font 10 | 11 | @Composable 12 | fun Monaspace() = FontFamily( 13 | Font(Res.font.MonaspaceNeonFrozen_Bold, FontWeight.Bold, FontStyle.Normal), 14 | Font(Res.font.MonaspaceNeonFrozen_BoldItalic, FontWeight.Bold, FontStyle.Italic), 15 | Font(Res.font.MonaspaceNeonFrozen_ExtraBold, FontWeight.ExtraBold, FontStyle.Normal), 16 | Font(Res.font.MonaspaceNeonFrozen_ExtraBoldItalic, FontWeight.ExtraBold, FontStyle.Italic), 17 | Font(Res.font.MonaspaceNeonFrozen_ExtraLight, FontWeight.ExtraLight, FontStyle.Normal), 18 | Font(Res.font.MonaspaceNeonFrozen_ExtraLightItalic, FontWeight.ExtraLight, FontStyle.Italic), 19 | Font(Res.font.MonaspaceNeonFrozen_Italic, FontWeight.Normal, FontStyle.Italic), 20 | Font(Res.font.MonaspaceNeonFrozen_Light, FontWeight.Light, FontStyle.Normal), 21 | Font(Res.font.MonaspaceNeonFrozen_LightItalic, FontWeight.Light, FontStyle.Italic), 22 | Font(Res.font.MonaspaceNeonFrozen_Medium, FontWeight.Medium, FontStyle.Normal), 23 | Font(Res.font.MonaspaceNeonFrozen_MediumItalic, FontWeight.Medium, FontStyle.Italic), 24 | Font(Res.font.MonaspaceNeonFrozen_Regular, FontWeight.Normal, FontStyle.Normal), 25 | Font(Res.font.MonaspaceNeonFrozen_SemiBold, FontWeight.SemiBold, FontStyle.Normal), 26 | Font(Res.font.MonaspaceNeonFrozen_SemiBoldItalic, FontWeight.SemiBold, FontStyle.Italic), 27 | ) 28 | 29 | fun Typography.withFontFamily(fontFamily: FontFamily): Typography { 30 | return Typography( 31 | displayLarge = displayLarge.copy(fontFamily = fontFamily), 32 | displayMedium = displayMedium.copy(fontFamily = fontFamily), 33 | displaySmall = displaySmall.copy(fontFamily = fontFamily), 34 | headlineLarge = headlineLarge.copy(fontFamily = fontFamily), 35 | headlineMedium = headlineMedium.copy(fontFamily = fontFamily), 36 | headlineSmall = headlineSmall.copy(fontFamily = fontFamily), 37 | titleLarge = titleLarge.copy(fontFamily = fontFamily), 38 | titleMedium = titleMedium.copy(fontFamily = fontFamily), 39 | titleSmall = titleSmall.copy(fontFamily = fontFamily), 40 | bodyLarge = bodyLarge.copy(fontFamily = fontFamily), 41 | bodyMedium = bodyMedium.copy(fontFamily = fontFamily), 42 | bodySmall = bodySmall.copy(fontFamily = fontFamily), 43 | labelLarge = labelLarge.copy(fontFamily = fontFamily), 44 | labelMedium = labelMedium.copy(fontFamily = fontFamily), 45 | labelSmall = labelSmall.copy(fontFamily = fontFamily) 46 | ) 47 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/decompiler/JavaDecompiler.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.decompiler 2 | 3 | import com.android.tools.smali.dexlib2.iface.ClassDef 4 | import com.android.tools.smali.dexlib2.writer.io.MemoryDataStore 5 | import com.android.tools.smali.dexlib2.writer.pool.DexPool 6 | import com.googlecode.d2j.dex.Dex2jar 7 | import com.googlecode.d2j.reader.MultiDexFileReader 8 | import com.googlecode.dex2jar.tools.BaksmaliBaseDexExceptionHandler 9 | import me.lkl.dalvikus.dalvikusSettings 10 | import me.lkl.dalvikus.smali.SoloDexFileWrapper 11 | import java.io.File 12 | import java.util.jar.JarFile 13 | 14 | interface JavaDecompiler : Decompiler { 15 | 16 | override suspend fun decompile(classDef: ClassDef): String { 17 | try { 18 | val verbose = dalvikusSettings["decompiler_verbose"] as Boolean 19 | // Build a tiny .dex containing only this one ClassDef 20 | val tempDex = SoloDexFileWrapper(classDef) 21 | val dataStore = MemoryDataStore().also { 22 | DexPool.writeTo(it, tempDex) 23 | } 24 | 25 | val jarFile = File.createTempFile("dalvikus", ".jar").apply { 26 | deleteOnExit() 27 | } 28 | 29 | val reader = MultiDexFileReader.open(dataStore.data) 30 | val handler = BaksmaliBaseDexExceptionHandler() 31 | Dex2jar.from(reader) 32 | .withExceptionHandler(handler) 33 | .topoLogicalSort() 34 | .skipDebug(!verbose) 35 | .to(jarFile.toPath()) 36 | 37 | if (handler.hasException()) { 38 | val tempErrorFile = File.createTempFile("dalvikus_error", ".txt").apply { 39 | handler.dump(toPath(), arrayOf()) 40 | deleteOnExit() 41 | } 42 | val tempErrorText = tempErrorFile.readText() 43 | return "Error during dex2jar conversion: $tempErrorText\n" 44 | } 45 | 46 | val internalName = classDef.type.substring(1, classDef.type.length - 1) 47 | val classPath = "$internalName.class" 48 | val bytes = JarFile(jarFile).use { jar -> 49 | jar.entries() 50 | .toList() 51 | .firstOrNull { it.name == classPath } 52 | ?.let { entry -> 53 | jar.getInputStream(entry).use { inputStream -> 54 | inputStream.readBytes() 55 | } 56 | } 57 | ?: return "Error: could not find $classPath inside dex2jar output" 58 | } 59 | 60 | return decompileJava(internalName, bytes) 61 | } catch (e: Exception) { 62 | return "Decompilation error: ${e.message}" 63 | } 64 | } 65 | 66 | suspend fun decompileJava(internalName: String, bytes: ByteArray): String 67 | } 68 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tools/Keytool.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tools 2 | 3 | import co.touchlab.kermit.Logger 4 | import me.lkl.dalvikus.dalvikusSettings 5 | import me.lkl.dalvikus.ui.snackbar.SnackbarManager 6 | import java.io.File 7 | 8 | class Keytool(val snackbarManager: SnackbarManager) { 9 | fun createKeystore( 10 | keystorePassword: CharArray, 11 | distinguishedName: String = "CN=dalvikus" 12 | ) { 13 | if (!isKeytoolAvailable()) { 14 | snackbarManager.showMessage("Keytool is not installed or not found in PATH.") 15 | return 16 | } 17 | 18 | val keystoreFile = dalvikusSettings["keystore_file"] as? File 19 | ?: run { 20 | snackbarManager.showMessage("Keystore file not configured.") 21 | return 22 | } 23 | 24 | val keyAlias = dalvikusSettings["key_alias"] as? String 25 | ?: run { 26 | snackbarManager.showMessage("Key alias not configured.") 27 | return 28 | } 29 | 30 | val command = listOf( 31 | "keytool", "-genkeypair", 32 | "-v", 33 | "-keystore", keystoreFile.absolutePath, 34 | "-keyalg", "RSA", 35 | "-keysize", "2048", 36 | "-validity", "10000", 37 | "-alias", keyAlias, 38 | "-storetype", "PKCS12", 39 | "-storepass", keystorePassword.concatToString(), 40 | "-dname", distinguishedName 41 | ) 42 | 43 | runCommand(command) 44 | } 45 | 46 | private fun runCommand(command: List) { 47 | try { 48 | val process = ProcessBuilder(command) 49 | .redirectErrorStream(true) 50 | .start() 51 | 52 | val output = process.inputStream.bufferedReader().use { it.readText() } 53 | val exitCode = process.waitFor() 54 | 55 | Logger.i("Keytool output:\n$output") 56 | Logger.i("Keytool process exited with code $exitCode") 57 | 58 | val finalOutput = if (exitCode == 2) { 59 | "$output\nPlease make sure keytool is installed." 60 | } else output 61 | 62 | snackbarManager.showMessage(finalOutput) 63 | } catch (e: Exception) { 64 | Logger.e("Failed to run keytool", e) 65 | snackbarManager.showError(e) 66 | } 67 | } 68 | 69 | private fun isKeytoolAvailable(): Boolean { 70 | return try { 71 | val process = ProcessBuilder("keytool", "--help") 72 | .redirectErrorStream(true) 73 | .start() 74 | 75 | process.inputStream.bufferedReader().use { it.readText() } 76 | val exitCode = process.waitFor() 77 | 78 | exitCode == 0 79 | } catch (e: Exception) { 80 | Logger.e("Keytool is not installed or not found in PATH", e) 81 | false 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /composeApp/src/jvmMain/kotlin/me/lkl/dalvikus/lexer/XmlLexerHighlight.jvm.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.lexer 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | import androidx.compose.ui.text.SpanStyle 5 | import androidx.compose.ui.text.buildAnnotatedString 6 | import androidx.compose.ui.text.font.FontStyle 7 | import androidx.compose.ui.text.font.FontWeight 8 | import org.antlr.v4.runtime.CharStreams 9 | import org.antlr.v4.runtime.CommonTokenStream 10 | import me.lkl.dalvikus.theme.CodeHighlightColors 11 | 12 | actual fun highlightXmlCode(code: String, colors: CodeHighlightColors): AnnotatedString { 13 | val lexer = XMLLexer(CharStreams.fromString(code)) 14 | val tokens = CommonTokenStream(lexer) 15 | tokens.fill() 16 | 17 | return buildAnnotatedString { 18 | append(code) 19 | 20 | for (token in tokens.tokens) { 21 | if (token.type == -1 || token.text.isNullOrBlank()) continue 22 | 23 | getXmlTokenStyle(token.type, colors)?.let { 24 | addStyle(it, token.startIndex, token.stopIndex + 1) 25 | } 26 | } 27 | } 28 | } 29 | 30 | private fun getXmlTokenStyle(tokenType: Int, colors: CodeHighlightColors): SpanStyle? { 31 | return when (tokenType) { 32 | // === Comments === 33 | XMLLexer.COMMENT -> SpanStyle( 34 | color = colors.onSurface.copy(alpha = 0.5f), 35 | fontStyle = FontStyle.Italic 36 | ) 37 | 38 | // === CDATA & DTD === 39 | XMLLexer.CDATA, 40 | XMLLexer.DTD -> SpanStyle(color = colors.secondary) 41 | 42 | // === Entity and Character references === 43 | XMLLexer.EntityRef, 44 | XMLLexer.CharRef -> SpanStyle(color = colors.tertiary) 45 | 46 | // === Opening & closing brackets and delimiters === 47 | XMLLexer.OPEN, // < 48 | XMLLexer.CLOSE, // > 49 | XMLLexer.SPECIAL_CLOSE, // ?> 50 | XMLLexer.SLASH_CLOSE, // /> 51 | XMLLexer.SLASH, // / 52 | XMLLexer.EQUALS // = 53 | -> SpanStyle(color = colors.onSurface.copy(alpha = 0.6f)) 54 | 55 | // === Strings (attribute values) === 56 | XMLLexer.STRING -> SpanStyle(color = colors.primary) 57 | 58 | // === Tag/Attribute Names === 59 | XMLLexer.Name -> SpanStyle( 60 | color = colors.quaternary, 61 | fontWeight = FontWeight.Medium 62 | ) 63 | 64 | // === XML declaration open: SpanStyle( 66 | color = colors.quinary, 67 | fontWeight = FontWeight.Bold 68 | ) 69 | 70 | // === Processing Instructions === 71 | XMLLexer.PI -> SpanStyle( 72 | color = colors.senary, 73 | fontStyle = FontStyle.Italic 74 | ) 75 | 76 | // === Text Content === 77 | XMLLexer.TEXT -> SpanStyle(color = colors.onSurface) 78 | 79 | // === Whitespace (ignored) === 80 | XMLLexer.SEA_WS, 81 | XMLLexer.S -> null 82 | 83 | else -> null 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/dex/DexFileNode.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree.dex 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Adb 5 | import com.android.tools.smali.dexlib2.Opcodes 6 | import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile 7 | import com.android.tools.smali.dexlib2.iface.ClassDef 8 | import com.android.tools.smali.dexlib2.writer.io.MemoryDataStore 9 | import com.android.tools.smali.dexlib2.writer.pool.DexPool 10 | import me.lkl.dalvikus.dalvikusSettings 11 | import me.lkl.dalvikus.tree.ContainerNode 12 | import me.lkl.dalvikus.tree.Node 13 | import me.lkl.dalvikus.tree.backing.Backing 14 | import me.lkl.dalvikus.tree.buildChildNodes 15 | 16 | class DexFileNode( 17 | override val name: String, 18 | private val backing: Backing, 19 | override val parent: ContainerNode? 20 | ) : ContainerNode() { 21 | 22 | override val icon = Icons.Filled.Adb 23 | override val changesWithChildren = true 24 | 25 | val entries = mutableMapOf() // Key: com/example/MyClass 26 | 27 | override suspend fun loadChildrenInternal(): List { 28 | val input = backing.read().inputStream().buffered() 29 | val dexFile = DexBackedDexFile.fromInputStream( 30 | Opcodes.forApi(dalvikusSettings["api_level"] as Int), 31 | input 32 | ) 33 | 34 | entries.clear() 35 | dexFile.classes.forEach { classDef -> 36 | val path = classDef.type.removePrefix("L").removeSuffix(";") // com/example/Foo 37 | entries[path] = classDef 38 | } 39 | 40 | return buildChildNodes( 41 | entries = entries, 42 | prefix = "", 43 | onFolder = { name, path -> 44 | DexEntryPackageNode(name, path, this, this) 45 | }, 46 | onFile = { name, path, classDef -> 47 | DexEntryClassNode(name, path, this, this) 48 | } 49 | ) 50 | 51 | } 52 | 53 | override suspend fun rebuild() { 54 | val newBytes: ByteArray = rebuildDex() 55 | backing.write(newBytes) 56 | } 57 | 58 | private fun rebuildDex(): ByteArray { 59 | val apiLevel = dalvikusSettings["api_level"] as Int 60 | val dexPool = DexPool(Opcodes.forApi(apiLevel)) 61 | val memoryDataStore = MemoryDataStore(524288) 62 | 63 | entries.values.forEach { classDef -> 64 | dexPool.internClass(classDef) 65 | } 66 | 67 | dexPool.writeTo(memoryDataStore) 68 | return memoryDataStore.data 69 | } 70 | 71 | fun readEntry(path: String): ClassDef { 72 | return entries[path] ?: error("Class not found: $path") 73 | } 74 | 75 | suspend fun updateEntry(path: String, newClassDef: ClassDef) { 76 | entries[path] = newClassDef 77 | rebuild() 78 | // to ensure e.g. class name changes are reflected in the UI. 79 | loadChildren() 80 | } 81 | 82 | fun hasClass(item: String): Boolean { 83 | return entries.containsKey(item) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # dalvikus [![GitHub release](https://img.shields.io/github/v/release/loerting/dalvikus?style=flat)](https://github.com/loerting/dalvikus/releases) [![Downloads](https://img.shields.io/github/downloads/loerting/dalvikus/total)](https://github.com/loerting/dalvikus/releases) [![Kotlin](https://img.shields.io/badge/Kotlin-Compose%20Multiplatform-7F52FF?style=flat&logo=kotlin)](https://www.jetbrains.com/compose-multiplatform/) 6 | 7 | **Dalvikus** is a modern Android reverse engineering and modification toolkit built with [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/). 8 | Designed for developers and researchers who want to inspect, edit, and rebuild Android apps seamlessly. 9 | 10 | > [!NOTE] 11 | > With great power comes great responsibility. Dalvikus is solely intended for ethical purposes. 12 | 13 | ## Screenshots 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |

Smali editor with code completion.

Comfortable dark theme and understandable errors.

Seamless multi decompiler integration.

Sign and deploy edited applications directly.

Browse app resources.

Search in many different ways.
29 | 30 | *Note: These might be subject to change as the project evolves.* 31 | 32 | ## Features 33 | 34 | - Open APK and DEX files, allowing direct editing of DEX files inside APKs **without unpacking manually** 35 | - Rich and comfortable smali language editor with **syntax highlighting** 36 | - Assist popup for **code completion** while typing 37 | - Light and dark themes for comfortable editing 38 | - Support for **multiple decompilers** to analyze dalvik code 39 | - Integrated app signing using **apksig** and **zipalign** for re-signing modified APKs 40 | - Built-in **ADB runner** to deploy and start apps on connected devices directly 41 | - Powerful search tools: **tree view, string constants, method and field references** 42 | - Browse resource IDs, XML files, and more using integrated **apktool** 43 | - Multiple languages: English, German, Chinese and Hindi 44 | 45 | ## Getting Started 46 | 47 | ### Requirements 48 | 49 | - JDK 11 or higher 50 | - Android SDK (build-tools and platform-tools, ADB and APK signing features) 51 | - Supported OS: Windows, Linux, macOS 52 | 53 | ### Installation 54 | 55 | Download the latest release from [Releases](https://github.com/loerting/dalvikus/releases) and follow the instructions for your platform. 56 | 57 | Or build from source: 58 | 59 | ```bash 60 | git clone https://github.com/loerting/dalvikus.git 61 | cd dalvikus 62 | ./gradlew :composeApp:run 63 | ``` 64 | 65 | ## License 66 | 67 | This project is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details. -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/icons/MatchCase.kt: -------------------------------------------------------------------------------- 1 | package io.github.composegears.valkyrie 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | import androidx.compose.ui.graphics.vector.path 8 | import androidx.compose.ui.unit.dp 9 | 10 | val Icons.Filled.MatchCase: ImageVector 11 | get() { 12 | if (_MatchCase != null) { 13 | return _MatchCase!! 14 | } 15 | _MatchCase = ImageVector.Builder( 16 | name = "Filled.MatchCase", 17 | defaultWidth = 24.dp, 18 | defaultHeight = 24.dp, 19 | viewportWidth = 960f, 20 | viewportHeight = 960f 21 | ).apply { 22 | path(fill = SolidColor(Color(0xFFE8EAED))) { 23 | moveToRelative(131f, 708f) 24 | lineToRelative(165f, -440f) 25 | horizontalLineToRelative(79f) 26 | lineToRelative(165f, 440f) 27 | horizontalLineToRelative(-76f) 28 | lineToRelative(-39f, -112f) 29 | lineTo(247f, 596f) 30 | lineToRelative(-40f, 112f) 31 | horizontalLineToRelative(-76f) 32 | close() 33 | moveTo(270f, 532f) 34 | horizontalLineToRelative(131f) 35 | lineToRelative(-64f, -182f) 36 | horizontalLineToRelative(-4f) 37 | lineToRelative(-63f, 182f) 38 | close() 39 | moveTo(665f, 718f) 40 | quadToRelative(-51f, 0f, -81f, -27.5f) 41 | reflectiveQuadTo(554f, 618f) 42 | quadToRelative(0f, -44f, 34.5f, -72.5f) 43 | reflectiveQuadTo(677f, 517f) 44 | quadToRelative(23f, 0f, 45f, 4f) 45 | reflectiveQuadToRelative(38f, 11f) 46 | verticalLineToRelative(-12f) 47 | quadToRelative(0f, -29f, -20.5f, -47f) 48 | reflectiveQuadTo(685f, 455f) 49 | quadToRelative(-23f, 0f, -42f, 9.5f) 50 | reflectiveQuadTo(610f, 492f) 51 | lineToRelative(-47f, -35f) 52 | quadToRelative(24f, -29f, 54.5f, -43f) 53 | reflectiveQuadToRelative(68.5f, -14f) 54 | quadToRelative(69f, 0f, 103f, 32.5f) 55 | reflectiveQuadToRelative(34f, 97.5f) 56 | verticalLineToRelative(178f) 57 | horizontalLineToRelative(-63f) 58 | verticalLineToRelative(-37f) 59 | horizontalLineToRelative(-4f) 60 | quadToRelative(-14f, 23f, -38f, 35f) 61 | reflectiveQuadToRelative(-53f, 12f) 62 | close() 63 | moveTo(677f, 664f) 64 | quadToRelative(35f, 0f, 59.5f, -24f) 65 | reflectiveQuadToRelative(24.5f, -56f) 66 | quadToRelative(-14f, -8f, -33.5f, -12.5f) 67 | reflectiveQuadTo(689f, 567f) 68 | quadToRelative(-32f, 0f, -50f, 14f) 69 | reflectiveQuadToRelative(-18f, 37f) 70 | quadToRelative(0f, 20f, 16f, 33f) 71 | reflectiveQuadToRelative(40f, 13f) 72 | close() 73 | } 74 | }.build() 75 | 76 | return _MatchCase!! 77 | } 78 | 79 | @Suppress("ObjectPropertyName") 80 | private var _MatchCase: ImageVector? = null 81 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/util/IOCommons.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.util 2 | 3 | import java.io.InputStream 4 | import java.nio.charset.Charset 5 | import java.nio.charset.StandardCharsets 6 | 7 | fun guessIfEditableTextually( 8 | inputStream: InputStream, 9 | maxBytesToCheck: Int = 512, 10 | charset: Charset = StandardCharsets.UTF_8 11 | ): Boolean { 12 | require(maxBytesToCheck >= 0) { "maxBytesToCheck must be non-negative" } 13 | 14 | val bomBytes = ByteArray(4) 15 | var bomLength = 0 16 | 17 | inputStream.mark(maxBytesToCheck + bomBytes.size) 18 | try { 19 | val bytesForBom = inputStream.read(bomBytes, 0, minOf(bomBytes.size, maxBytesToCheck)) 20 | if (bytesForBom > 0) { 21 | when { 22 | bytesForBom >= 3 && bomBytes[0].toUByte() == 0xEF.toUByte() && bomBytes[1].toUByte() == 0xBB.toUByte() && bomBytes[2].toUByte() == 0xBF.toUByte() -> { 23 | bomLength = 3 24 | } 25 | bytesForBom >= 2 && bomBytes[0].toUByte() == 0xFE.toUByte() && bomBytes[1].toUByte() == 0xFF.toUByte() -> { 26 | bomLength = 2 27 | } 28 | bytesForBom >= 2 && bomBytes[0].toUByte() == 0xFF.toUByte() && bomBytes[1].toUByte() == 0xFE.toUByte() -> { 29 | bomLength = 2 30 | } 31 | bytesForBom >= 4 && bomBytes[0].toUByte() == 0x00.toUByte() && bomBytes[1].toUByte() == 0x00.toUByte() && bomBytes[2].toUByte() == 0xFE.toUByte() && bomBytes[3].toUByte() == 0xFF.toUByte() -> { 32 | bomLength = 4 33 | } 34 | bytesForBom >= 4 && bomBytes[0].toUByte() == 0xFF.toUByte() && bomBytes[1].toUByte() == 0xFE.toUByte() && bomBytes[2].toUByte() == 0x00.toUByte() && bomBytes[3].toUByte() == 0x00.toUByte() -> { 35 | bomLength = 4 36 | } 37 | } 38 | } 39 | } finally { 40 | inputStream.reset() 41 | if (bomLength > 0) { 42 | inputStream.skip(bomLength.toLong()) 43 | } 44 | } 45 | 46 | return inputStream.buffered().use { bufferedStream -> 47 | val buffer = ByteArray(maxBytesToCheck) 48 | val bytesRead = bufferedStream.read(buffer) 49 | 50 | if (bytesRead <= 0) { 51 | return false 52 | } 53 | 54 | val text = try { 55 | buffer.copyOf(bytesRead).toString(charset) 56 | } catch (e: Exception) { 57 | return false 58 | } 59 | 60 | val weirdCharCount = text.count { char -> 61 | val code = char.code 62 | (code in 0..31 && char !in setOf('\n', '\r', '\t')) || code == 127 || code == 0 63 | } 64 | 65 | val weirdCharRatio = weirdCharCount.toDouble() / text.length 66 | val threshold = 0.01 67 | 68 | weirdCharRatio < threshold 69 | } 70 | } 71 | 72 | fun formatFileSize(sizeInBytes: Long): String { 73 | if (sizeInBytes < 0) return "Invalid size" 74 | 75 | val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB") 76 | var size = sizeInBytes.toDouble() 77 | var unitIndex = 0 78 | while (size >= 1024 && unitIndex < units.size - 1) { 79 | size /= 1024 80 | unitIndex++ 81 | } 82 | return String.format("%.2f %s", size, units[unitIndex]) 83 | } 84 | 85 | fun formatFileDate(timestamp: Long): String { 86 | val date = java.util.Date(timestamp) 87 | val formatter = java.text.SimpleDateFormat("yy-MM-dd HH:mm:ss") 88 | return formatter.format(date) 89 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/tree/FileSelector.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.ui.tree 2 | 3 | import androidx.compose.foundation.border 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.outlined.Cancel 8 | import androidx.compose.material.icons.outlined.Check 9 | import androidx.compose.material3.AlertDialog 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.material3.TextButton 14 | import androidx.compose.runtime.* 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.unit.dp 18 | import dalvikus.composeapp.generated.resources.Res 19 | import dalvikus.composeapp.generated.resources.cancel 20 | import dalvikus.composeapp.generated.resources.select 21 | import me.lkl.dalvikus.tree.Node 22 | import me.lkl.dalvikus.tree.filesystem.FileSystemFileNode 23 | import me.lkl.dalvikus.tree.filesystem.FileSystemFolderNode 24 | import me.lkl.dalvikus.tree.root.HiddenRoot 25 | import org.jetbrains.compose.resources.stringResource 26 | import java.io.File 27 | 28 | @Composable 29 | fun FileSelectorDialog( 30 | title: String, 31 | message: String? = null, 32 | filePredicate: (Node) -> Boolean = { it is FileSystemFileNode }, 33 | onDismissRequest: () -> Unit, 34 | onFileSelected: (Node) -> Unit, 35 | ) { 36 | var selectedFile by remember { 37 | mutableStateOf(null) 38 | } 39 | var treeRoot by remember { 40 | mutableStateOf(HiddenRoot(FileSystemFolderNode("Home directory", File(System.getProperty("user.home")), null))) 41 | } 42 | 43 | AlertDialog( 44 | onDismissRequest = onDismissRequest, 45 | title = { 46 | Text(text = title) 47 | }, 48 | text = { 49 | Column { 50 | message?.let { 51 | Text(text = it) 52 | Spacer(Modifier.size(8.dp)) 53 | } 54 | Box(modifier = Modifier 55 | .size(800.dp, 600.dp) 56 | .border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.12f), shape = RoundedCornerShape(4.dp)) 57 | ) { 58 | TreeView( 59 | root = treeRoot, 60 | onFileSelected = { selectedFile = it }, 61 | selectedElement = selectedFile, 62 | ) 63 | } 64 | } 65 | }, 66 | confirmButton = { 67 | TextButton( 68 | enabled = selectedFile != null && filePredicate(selectedFile!!), 69 | onClick = { 70 | onFileSelected(selectedFile!!) 71 | } 72 | ) { 73 | Icon( 74 | imageVector = Icons.Outlined.Check, 75 | contentDescription = null 76 | ) 77 | Spacer(Modifier.width(8.dp)) 78 | Text(stringResource(Res.string.select)) 79 | } 80 | }, 81 | dismissButton = { 82 | TextButton(onClick = onDismissRequest) { 83 | Icon( 84 | imageVector = Icons.Outlined.Cancel, 85 | contentDescription = null 86 | ) 87 | Spacer(Modifier.width(8.dp)) 88 | Text(stringResource(Res.string.cancel)) 89 | } 90 | } 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/icons/RegularExpression.kt: -------------------------------------------------------------------------------- 1 | package io.github.composegears.valkyrie 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | import androidx.compose.ui.graphics.vector.path 8 | import androidx.compose.ui.unit.dp 9 | 10 | val Icons.Filled.RegularExpression: ImageVector 11 | get() { 12 | if (_RegularExpression != null) { 13 | return _RegularExpression!! 14 | } 15 | _RegularExpression = ImageVector.Builder( 16 | name = "Filled.RegularExpression", 17 | defaultWidth = 24.dp, 18 | defaultHeight = 24.dp, 19 | viewportWidth = 960f, 20 | viewportHeight = 960f 21 | ).apply { 22 | path(fill = SolidColor(Color(0xFFE8EAED))) { 23 | moveTo(197f, 761f) 24 | quadToRelative(-56f, -57f, -86.5f, -130f) 25 | reflectiveQuadTo(80f, 478f) 26 | quadToRelative(0f, -80f, 30f, -153f) 27 | reflectiveQuadToRelative(87f, -130f) 28 | lineToRelative(57f, 57f) 29 | quadToRelative(-46f, 45f, -70f, 103.5f) 30 | reflectiveQuadTo(160f, 478f) 31 | quadToRelative(0f, 64f, 24.5f, 122.5f) 32 | reflectiveQuadTo(254f, 704f) 33 | lineToRelative(-57f, 57f) 34 | close() 35 | moveTo(380f, 720f) 36 | quadToRelative(-25f, 0f, -42.5f, -17.5f) 37 | reflectiveQuadTo(320f, 660f) 38 | quadToRelative(0f, -25f, 17.5f, -42.5f) 39 | reflectiveQuadTo(380f, 600f) 40 | quadToRelative(25f, 0f, 42.5f, 17.5f) 41 | reflectiveQuadTo(440f, 660f) 42 | quadToRelative(0f, 25f, -17.5f, 42.5f) 43 | reflectiveQuadTo(380f, 720f) 44 | close() 45 | moveTo(519f, 520f) 46 | verticalLineToRelative(-71f) 47 | lineToRelative(-61f, 36f) 48 | lineToRelative(-40f, -70f) 49 | lineToRelative(61f, -35f) 50 | lineToRelative(-61f, -35f) 51 | lineToRelative(40f, -70f) 52 | lineToRelative(61f, 36f) 53 | verticalLineToRelative(-71f) 54 | horizontalLineToRelative(80f) 55 | verticalLineToRelative(71f) 56 | lineToRelative(61f, -36f) 57 | lineToRelative(40f, 70f) 58 | lineToRelative(-61f, 35f) 59 | lineToRelative(61f, 35f) 60 | lineToRelative(-40f, 70f) 61 | lineToRelative(-61f, -36f) 62 | verticalLineToRelative(71f) 63 | horizontalLineToRelative(-80f) 64 | close() 65 | moveTo(763f, 761f) 66 | lineTo(706f, 704f) 67 | quadToRelative(46f, -45f, 70f, -103.5f) 68 | reflectiveQuadTo(800f, 478f) 69 | quadToRelative(0f, -64f, -24.5f, -122.5f) 70 | reflectiveQuadTo(706f, 252f) 71 | lineToRelative(57f, -57f) 72 | quadToRelative(56f, 57f, 86.5f, 130f) 73 | reflectiveQuadTo(880f, 478f) 74 | quadToRelative(0f, 80f, -30f, 153f) 75 | reflectiveQuadToRelative(-87f, 130f) 76 | close() 77 | } 78 | }.build() 79 | 80 | return _RegularExpression!! 81 | } 82 | 83 | @Suppress("ObjectPropertyName") 84 | private var _RegularExpression: ImageVector? = null 85 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/settings/SettingsView.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.VerticalScrollbar 2 | import androidx.compose.foundation.layout.* 3 | import androidx.compose.foundation.lazy.grid.GridCells 4 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 5 | import androidx.compose.foundation.lazy.grid.rememberLazyGridState 6 | import androidx.compose.foundation.rememberScrollbarAdapter 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.currentCompositionContext 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import dalvikus.composeapp.generated.resources.Res 15 | import dalvikus.composeapp.generated.resources.credits_and_version 16 | import me.lkl.dalvikus.dalvikusSettings 17 | import me.lkl.dalvikus.settings.Setting 18 | import me.lkl.dalvikus.util.CollapseCard 19 | import me.lkl.dalvikus.util.CollapseCardMaxWidth 20 | import org.jetbrains.compose.resources.stringResource 21 | 22 | @Composable 23 | fun SettingsView() { 24 | val grouped = dalvikusSettings.groupedByCategory() 25 | val gridState = rememberLazyGridState() 26 | 27 | Box(modifier = Modifier.fillMaxSize()) { 28 | Text( 29 | text = stringResource(Res.string.credits_and_version, dalvikusSettings.getVersion()), 30 | style = MaterialTheme.typography.bodyMedium, 31 | color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), 32 | modifier = Modifier 33 | .align(Alignment.BottomEnd) 34 | .padding(16.dp) 35 | ) 36 | 37 | LazyVerticalGrid( 38 | columns = GridCells.Adaptive(minSize = CollapseCardMaxWidth), 39 | state = gridState, 40 | modifier = Modifier 41 | .fillMaxSize() 42 | .padding(end = 12.dp), 43 | contentPadding = PaddingValues(8.dp), 44 | horizontalArrangement = Arrangement.spacedBy(12.dp), 45 | verticalArrangement = Arrangement.spacedBy(12.dp) 46 | ) { 47 | grouped.forEach { (category, settings) -> 48 | item { 49 | CollapseCard( 50 | title = stringResource(category.nameRes), 51 | icon = category.icon, 52 | defaultState = false 53 | ) { 54 | Column { 55 | settings.forEach { setting -> 56 | SettingRow(setting) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | VerticalScrollbar( 65 | adapter = rememberScrollbarAdapter(scrollState = gridState), 66 | modifier = Modifier 67 | .align(Alignment.CenterEnd) 68 | .fillMaxHeight() 69 | .padding(vertical = 8.dp, horizontal = 8.dp) 70 | ) 71 | 72 | } 73 | } 74 | 75 | val settingPadVer = 8.dp 76 | val settingPadHor = 16.dp 77 | 78 | @Composable 79 | fun SettingRow(setting: Setting<*>) { 80 | Column( 81 | modifier = Modifier 82 | .fillMaxWidth() 83 | .padding(vertical = settingPadVer, horizontal = settingPadHor) 84 | ) { 85 | Text( 86 | text = stringResource(setting.nameRes), 87 | style = MaterialTheme.typography.bodyLarge, 88 | 89 | ) 90 | setting.Editor { 91 | dalvikusSettings.saveAll() 92 | } 93 | Spacer(modifier = Modifier.height(settingPadVer)) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/icons/FamilyHistory.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.icons 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | import androidx.compose.ui.graphics.vector.path 8 | import androidx.compose.ui.unit.dp 9 | 10 | val Icons.Filled.FamilyHistory: ImageVector 11 | get() { 12 | if (_FamilyHistory != null) { 13 | return _FamilyHistory!! 14 | } 15 | _FamilyHistory = ImageVector.Builder( 16 | name = "Filled.FamilyHistory", 17 | defaultWidth = 24.dp, 18 | defaultHeight = 24.dp, 19 | viewportWidth = 960f, 20 | viewportHeight = 960f 21 | ).apply { 22 | path(fill = SolidColor(Color(0xFFE8EAED))) { 23 | moveTo(480f, 900f) 24 | quadToRelative(-63f, 0f, -106.5f, -43.5f) 25 | reflectiveQuadTo(330f, 750f) 26 | quadToRelative(0f, -52f, 31f, -91.5f) 27 | reflectiveQuadToRelative(79f, -53.5f) 28 | verticalLineToRelative(-85f) 29 | lineTo(200f, 520f) 30 | verticalLineToRelative(-160f) 31 | lineTo(100f, 360f) 32 | verticalLineToRelative(-280f) 33 | horizontalLineToRelative(280f) 34 | verticalLineToRelative(280f) 35 | lineTo(280f, 360f) 36 | verticalLineToRelative(80f) 37 | horizontalLineToRelative(400f) 38 | verticalLineToRelative(-85f) 39 | quadToRelative(-48f, -14f, -79f, -53.5f) 40 | reflectiveQuadTo(570f, 210f) 41 | quadToRelative(0f, -63f, 43.5f, -106.5f) 42 | reflectiveQuadTo(720f, 60f) 43 | quadToRelative(63f, 0f, 106.5f, 43.5f) 44 | reflectiveQuadTo(870f, 210f) 45 | quadToRelative(0f, 52f, -31f, 91.5f) 46 | reflectiveQuadTo(760f, 355f) 47 | verticalLineToRelative(165f) 48 | lineTo(520f, 520f) 49 | verticalLineToRelative(85f) 50 | quadToRelative(48f, 14f, 79f, 53.5f) 51 | reflectiveQuadToRelative(31f, 91.5f) 52 | quadToRelative(0f, 63f, -43.5f, 106.5f) 53 | reflectiveQuadTo(480f, 900f) 54 | close() 55 | moveTo(720f, 280f) 56 | quadToRelative(29f, 0f, 49.5f, -20.5f) 57 | reflectiveQuadTo(790f, 210f) 58 | quadToRelative(0f, -29f, -20.5f, -49.5f) 59 | reflectiveQuadTo(720f, 140f) 60 | quadToRelative(-29f, 0f, -49.5f, 20.5f) 61 | reflectiveQuadTo(650f, 210f) 62 | quadToRelative(0f, 29f, 20.5f, 49.5f) 63 | reflectiveQuadTo(720f, 280f) 64 | close() 65 | moveTo(180f, 280f) 66 | horizontalLineToRelative(120f) 67 | verticalLineToRelative(-120f) 68 | lineTo(180f, 160f) 69 | verticalLineToRelative(120f) 70 | close() 71 | moveTo(480f, 820f) 72 | quadToRelative(29f, 0f, 49.5f, -20.5f) 73 | reflectiveQuadTo(550f, 750f) 74 | quadToRelative(0f, -29f, -20.5f, -49.5f) 75 | reflectiveQuadTo(480f, 680f) 76 | quadToRelative(-29f, 0f, -49.5f, 20.5f) 77 | reflectiveQuadTo(410f, 750f) 78 | quadToRelative(0f, 29f, 20.5f, 49.5f) 79 | reflectiveQuadTo(480f, 820f) 80 | close() 81 | moveTo(240f, 220f) 82 | close() 83 | moveTo(720f, 210f) 84 | close() 85 | moveTo(480f, 750f) 86 | close() 87 | } 88 | }.build() 89 | 90 | return _FamilyHistory!! 91 | } 92 | 93 | @Suppress("ObjectPropertyName") 94 | private var _FamilyHistory: ImageVector? = null 95 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/icons/ThreadUnread.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.icons 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | import androidx.compose.ui.graphics.vector.path 8 | import androidx.compose.ui.unit.dp 9 | 10 | val Icons.Filled.ThreadUnread: ImageVector 11 | get() { 12 | if (_ThreadUnread != null) { 13 | return _ThreadUnread!! 14 | } 15 | _ThreadUnread = ImageVector.Builder( 16 | name = "Filled.ThreadUnread", 17 | defaultWidth = 24.dp, 18 | defaultHeight = 24.dp, 19 | viewportWidth = 960f, 20 | viewportHeight = 960f 21 | ).apply { 22 | path(fill = SolidColor(Color(0xFFE8EAED))) { 23 | moveTo(554f, 840f) 24 | quadToRelative(-54f, 0f, -91f, -37f) 25 | reflectiveQuadToRelative(-37f, -89f) 26 | quadToRelative(0f, -76f, 61.5f, -137.5f) 27 | reflectiveQuadTo(641f, 500f) 28 | quadToRelative(-3f, -36f, -18f, -54.5f) 29 | reflectiveQuadTo(582f, 427f) 30 | quadToRelative(-30f, 0f, -65f, 25f) 31 | reflectiveQuadToRelative(-83f, 82f) 32 | quadToRelative(-78f, 93f, -114.5f, 121f) 33 | reflectiveQuadTo(241f, 683f) 34 | quadToRelative(-51f, 0f, -86f, -38f) 35 | reflectiveQuadToRelative(-35f, -92f) 36 | quadToRelative(0f, -54f, 23.5f, -110.5f) 37 | reflectiveQuadTo(223f, 307f) 38 | quadToRelative(19f, -26f, 28f, -44f) 39 | reflectiveQuadToRelative(9f, -29f) 40 | quadToRelative(0f, -7f, -2.5f, -10.5f) 41 | reflectiveQuadTo(250f, 220f) 42 | quadToRelative(-10f, 0f, -25f, 12.5f) 43 | reflectiveQuadTo(190f, 271f) 44 | lineToRelative(-70f, -71f) 45 | quadToRelative(32f, -39f, 65f, -59.5f) 46 | reflectiveQuadToRelative(65f, -20.5f) 47 | quadToRelative(46f, 0f, 78f, 32f) 48 | reflectiveQuadToRelative(32f, 80f) 49 | quadToRelative(0f, 29f, -15f, 64f) 50 | reflectiveQuadToRelative(-50f, 84f) 51 | quadToRelative(-38f, 54f, -56.5f, 95f) 52 | reflectiveQuadTo(220f, 547f) 53 | quadToRelative(0f, 17f, 5.5f, 26.5f) 54 | reflectiveQuadTo(241f, 583f) 55 | quadToRelative(10f, 0f, 17.5f, -5.5f) 56 | reflectiveQuadTo(286f, 551f) 57 | quadToRelative(13f, -14f, 31f, -34.5f) 58 | reflectiveQuadToRelative(44f, -50.5f) 59 | quadToRelative(63f, -75f, 114f, -107f) 60 | reflectiveQuadToRelative(107f, -32f) 61 | quadToRelative(67f, 0f, 110f, 45f) 62 | reflectiveQuadToRelative(49f, 123f) 63 | horizontalLineToRelative(99f) 64 | verticalLineToRelative(100f) 65 | horizontalLineToRelative(-99f) 66 | quadToRelative(-8f, 112f, -58.5f, 178.5f) 67 | reflectiveQuadTo(554f, 840f) 68 | close() 69 | moveTo(556f, 740f) 70 | quadToRelative(32f, 0f, 54f, -36.5f) 71 | reflectiveQuadTo(640f, 602f) 72 | quadToRelative(-46f, 11f, -80f, 43.5f) 73 | reflectiveQuadTo(526f, 710f) 74 | quadToRelative(0f, 14f, 8f, 22f) 75 | reflectiveQuadToRelative(22f, 8f) 76 | close() 77 | moveTo(800f, 280f) 78 | quadToRelative(-50f, 0f, -85f, -35f) 79 | reflectiveQuadToRelative(-35f, -85f) 80 | quadToRelative(0f, -50f, 35f, -85f) 81 | reflectiveQuadToRelative(85f, -35f) 82 | quadToRelative(50f, 0f, 85f, 35f) 83 | reflectiveQuadToRelative(35f, 85f) 84 | quadToRelative(0f, 50f, -35f, 85f) 85 | reflectiveQuadToRelative(-85f, 35f) 86 | close() 87 | } 88 | }.build() 89 | 90 | return _ThreadUnread!! 91 | } 92 | 93 | @Suppress("ObjectPropertyName") 94 | private var _ThreadUnread: ImageVector? = null 95 | -------------------------------------------------------------------------------- /composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.ExperimentalComposeLibrary 2 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 3 | import org.jetbrains.compose.reload.ComposeHotRun 4 | import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag 5 | 6 | plugins { 7 | alias(libs.plugins.multiplatform) 8 | alias(libs.plugins.compose.compiler) 9 | alias(libs.plugins.compose) 10 | alias(libs.plugins.hotReload) 11 | } 12 | 13 | kotlin { 14 | jvm() 15 | 16 | sourceSets { 17 | commonMain.dependencies { 18 | implementation(compose.runtime) 19 | implementation(compose.foundation) 20 | // TODO replace with compose.material3 again when material expressive is stable. 21 | implementation("org.jetbrains.compose.material3:material3:1.9.0-alpha04") 22 | implementation(compose.components.resources) 23 | implementation(compose.components.uiToolingPreview) 24 | 25 | implementation(compose.materialIconsExtended) 26 | implementation(libs.splitPaneDesktop) 27 | implementation(libs.haze) 28 | implementation(libs.hazeMaterials) 29 | implementation(libs.materialKolor) 30 | 31 | implementation(libs.kermit) 32 | implementation(libs.multiplatformSettings) 33 | 34 | implementation(libs.kotlinxCoroutinesSwing) 35 | 36 | implementation(libs.smali) 37 | implementation(libs.smaliDexlib2) 38 | implementation(libs.smaliBaksmali) 39 | implementation(libs.smaliUtil) 40 | 41 | implementation(libs.dex2jarNicoMexis) 42 | implementation(libs.jadx) 43 | implementation(libs.jadxSmaliInput) 44 | implementation(libs.cfr) 45 | implementation(libs.vineflower) 46 | 47 | implementation(libs.antlr4) 48 | 49 | implementation(libs.apksig) 50 | implementation(libs.ddmlib) 51 | 52 | implementation(libs.apktoolLib) 53 | 54 | } 55 | 56 | commonTest.dependencies { 57 | implementation(kotlin("test")) 58 | @OptIn(ExperimentalComposeLibrary::class) 59 | implementation(compose.uiTest) 60 | } 61 | 62 | jvmMain.dependencies { 63 | implementation(compose.desktop.currentOs) 64 | } 65 | 66 | } 67 | } 68 | 69 | val appVersion = "1.0.13" 70 | 71 | compose.desktop { 72 | application { 73 | mainClass = "MainKt" 74 | 75 | buildTypes.release.proguard { 76 | configurationFiles.from( 77 | project.file("proguard-rules.pro") 78 | ) 79 | // TODO enable when ready. 80 | isEnabled.set(false) 81 | } 82 | 83 | jvmArgs("-Dapp.version=$appVersion") 84 | 85 | nativeDistributions { 86 | // add zipfs support for dex2jar 87 | modules( 88 | "jdk.zipfs", 89 | ) 90 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Exe, TargetFormat.Deb) 91 | packageName = "dalvikus" 92 | packageVersion = appVersion 93 | description = "Dalvikus reverse engineering tool" 94 | vendor = "Leonhard Kohl-Loerting" 95 | copyright = "© 2025 Leonhard Kohl-Loerting - License: GPL-3.0" 96 | licenseFile.set(project.file("../LICENSE")) 97 | 98 | linux { 99 | iconFile.set(project.file("desktopAppIcons/logo.png")) 100 | debPackageVersion = appVersion 101 | debMaintainer = "Leonhard Kohl-Loerting" 102 | } 103 | 104 | windows { 105 | iconFile.set(project.file("desktopAppIcons/logo.ico")) 106 | exePackageVersion = appVersion 107 | msiPackageVersion = appVersion 108 | 109 | menu = true 110 | shortcut = true 111 | 112 | } 113 | macOS { 114 | iconFile.set(project.file("desktopAppIcons/logo.icns")) 115 | bundleID = "me.lkl.dalvikus.desktopApp" 116 | } 117 | } 118 | } 119 | } 120 | 121 | //https://github.com/JetBrains/compose-hot-reload 122 | composeCompiler { 123 | featureFlags.add(ComposeFeatureFlag.OptimizeNonSkippingGroups) 124 | } 125 | 126 | tasks.withType().configureEach { 127 | mainClass.set("MainKt") 128 | } 129 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/archive/ZipNode.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree.archive 2 | 3 | import me.lkl.dalvikus.tree.ContainerNode 4 | import me.lkl.dalvikus.tree.Node 5 | import me.lkl.dalvikus.tree.backing.Backing 6 | import me.lkl.dalvikus.tree.backing.ZipBacking 7 | import me.lkl.dalvikus.tree.buildChildNodes 8 | import me.lkl.dalvikus.tree.dex.DexFileNode 9 | import me.lkl.dalvikus.theme.getFileExtensionMeta 10 | import me.lkl.dalvikus.theme.readableImageFormats 11 | import java.io.File 12 | import java.util.zip.CRC32 13 | import java.util.zip.ZipEntry 14 | import java.util.zip.ZipFile 15 | import java.util.zip.ZipOutputStream 16 | 17 | open class ZipNode( 18 | override val name: String, 19 | open val backing: Backing, 20 | override val parent: ContainerNode? 21 | ) : ContainerNode() { 22 | 23 | val entries = mutableMapOf() 24 | 25 | override val icon 26 | get() = getFileExtensionMeta(name).icon 27 | override val changesWithChildren = true 28 | 29 | override suspend fun loadChildrenInternal(): List { 30 | entries.clear() 31 | 32 | val zip = ZipFile(backing.getFileOrCreateTemp(".zip")) 33 | 34 | zip.entries().toList().forEach { entry -> 35 | val name = entry.name 36 | val bytes = if (!entry.isDirectory) zip.getInputStream(entry).readBytes() else null 37 | if (bytes != null) entries[name] = bytes 38 | } 39 | 40 | zip.close() 41 | 42 | return buildChildNodes( 43 | entries = entries, 44 | prefix = "", 45 | onFolder = { name, path -> 46 | ZipEntryFolderNode(name, path, this, this) 47 | }, 48 | onFile = { name, path, bytes -> 49 | when { 50 | name.endsWith(".dex") -> DexFileNode(name, ZipBacking(path, this), this) 51 | name.endsWith(".xml") && this is ApkNode -> ApkEntryXmlNode(name, path, this, this) 52 | name.substringAfterLast(".").lowercase() in readableImageFormats -> ZipEntryImageNode(name, path, this, this) 53 | // Support nested zip/apk files 54 | name.endsWith(".zip", ignoreCase = true) || 55 | name.endsWith(".jar", ignoreCase = true) || 56 | name.endsWith(".xapk", ignoreCase = true) || 57 | name.endsWith(".apks", ignoreCase = true) -> ZipNode(name, ZipBacking(path, this), this) 58 | name.endsWith(".apk", ignoreCase = true) -> ApkNode(name, ZipBacking(path, this), this) 59 | name.endsWith(".aab", ignoreCase = true) -> ZipNode(name, ZipBacking(path, this), this) 60 | else -> ZipEntryFileNode(name, path, this, this) 61 | } 62 | } 63 | ) 64 | } 65 | 66 | open fun readEntry(path: String): ByteArray { 67 | if (entries.isEmpty()) { 68 | throw IllegalStateException("Entries not loaded. Call loadChildrenInternal() first.") 69 | } 70 | return entries[path] ?: error("Entry not found: $path") 71 | } 72 | 73 | open suspend fun updateEntry(path: String, newContent: ByteArray) { 74 | entries[path] = newContent 75 | rebuild() 76 | } 77 | 78 | override suspend fun rebuild() { 79 | val newBytes = rebuildZipBytes() 80 | backing.write(newBytes) 81 | } 82 | 83 | private fun rebuildZipBytes(): ByteArray { 84 | val tmp = File.createTempFile("rebuild", ".zip") 85 | try { 86 | ZipOutputStream(tmp.outputStream()).use { zos -> 87 | for ((path, data) in entries) { 88 | val entry = ZipEntry(path) 89 | 90 | if (this is ApkNode && (path == "resources.arsc" || (path.startsWith("lib/") && path.endsWith(".so")))) { 91 | entry.method = ZipEntry.STORED 92 | 93 | val crc = CRC32() 94 | crc.update(data) 95 | 96 | entry.size = data.size.toLong() 97 | entry.compressedSize = data.size.toLong() 98 | entry.crc = crc.value 99 | } 100 | 101 | zos.putNextEntry(entry) 102 | zos.write(data) 103 | zos.closeEntry() 104 | } 105 | } 106 | return tmp.readBytes() 107 | } finally { 108 | tmp.delete() 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/lexer/SmaliLexerHighlight.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.lexer 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | import androidx.compose.ui.text.SpanStyle 5 | import androidx.compose.ui.text.buildAnnotatedString 6 | import androidx.compose.ui.text.font.FontStyle 7 | import androidx.compose.ui.text.font.FontWeight 8 | import androidx.compose.ui.text.style.TextDecoration 9 | import com.android.tools.smali.smali.smaliFlexLexer 10 | import com.android.tools.smali.smali.smaliParser 11 | import me.lkl.dalvikus.dalvikusSettings 12 | import me.lkl.dalvikus.smali.ErrorHandlingSmaliParser 13 | import me.lkl.dalvikus.theme.CodeHighlightColors 14 | import org.antlr.runtime.* 15 | 16 | 17 | fun highlightSmaliCode(code: String, colors: CodeHighlightColors): AnnotatedString { 18 | val apiLevel = dalvikusSettings["api_level"] as Int 19 | val lexer = smaliFlexLexer(code.reader(), apiLevel) 20 | val tokens = CommonTokenStream(lexer) 21 | 22 | val parser = ErrorHandlingSmaliParser(tokens) 23 | 24 | parser.setVerboseErrors(true) 25 | parser.setAllowOdex(true) 26 | parser.setApiLevel(apiLevel) 27 | parser.smali_file() 28 | 29 | return buildAnnotatedString { 30 | append(code) 31 | 32 | for (token in tokens.tokens) { 33 | if (token !is CommonToken) continue 34 | if (token.type < 0 || token.text.isNullOrBlank()) continue 35 | 36 | val start = token.startIndex 37 | val end = token.stopIndex + 1 38 | 39 | if (token.type == smaliParser.CLASS_DESCRIPTOR) { 40 | val clazz = token.text.removePrefix("L").removeSuffix(";") 41 | addStringAnnotation("class", clazz, start, end) 42 | } 43 | else if (token.type in listOf( 44 | smaliParser.INTEGER_LITERAL, 45 | smaliParser.POSITIVE_INTEGER_LITERAL, 46 | smaliParser.SHORT_LITERAL, 47 | smaliParser.LONG_LITERAL, 48 | ) && token.text.startsWith("0x") || token.text.startsWith("-0x")) { 49 | addStringAnnotation("hex", token.text, start, end) 50 | } 51 | 52 | if (token in parser.errorTokens) { 53 | addStringAnnotation("error", parser.errorTokens[token]!!, start, end) 54 | addStyle( 55 | SpanStyle( 56 | color = colors.error, 57 | textDecoration = TextDecoration.Underline 58 | ), 59 | start, end 60 | ) 61 | } else { 62 | getSmaliTokenStyle(token.type, colors)?.let { style -> 63 | addStyle(style, start, end) 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | private fun getSmaliTokenName(tokenType: Int): String? { 71 | return smaliParser::class.java.fields 72 | .firstOrNull { it.type == Int::class.java && it.get(null) == tokenType } 73 | ?.name 74 | } 75 | 76 | fun getSmaliTokenStyle(tokenType: Int, colors: CodeHighlightColors): SpanStyle? { 77 | val tokenName = getSmaliTokenName(tokenType) ?: return null 78 | 79 | val color = when { 80 | tokenName.startsWith("INSTRUCTION_") -> colors.senary 81 | tokenName.endsWith("_DIRECTIVE") -> colors.primary 82 | tokenName.endsWith("_LITERAL") -> colors.tertiary 83 | tokenName.endsWith("_TYPE") || tokenName == "CLASS_DESCRIPTOR" || tokenName == "ARRAY_TYPE_PREFIX" -> colors.quaternary 84 | tokenName.endsWith("_NAME") -> colors.quinary 85 | 86 | tokenName == "REGISTER" -> colors.secondary 87 | tokenName == "ACCESS_SPEC" || tokenName == "ANNOTATION_VISIBILITY" -> colors.septenary 88 | 89 | tokenName in listOf("COLON", "COMMA", "OPEN_PAREN", "CLOSE_PAREN") -> colors.onSurface.copy(alpha = 0.7f) 90 | tokenName == "LINE_COMMENT" -> colors.onSurface.copy(alpha = 0.5f) 91 | else -> colors.onSurface 92 | } 93 | 94 | val weight = when { 95 | tokenName == "ARROW" -> FontWeight.Bold 96 | tokenName.startsWith("INSTRUCTION_") -> FontWeight.Medium 97 | tokenName == "ACCESS_SPEC" || tokenName == "ANNOTATION_VISIBILITY" -> FontWeight.SemiBold 98 | tokenName.endsWith("_DIRECTIVE") -> FontWeight.SemiBold 99 | 100 | else -> FontWeight.Normal 101 | } 102 | 103 | val style = when (tokenName) { 104 | "LINE_COMMENT", "SIMPLE_NAME" -> FontStyle.Italic 105 | else -> FontStyle.Normal 106 | } 107 | 108 | return SpanStyle(color = color, fontWeight = weight, fontStyle = style) 109 | } 110 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/image/ImageView.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.ui.image 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.material3.CircularProgressIndicator 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.* 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.geometry.Offset 16 | import androidx.compose.ui.graphics.FilterQuality 17 | import androidx.compose.ui.graphics.ImageBitmap 18 | import androidx.compose.ui.graphics.graphicsLayer 19 | import androidx.compose.ui.graphics.toComposeImageBitmap 20 | import androidx.compose.ui.input.mouse.mouseScrollFilter 21 | import androidx.compose.ui.input.pointer.PointerEventType 22 | import androidx.compose.ui.input.pointer.pointerInput 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.unit.sp 25 | import kotlinx.coroutines.Dispatchers 26 | import kotlinx.coroutines.withContext 27 | import me.lkl.dalvikus.tabs.ImageTab 28 | import org.jetbrains.skia.Image as SkiaImage 29 | 30 | @Composable 31 | fun ImageView(tab: ImageTab) { 32 | val imageByteArray by tab.contentProvider.contentFlow.collectAsState() 33 | var image by remember(imageByteArray) { mutableStateOf(null) } 34 | var scale by remember { mutableStateOf(1f) } 35 | var offset by remember { mutableStateOf(Offset.Zero) } 36 | 37 | LaunchedEffect(tab) { 38 | tab.contentProvider.loadContent() 39 | } 40 | 41 | LaunchedEffect(imageByteArray) { 42 | image = withContext(Dispatchers.Default) { 43 | runCatching { 44 | SkiaImage.makeFromEncoded(imageByteArray).toComposeImageBitmap() 45 | }.getOrNull() 46 | } 47 | scale = 1f 48 | offset = Offset.Zero 49 | } 50 | 51 | val gestureModifier = Modifier.pointerInput(Unit) { 52 | awaitPointerEventScope { 53 | while (true) { 54 | val event = awaitPointerEvent() 55 | if (event.type == PointerEventType.Scroll) { 56 | val scrollDelta = event.changes.first().scrollDelta 57 | val zoomFactor1 = 1.1f 58 | scale = if (scrollDelta.y > 0) { 59 | (scale * zoomFactor1).coerceIn(0.1f, 10f) 60 | } else { 61 | (scale / zoomFactor1).coerceIn(0.1f, 10f) 62 | } 63 | if (true) { 64 | event.changes.first().consume() 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | Box( 72 | modifier = Modifier 73 | .fillMaxSize() 74 | .background(MaterialTheme.colorScheme.surfaceContainerLow) 75 | .padding(24.dp), 76 | contentAlignment = Alignment.Center 77 | ) { 78 | image?.let { img -> 79 | val imageWidth = img.width 80 | val imageHeight = img.height 81 | 82 | Box( 83 | modifier = gestureModifier 84 | .graphicsLayer( 85 | scaleX = scale, 86 | scaleY = scale, 87 | translationX = offset.x, 88 | translationY = offset.y 89 | ) 90 | .align(Alignment.Center) 91 | ) { 92 | Image( 93 | bitmap = img, 94 | filterQuality = FilterQuality.None, 95 | contentDescription = null, 96 | modifier = Modifier 97 | .align(Alignment.Center) 98 | .background(MaterialTheme.colorScheme.surfaceVariant) 99 | ) 100 | } 101 | 102 | Text( 103 | text = "$imageWidth x $imageHeight", 104 | color = MaterialTheme.colorScheme.onBackground, 105 | fontSize = 14.sp, 106 | modifier = Modifier 107 | .align(Alignment.BottomEnd) 108 | .padding(8.dp) 109 | ) 110 | } ?: run { 111 | CircularProgressIndicator( 112 | modifier = Modifier.align(Alignment.Center), 113 | color = MaterialTheme.colorScheme.primary 114 | ) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/editor/suggestions/EditorAnnotationPopup.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.ui.editor.suggestions 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.foundation.text.selection.SelectionContainer 8 | import androidx.compose.material3.* 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.graphics.lerp 14 | import androidx.compose.ui.graphics.vector.ImageVector 15 | import androidx.compose.ui.platform.LocalDensity 16 | import androidx.compose.ui.text.AnnotatedString 17 | import androidx.compose.ui.text.TextStyle 18 | import androidx.compose.ui.unit.IntOffset 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.window.Popup 21 | import androidx.compose.ui.window.PopupProperties 22 | import dev.chrisbanes.haze.HazeState 23 | import dev.chrisbanes.haze.hazeEffect 24 | import me.lkl.dalvikus.ui.editor.LayoutSnapshot 25 | import me.lkl.dalvikus.util.defaultHazeStyle 26 | 27 | @Composable 28 | fun EditorAnnotationPopup( 29 | lastLayoutSnapshot: LayoutSnapshot?, 30 | annotation: AnnotatedString.Range, 31 | content: @Composable () -> Unit, 32 | contentWidthEstimate: Int 33 | ) { 34 | val density = LocalDensity.current 35 | val layoutResult = lastLayoutSnapshot?.layout ?: return 36 | 37 | val layoutTextLength = layoutResult.layoutInput.text.length 38 | val safeStart = annotation.start.coerceIn(0, layoutTextLength - 1) 39 | val safeEnd = annotation.end.coerceIn(0, layoutTextLength - 1) 40 | 41 | val boxStart = layoutResult.getBoundingBox(safeStart) 42 | val boxEnd = layoutResult.getBoundingBox(safeEnd) 43 | 44 | Popup( 45 | offset = with(density) { 46 | IntOffset( 47 | x = ((boxStart.left + boxEnd.left) / 2).toInt() - contentWidthEstimate / 2 - 40.dp.toPx().toInt(), 48 | y = (boxStart.top - 60.dp.toPx()).toInt() 49 | ) 50 | }, 51 | properties = PopupProperties( 52 | clippingEnabled = true, 53 | focusable = false 54 | ) 55 | ) { 56 | content() 57 | } 58 | } 59 | 60 | @Composable 61 | fun ModernPopupContainer( 62 | color: Color, 63 | icon: ImageVector? = null, 64 | text: String, 65 | textStyle: TextStyle, 66 | hazeState: HazeState, 67 | action: String? = null, 68 | actionIcon: ImageVector? = null, 69 | actionOnClick: (() -> Unit)? = null, 70 | ) { 71 | 72 | Surface( 73 | color = Color.Transparent, 74 | modifier = Modifier 75 | .wrapContentSize() 76 | .hazeEffect( 77 | state = hazeState, 78 | style = defaultHazeStyle(lerp(color, MaterialTheme.colorScheme.surface, 0.8f)) 79 | ) 80 | .border(1.dp, color.copy(alpha = 0.8f)) 81 | ) { 82 | Row( 83 | verticalAlignment = Alignment.CenterVertically 84 | ) { 85 | if (icon != null) { 86 | Icon( 87 | imageVector = icon, 88 | contentDescription = null, 89 | tint = color, 90 | modifier = Modifier 91 | .padding(8.dp) 92 | .size(20.dp) 93 | ) 94 | } 95 | 96 | SelectionContainer { 97 | Text( 98 | text = text, 99 | style = textStyle.copy(color = color), 100 | modifier = Modifier 101 | .padding(8.dp) 102 | ) 103 | } 104 | 105 | if (action != null && actionOnClick != null && actionIcon != null) { 106 | val buttonShape = RoundedCornerShape(16.dp) 107 | val buttonColors = ButtonDefaults.textButtonColors( 108 | contentColor = color.copy(alpha = 0.8f) 109 | ) 110 | 111 | TextButton( 112 | onClick = actionOnClick, 113 | shape = buttonShape, 114 | colors = buttonColors, 115 | modifier = Modifier 116 | .padding(horizontal = 8.dp) 117 | ) { 118 | Icon( 119 | imageVector = actionIcon, 120 | contentDescription = null, 121 | modifier = Modifier.size(18.dp) 122 | ) 123 | Spacer(Modifier.width(8.dp)) 124 | Text(action, softWrap = false) 125 | } 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/decompiler/CFRDecompiler.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.decompiler 2 | 3 | import co.touchlab.kermit.Logger 4 | import me.lkl.dalvikus.dalvikusSettings 5 | import org.benf.cfr.reader.api.CfrDriver 6 | import org.benf.cfr.reader.api.ClassFileSource 7 | import org.benf.cfr.reader.api.OutputSinkFactory 8 | import org.benf.cfr.reader.api.SinkReturns 9 | import org.benf.cfr.reader.bytecode.analysis.parse.utils.Pair 10 | import java.io.PrintWriter 11 | import java.io.StringWriter 12 | import java.util.* 13 | 14 | class CFRDecompiler : JavaDecompiler { 15 | override suspend fun decompileJava(internalName: String, bytes: ByteArray): String { 16 | try { 17 | val verbose = dalvikusSettings["decompiler_verbose"] as Boolean 18 | // 4) CFR bridge: feed the bytes into CFR to get Java source 19 | val options = mutableMapOf().apply { 20 | put("showversion", verbose.toString()) 21 | put("silent", (!verbose).toString()) 22 | put("dumpclasspath", verbose.toString()) 23 | 24 | put("hidelongstrings", "true") 25 | put("hideutf", "false") 26 | put("innerclasses", "false") 27 | 28 | put("forcetopsort", "true") 29 | put("forcetopsortaggress", "true") 30 | } 31 | 32 | var result: String? = null 33 | 34 | val sinkFactory = object : OutputSinkFactory { 35 | override fun getSink( 36 | sinkType: OutputSinkFactory.SinkType, 37 | sinkClass: OutputSinkFactory.SinkClass 38 | ): OutputSinkFactory.Sink { 39 | return when (sinkType) { 40 | OutputSinkFactory.SinkType.JAVA if sinkClass == OutputSinkFactory.SinkClass.DECOMPILED -> OutputSinkFactory.Sink { x -> 41 | val dr = x as SinkReturns.Decompiled 42 | result = dr.java 43 | } 44 | OutputSinkFactory.SinkType.EXCEPTION if sinkClass == OutputSinkFactory.SinkClass.EXCEPTION_MESSAGE -> OutputSinkFactory.Sink { x -> 45 | val dr = x as SinkReturns.ExceptionMessage 46 | val sw = StringWriter().also { dr.thrownException.printStackTrace(PrintWriter(it)) } 47 | result = "CFR Exception: ${dr.message}\n$sw" 48 | } 49 | else -> OutputSinkFactory.Sink { _ -> 50 | //throw UnsupportedOperationException("Unsupported sink type or class: $sinkType, $sinkClass") 51 | } 52 | } 53 | } 54 | 55 | override fun getSupportedSinks( 56 | sinkType: OutputSinkFactory.SinkType, 57 | available: Collection 58 | ) = when (sinkType) { 59 | OutputSinkFactory.SinkType.JAVA -> listOf(OutputSinkFactory.SinkClass.DECOMPILED) 60 | OutputSinkFactory.SinkType.EXCEPTION -> listOf(OutputSinkFactory.SinkClass.EXCEPTION_MESSAGE) 61 | else -> emptyList() 62 | } 63 | } 64 | 65 | val classSource = object : ClassFileSource { 66 | override fun informAnalysisRelativePathDetail(path: String?, detail: String?) = Unit 67 | override fun getPossiblyRenamedPath(path: String?) = path 68 | override fun getClassFileContent(path: String): Pair { 69 | val pathInternalName = path.substringBeforeLast(".class") 70 | return if (pathInternalName == internalName) { 71 | Pair.make(bytes, internalName) 72 | } else { 73 | // fallback to standard resource loading 74 | val url = this::class.java.classLoader.getResource("$path") 75 | ?: throw IllegalArgumentException("Class file not found: $path") 76 | val data = url.openStream().use { it.readBytes() } 77 | Pair.make(data, path.substringBeforeLast(".class")) 78 | } 79 | } 80 | 81 | override fun addJar(path: String) = throw UnsupportedOperationException() 82 | } 83 | 84 | val driver = CfrDriver.Builder() 85 | .withClassFileSource(classSource) 86 | .withOutputSink(sinkFactory) 87 | .withOptions(options) 88 | 89 | .build() 90 | driver.analyse(Collections.singletonList(internalName)) 91 | 92 | return result?.takeIf { it.isNotBlank() } ?: "No CFR output received" 93 | } catch (e: Throwable) { 94 | Logger.e("CFR decompilation error", e) 95 | val sw = StringWriter().also { e.printStackTrace(PrintWriter(it)) } 96 | return "Decompilation error: ${e.message}\n$sw" 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/theme/FileTypeMeta.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.theme 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.automirrored.outlined.Article 5 | import androidx.compose.material.icons.filled.Android 6 | import androidx.compose.material.icons.filled.Api 7 | import androidx.compose.material.icons.filled.Approval 8 | import androidx.compose.material.icons.filled.Book 9 | import androidx.compose.material.icons.filled.Class 10 | import androidx.compose.material.icons.filled.Code 11 | import androidx.compose.material.icons.filled.Directions 12 | import androidx.compose.material.icons.filled.FolderZip 13 | import androidx.compose.material.icons.filled.Memory 14 | import androidx.compose.material.icons.outlined.Code 15 | import androidx.compose.material.icons.outlined.Description 16 | import androidx.compose.material.icons.outlined.Image 17 | import androidx.compose.material.icons.outlined.Movie 18 | import androidx.compose.material.icons.outlined.MusicNote 19 | import androidx.compose.material.icons.outlined.PictureAsPdf 20 | import androidx.compose.material.icons.outlined.Slideshow 21 | import androidx.compose.material.icons.outlined.TableChart 22 | import androidx.compose.ui.graphics.Color 23 | import androidx.compose.ui.graphics.vector.ImageVector 24 | import com.materialkolor.ktx.darken 25 | import com.materialkolor.ktx.harmonize 26 | import me.lkl.dalvikus.ui.editor.suggestions.SuggestionType 27 | 28 | data class FileTypeMeta( 29 | val icon: ImageVector, 30 | val color: Color? = null 31 | ) 32 | 33 | fun getSuggestionTypeIcon(type: SuggestionType): ImageVector { 34 | return when (type) { 35 | SuggestionType.Instruction -> Icons.Default.Code 36 | SuggestionType.Directive -> Icons.Default.Directions 37 | SuggestionType.Register -> Icons.Default.Memory 38 | SuggestionType.Access -> Icons.Default.Api 39 | SuggestionType.LabelOrType -> Icons.Default.Class 40 | } 41 | } 42 | 43 | val readableImageFormats = listOf("png", "jpg", "jpeg", "bmp", "webp", "gif", "ico") 44 | 45 | fun getFileExtensionMeta(fileName: String): FileTypeMeta { 46 | when (fileName) { 47 | "AndroidManifest.xml" -> { 48 | return FileTypeMeta(Icons.Filled.Book, CodeBlue) 49 | } 50 | "resources.arsc" -> { 51 | return FileTypeMeta(Icons.Filled.Android, AndroidGreen) 52 | } 53 | "MANIFEST.MF" -> { 54 | return FileTypeMeta(Icons.Filled.Api, PackageOrange) 55 | } 56 | "CERT.RSA", "CERT.SF", "SIGNER.SF", "SIGNER.RSA" -> { 57 | return FileTypeMeta(Icons.Filled.Approval, PackageOrange) 58 | } 59 | else -> { 60 | val ext = fileName.substringAfterLast('.', "").lowercase() 61 | 62 | return when (ext) { 63 | "txt", "md", "log" -> FileTypeMeta(Icons.Outlined.Description) 64 | in readableImageFormats -> 65 | FileTypeMeta(Icons.Outlined.Image, ImagePurple) 66 | 67 | "mp3", "wav", "ogg", "flac", "aac" -> 68 | FileTypeMeta(Icons.Outlined.MusicNote, AudioTeal) 69 | 70 | "mp4", "avi", "mov", "mkv", "webm" -> 71 | FileTypeMeta(Icons.Outlined.Movie, VideoRed) 72 | 73 | "pdf" -> FileTypeMeta(Icons.Outlined.PictureAsPdf, PdfRed) 74 | "zip", "jar", "rar", "7z", "tar", "gz" -> 75 | FileTypeMeta(Icons.Filled.FolderZip, ArchiveGray) 76 | 77 | "doc", "docx" -> FileTypeMeta(Icons.AutoMirrored.Outlined.Article, WordBlue) 78 | "xls", "xlsx" -> FileTypeMeta(Icons.Outlined.TableChart, ExcelGreen) 79 | "ppt", "pptx" -> FileTypeMeta(Icons.Outlined.Slideshow, PowerPointOrange) 80 | "html", "xml", "json", "yaml", "yml" -> 81 | FileTypeMeta(Icons.Outlined.Code, CodeBlue) 82 | 83 | "apk", "apks", "aab", "xapk", "dex", "odex" -> 84 | FileTypeMeta(Icons.Filled.Android, AndroidGreen) 85 | 86 | else -> FileTypeMeta(Icons.Outlined.Description) 87 | } 88 | } 89 | } 90 | } 91 | /* TODO, don't harmonize with SeedColor, harmonize with the theme primary color instead. (different when dark) 92 | to do this, pass FileTypeMeta until inside composable, then define getColor() which is composable and only then harmonize. */ 93 | internal val AndroidGreen = Color(0xFF97AA4E).harmonize(SeedColor) 94 | internal val ArchiveGray = Color(0xFF878B87).darken().harmonize(SeedColor) 95 | 96 | internal val CodeBlue = Color(0xFF5C87B8).harmonize(SeedColor) 97 | internal val ImagePurple = Color(0xFF7A5A99).harmonize(SeedColor) 98 | internal val AudioTeal = Color(0xFF5E8E85).harmonize(SeedColor) 99 | internal val VideoRed = Color(0xFFB45757).harmonize(SeedColor) 100 | internal val PdfRed = Color(0xFF934444).harmonize(SeedColor) 101 | internal val WordBlue = Color(0xFF4A5FA8).harmonize(SeedColor) 102 | internal val ExcelGreen = Color(0xFF61895E).harmonize(SeedColor) 103 | internal val PowerPointOrange = Color(0xFFBA6E4E).harmonize(SeedColor) 104 | internal val PackageOrange = Color(0xFFC28852).harmonize(SeedColor) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tools/AdbDeployer.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tools 2 | 3 | import co.touchlab.kermit.Logger 4 | import com.android.ddmlib.AdbCommandRejectedException 5 | import com.android.ddmlib.AndroidDebugBridge 6 | import com.android.ddmlib.IDevice 7 | import com.android.ddmlib.InstallException 8 | import com.android.ddmlib.NullOutputReceiver 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.TimeoutCancellationException 11 | import kotlinx.coroutines.delay 12 | import kotlinx.coroutines.withContext 13 | import kotlinx.coroutines.withTimeout 14 | import me.lkl.dalvikus.dalvikusSettings 15 | import me.lkl.dalvikus.tree.backing.Backing 16 | import me.lkl.dalvikus.ui.snackbar.SnackbarManager 17 | import java.io.File 18 | import java.util.concurrent.TimeUnit 19 | 20 | class AdbDeployer(val snackbarManager: SnackbarManager) { 21 | 22 | suspend fun deployApk( 23 | apkBacking: Backing, 24 | packageName: String? = null, 25 | onFinish: (Boolean) -> Unit 26 | ) = withContext(Dispatchers.Default) { 27 | try { 28 | val bridge = initializeAdbBridge() 29 | if (!waitForConnection(bridge)) { 30 | throw IllegalStateException("Timeout while waiting for device connection.") 31 | } 32 | 33 | val devices = bridge.devices 34 | if (devices.isEmpty()) { 35 | throw IllegalStateException("No connected Android devices found.") 36 | } 37 | 38 | notifyDevicesFound(devices) 39 | 40 | val apk = apkBacking.getFileOrCreateTemp(".apk") 41 | installApkOnDevices(devices, apk) 42 | 43 | packageName?.let { 44 | launchAppOnDevices(devices, it) 45 | } ?: snackbarManager.showMessage("APK installed, but no package name provided to launch the app.") 46 | 47 | onFinish(true) 48 | } catch (timeout: TimeoutCancellationException) { 49 | Logger.e("Deployment timed out: ${timeout.message}", timeout) 50 | snackbarManager.showMessage("Operation timed out. Please check your device connection and try again.") 51 | onFinish(false) 52 | } catch (e: Exception) { 53 | Logger.e("Error deploying APK: ${e.message}", e) 54 | snackbarManager.showError(e) 55 | onFinish(false) 56 | } finally { 57 | cleanupAdbBridge() 58 | } 59 | } 60 | 61 | private fun initializeAdbBridge(): AndroidDebugBridge { 62 | AndroidDebugBridge.init(false) 63 | 64 | val adbExecutable = dalvikusSettings["adb_path"] as? File 65 | ?: throw IllegalStateException("ADB path is not set in settings.") 66 | 67 | if (!adbExecutable.exists() || !adbExecutable.canExecute()) { 68 | throw IllegalStateException("ADB executable not found or not executable: ${adbExecutable.absolutePath}.") 69 | } 70 | 71 | return AndroidDebugBridge.createBridge(adbExecutable.absolutePath, false, 10000, TimeUnit.MILLISECONDS) 72 | ?: throw IllegalStateException("Failed to create ADB bridge.") 73 | } 74 | 75 | private suspend fun waitForConnection(bridge: AndroidDebugBridge): Boolean { 76 | repeat(20) { // check every 500ms for 10s total 77 | if (bridge.isConnected && bridge.hasInitialDeviceList() && bridge.devices.isNotEmpty()) return true 78 | delay(500) 79 | } 80 | return false 81 | } 82 | 83 | private fun notifyDevicesFound(devices: Array) { 84 | val message = "Device(s) found: ${devices.joinToString { it.serialNumber }}" 85 | snackbarManager.showMessage(message) 86 | } 87 | 88 | private suspend fun installApkOnDevices(devices: Array, apk: File) { 89 | for (device in devices) { 90 | try { 91 | withTimeout(15_000) { 92 | device.installPackage(apk.absolutePath, true) 93 | } 94 | } catch (ie: InstallException) { 95 | val cause = ie.cause 96 | if (cause is AdbCommandRejectedException && !cause.isDeviceOffline) { 97 | snackbarManager.showMessage("Check for a confirmation dialog on the device.") 98 | } else { 99 | throw Exception("Failed to install APK on ${device.serialNumber}: ${ie.message}", cause ?: ie) 100 | } 101 | } 102 | } 103 | } 104 | 105 | private suspend fun launchAppOnDevices(devices: Array, packageName: String) { 106 | for (device in devices) { 107 | try { 108 | withTimeout(5_000) { 109 | val launchCommand = "monkey -p $packageName -c android.intent.category.LAUNCHER 1" 110 | device.executeShellCommand(launchCommand, NullOutputReceiver()) 111 | } 112 | } catch (e: Exception) { 113 | snackbarManager.showMessage("Failed to launch app on ${device.serialNumber}: ${e.message}") 114 | } 115 | } 116 | } 117 | 118 | private fun cleanupAdbBridge() { 119 | AndroidDebugBridge.disconnectBridge(1000, TimeUnit.MILLISECONDS) 120 | AndroidDebugBridge.terminate() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/dex/DexEntryClassNode.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree.dex 2 | 3 | import androidx.compose.material.icons.Icons 4 | import co.touchlab.kermit.Logger 5 | import com.android.tools.smali.baksmali.Adaptors.ClassDefinition 6 | import com.android.tools.smali.baksmali.formatter.BaksmaliWriter 7 | import com.android.tools.smali.dexlib2.Opcodes 8 | import com.android.tools.smali.dexlib2.iface.ClassDef 9 | import com.android.tools.smali.dexlib2.writer.builder.DexBuilder 10 | import com.android.tools.smali.smali.smaliFlexLexer 11 | import com.android.tools.smali.smali.smaliTreeWalker 12 | import io.github.composegears.valkyrie.DeployedCode 13 | import me.lkl.dalvikus.dalvikusSettings 14 | import me.lkl.dalvikus.smali.ErrorHandlingSmaliParser 15 | import me.lkl.dalvikus.tabs.SmaliTab 16 | import me.lkl.dalvikus.tabs.TabElement 17 | import me.lkl.dalvikus.tree.ContainerNode 18 | import me.lkl.dalvikus.tree.FileNode 19 | import me.lkl.dalvikus.tree.Metadata 20 | import org.antlr.runtime.CommonTokenStream 21 | import org.antlr.runtime.tree.CommonTree 22 | import org.antlr.runtime.tree.CommonTreeNodeStream 23 | import java.io.BufferedWriter 24 | import java.io.StringWriter 25 | 26 | class DexEntryClassNode( 27 | override var name: String, 28 | var fullPath: String, // com/example/MyClass 29 | val root: DexFileNode, 30 | override val parent: ContainerNode? 31 | ) : FileNode() { 32 | 33 | override val icon = Icons.Filled.DeployedCode 34 | 35 | fun getClassDef(): ClassDef = root.readEntry(fullPath) 36 | 37 | override suspend fun getContent(): ByteArray { 38 | val classDefinition = ClassDefinition(dalvikusSettings.baksmaliOptions, getClassDef()) 39 | 40 | val stringWriter = StringWriter() 41 | val bufferedWriter = BufferedWriter(stringWriter) 42 | 43 | val writer = BaksmaliWriter(bufferedWriter, getClassDef().type) 44 | 45 | classDefinition.writeTo(writer) 46 | 47 | bufferedWriter.flush() 48 | stringWriter.toString().let { content -> 49 | return content.toByteArray(Charsets.UTF_8) 50 | } 51 | } 52 | 53 | override suspend fun writeContent(newContent: ByteArray) { 54 | val apiLevel = dalvikusSettings["api_level"] as Int 55 | val lexer = smaliFlexLexer(newContent.inputStream().reader(), apiLevel) 56 | val tokens = CommonTokenStream(lexer) 57 | 58 | val parser = ErrorHandlingSmaliParser(tokens) 59 | parser.setVerboseErrors(true) 60 | parser.setAllowOdex(true) 61 | parser.setApiLevel(apiLevel) 62 | 63 | val result = parser.smali_file() 64 | 65 | if (parser.errorLines.isNotEmpty()) { 66 | Logger.e("Failed to parse smali content for assembly. " + 67 | "Lexer errors: ${lexer.numberOfSyntaxErrors}, " + 68 | "Parser errors: ${parser.numberOfSyntaxErrors}") 69 | 70 | throw AssemblyException(parser.errorLines, lexer.numberOfSyntaxErrors, parser.numberOfSyntaxErrors) 71 | } 72 | 73 | val t: CommonTree = result.getTree() 74 | 75 | val treeStream = CommonTreeNodeStream(t) 76 | treeStream.tokenStream = tokens 77 | 78 | val dexGen = smaliTreeWalker(treeStream) 79 | dexGen.setApiLevel(apiLevel) 80 | 81 | dexGen.setVerboseErrors(true) 82 | val dexBuilder = DexBuilder(Opcodes.forApi(apiLevel)) 83 | 84 | dexGen.setDexBuilder(dexBuilder) 85 | val classDef = dexGen.smali_file() 86 | 87 | root.updateEntry(path = fullPath, newClassDef = classDef) 88 | reflectChanges(classDef) 89 | } 90 | 91 | private suspend fun reflectChanges(newClassDef: ClassDef) { 92 | // Update the name and fullPath based on the classDef type 93 | val newType = newClassDef.type.removePrefix("L").removeSuffix(";") 94 | val newName = newType.substringAfterLast('/') 95 | if (newName != name) { 96 | name = newName 97 | fullPath = newType 98 | // TODO actually we would have to update the parent as well, but currently it is not important. 99 | } 100 | notifyChanged() 101 | } 102 | 103 | override suspend fun notifyChanged() { 104 | parent?.notifyChanged() 105 | } 106 | 107 | override fun getFileType(): String { 108 | return "smali" 109 | } 110 | 111 | override suspend fun createTab(): TabElement { 112 | return SmaliTab( 113 | tabId = fullPath, 114 | dexEntryClassNode = this 115 | ) 116 | } 117 | 118 | override fun getSizeEstimate(): Long = 8 * 1024 // 8 kB 119 | override fun isDisplayable(): Boolean = true 120 | override fun isEditable(): Boolean = true 121 | 122 | override fun getMetadata(): Set> { 123 | return setOf( 124 | Metadata.METHOD_COUNT to getClassDef().methods.count(), 125 | Metadata.FIELD_COUNT to getClassDef().fields.count() 126 | ) 127 | } 128 | } 129 | 130 | class AssemblyException( 131 | val errorLines: List, 132 | val lexerErrors: Int, 133 | val parserErrors: Int 134 | ) : Exception("Assembly failed with ${lexerErrors} lexer errors and ${parserErrors} parser errors. " + 135 | "Error lines: ${errorLines.joinToString(", ")}") -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/util/UICommons.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.util 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.expandVertically 5 | import androidx.compose.animation.fadeIn 6 | import androidx.compose.animation.fadeOut 7 | import androidx.compose.animation.shrinkVertically 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.* 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.ExpandLess 12 | import androidx.compose.material.icons.filled.ExpandMore 13 | import androidx.compose.material.icons.filled.Visibility 14 | import androidx.compose.material.icons.filled.VisibilityOff 15 | import androidx.compose.material3.* 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.graphics.vector.ImageVector 24 | import androidx.compose.ui.text.TextStyle 25 | import androidx.compose.ui.text.input.PasswordVisualTransformation 26 | import androidx.compose.ui.text.input.VisualTransformation 27 | import androidx.compose.ui.text.rememberTextMeasurer 28 | import androidx.compose.ui.unit.dp 29 | 30 | val CollapseCardMaxWidth = 600.dp 31 | 32 | @Composable 33 | internal fun CollapseCard( 34 | title: String, 35 | icon: ImageVector, 36 | modifier: Modifier = Modifier, 37 | defaultState : Boolean = false, 38 | content: @Composable () -> Unit 39 | ) { 40 | var expanded by remember { mutableStateOf(defaultState) } 41 | 42 | DefaultCard(modifier = modifier.sizeIn(maxWidth = CollapseCardMaxWidth)) { 43 | Column { 44 | Row( 45 | modifier = Modifier 46 | .fillMaxWidth() 47 | .clickable { expanded = !expanded } 48 | .padding(vertical = 16.dp, horizontal = 12.dp), 49 | verticalAlignment = Alignment.CenterVertically 50 | ) { 51 | Icon( 52 | imageVector = icon, 53 | contentDescription = null, 54 | modifier = Modifier.size(32.dp) 55 | ) 56 | Spacer(Modifier.width(8.dp)) 57 | Text( 58 | title, 59 | style = MaterialTheme.typography.titleLarge, 60 | modifier = Modifier.weight(1f) 61 | ) 62 | Icon( 63 | imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, 64 | contentDescription = if (expanded) "Collapse" else "Expand" 65 | ) 66 | } 67 | 68 | AnimatedVisibility( 69 | visible = expanded, 70 | enter = expandVertically() + fadeIn(), 71 | exit = shrinkVertically() + fadeOut() 72 | ) { 73 | Column(Modifier.padding(16.dp)) { 74 | content() 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | 82 | @Composable 83 | internal fun DefaultCard(modifier: Modifier = Modifier, content: @Composable () -> Unit) { 84 | Card( 85 | colors = CardDefaults.cardColors( 86 | containerColor = MaterialTheme.colorScheme.surfaceContainerLow 87 | ), 88 | modifier = modifier.fillMaxWidth().padding(8.dp), 89 | ) { 90 | content() 91 | } 92 | } 93 | 94 | @Composable 95 | fun PasswordField( 96 | password: String, 97 | onPasswordChange: (String) -> Unit, 98 | modifier: Modifier = Modifier, 99 | isError: Boolean = false, 100 | errorMessage: String? = null, 101 | ) { 102 | var passwordVisible by remember { mutableStateOf(false) } 103 | 104 | OutlinedTextField( 105 | value = password, 106 | onValueChange = onPasswordChange, 107 | modifier = modifier.fillMaxWidth(), 108 | isError = isError, 109 | singleLine = true, 110 | visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), 111 | trailingIcon = { 112 | val image = if (!passwordVisible) 113 | Icons.Default.Visibility 114 | else 115 | Icons.Default.VisibilityOff 116 | 117 | IconButton(onClick = { passwordVisible = !passwordVisible }) { 118 | Icon( 119 | imageVector = image, 120 | contentDescription = if (passwordVisible) "Hide password" else "Show password" 121 | ) 122 | } 123 | } 124 | ) 125 | if (isError && errorMessage != null) { 126 | Text( 127 | text = errorMessage, 128 | style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.error), 129 | modifier = Modifier.padding(start = 8.dp, top = 4.dp) 130 | ) 131 | } 132 | } 133 | 134 | @Composable 135 | fun getTextWidth(text: String, textStyle: TextStyle): Int { 136 | val textMeasurer = rememberTextMeasurer() 137 | 138 | val result = textMeasurer.measure( 139 | text = text, 140 | style = textStyle 141 | ) 142 | 143 | return result.size.width 144 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tools/SdkLocator.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tools 2 | 3 | import co.touchlab.kermit.Logger 4 | import java.io.File 5 | 6 | /** 7 | * Locates the Android SDK installation 8 | */ 9 | class AndroidSdkLocator { 10 | 11 | fun findSdkPath(): File? { 12 | // Try environment variables first 13 | listOf("ANDROID_HOME", "ANDROID_SDK_ROOT").forEach { envVar -> 14 | System.getenv(envVar)?.let { path -> 15 | val sdkDir = File(path) 16 | if (isValidSdk(sdkDir)) { 17 | Logger.i("Found Android SDK via $envVar: $path") 18 | return sdkDir 19 | } 20 | } 21 | } 22 | 23 | // Search common locations 24 | getCommonSdkPaths().forEach { path -> 25 | val sdkDir = File(path) 26 | if (isValidSdk(sdkDir)) { 27 | Logger.i("Found Android SDK at: $path") 28 | return sdkDir 29 | } 30 | } 31 | 32 | Logger.w("Android SDK not found") 33 | return null 34 | } 35 | 36 | private fun isValidSdk(dir: File): Boolean { 37 | return dir.exists() && dir.isDirectory && 38 | (File(dir, "platform-tools").exists() || File(dir, "build-tools").exists()) 39 | } 40 | 41 | private fun getCommonSdkPaths(): List { 42 | val userHome = System.getProperty("user.home") 43 | val os = System.getProperty("os.name").lowercase() 44 | 45 | return when { 46 | os.contains("win") -> listOf( 47 | "${System.getenv("LOCALAPPDATA")}\\Android\\Sdk", 48 | "$userHome\\AppData\\Local\\Android\\Sdk", 49 | "C:\\Android\\Sdk" 50 | ) 51 | os.contains("mac") || os.contains("darwin") -> listOf( 52 | "$userHome/Library/Android/sdk", 53 | "$userHome/Android/Sdk", 54 | "/usr/local/android-sdk" 55 | ) 56 | else -> listOf( // Linux 57 | "$userHome/Android/Sdk", 58 | "$userHome/android-sdk", 59 | "/usr/local/android-sdk" 60 | ) 61 | } 62 | } 63 | 64 | fun isSdkInstalled(): Boolean = findSdkPath() != null 65 | } 66 | 67 | /** 68 | * Locates Android tools (adb, zipalign) 69 | */ 70 | class AndroidToolsLocator(private val sdkLocator: AndroidSdkLocator = AndroidSdkLocator()) { 71 | 72 | fun findAdb(): File? = findTool("adb", "platform-tools") 73 | 74 | fun findZipalign(): File? = findTool("zipalign", "build-tools") 75 | 76 | private fun findTool(toolName: String, sdkSubdir: String): File? { 77 | val isWindows = System.getProperty("os.name").lowercase().contains("win") 78 | val execName = if (isWindows) "$toolName.exe" else toolName 79 | 80 | // 1. Try Android SDK 81 | sdkLocator.findSdkPath()?.let { sdkPath -> 82 | val toolsDir = File(sdkPath, sdkSubdir) 83 | 84 | if (sdkSubdir == "build-tools") { 85 | // Find latest build-tools version 86 | toolsDir.listFiles() 87 | ?.filter { it.isDirectory } 88 | ?.maxByOrNull { it.name } 89 | ?.let { versionDir -> 90 | val tool = File(versionDir, execName) 91 | if (tool.exists() && tool.canExecute()) { 92 | Logger.i("Found $toolName in build-tools ${versionDir.name}") 93 | return tool 94 | } 95 | } 96 | } else { 97 | // Direct lookup in platform-tools 98 | val tool = File(toolsDir, execName) 99 | if (tool.exists() && tool.canExecute()) { 100 | Logger.i("Found $toolName in $sdkSubdir") 101 | return tool 102 | } 103 | } 104 | } 105 | 106 | // 2. Try system PATH 107 | findInPath(execName)?.let { 108 | Logger.i("Found $toolName in PATH") 109 | return it 110 | } 111 | 112 | // 3. Try common system locations 113 | getCommonToolPaths().forEach { path -> 114 | val tool = File(path, execName) 115 | if (tool.exists() && tool.canExecute()) { 116 | Logger.i("Found $toolName at: $path") 117 | return tool 118 | } 119 | } 120 | 121 | Logger.w("$toolName not found") 122 | return null 123 | } 124 | 125 | private fun findInPath(execName: String): File? { 126 | val pathEnv = System.getenv("PATH") ?: return null 127 | val separator = if (System.getProperty("os.name").lowercase().contains("win")) ";" else ":" 128 | 129 | return pathEnv.split(separator) 130 | .map { File(it.trim(), execName) } 131 | .firstOrNull { it.exists() && it.canExecute() } 132 | } 133 | 134 | private fun getCommonToolPaths(): List { 135 | val os = System.getProperty("os.name").lowercase() 136 | 137 | return when { 138 | os.contains("win") -> listOf( 139 | "${System.getenv("LOCALAPPDATA")}\\Android\\Sdk\\platform-tools", 140 | "C:\\Android\\platform-tools" 141 | ) 142 | os.contains("mac") || os.contains("darwin") -> listOf( 143 | "/usr/local/bin", 144 | "/opt/homebrew/bin" 145 | ) 146 | else -> listOf( // Linux 147 | "/usr/bin", 148 | "/usr/local/bin" 149 | ) 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tools/ApkSigner.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tools 2 | 3 | import co.touchlab.kermit.Logger 4 | import com.android.apksig.ApkSigner 5 | import com.android.apksig.ApkVerifier 6 | import com.android.apksig.KeyConfig 7 | import kotlinx.coroutines.* 8 | import me.lkl.dalvikus.dalvikusSettings 9 | import me.lkl.dalvikus.tree.backing.Backing 10 | import me.lkl.dalvikus.ui.packaging.KeystoreInfo 11 | import me.lkl.dalvikus.ui.snackbar.SnackbarManager 12 | import java.io.File 13 | import java.security.KeyStore 14 | import java.security.MessageDigest 15 | import java.security.PrivateKey 16 | import java.security.cert.X509Certificate 17 | 18 | class ApkSigner(val snackbarManager: SnackbarManager) { 19 | suspend fun checkSignature(apk: File): ApkVerifier.Result? = withContext(Dispatchers.IO) { 20 | try { 21 | val verifier = ApkVerifier.Builder(apk).build() 22 | val verify = verifier.verify() 23 | Logger.i("APK signature verification result for ${apk.absolutePath}: ${verify.isVerified}, errors: ${verify.errors}") 24 | verify 25 | } catch (e: Exception) { 26 | Logger.e("Error verifying APK signature: ${e.message}", e) 27 | snackbarManager.showError(e) 28 | null 29 | } 30 | } 31 | 32 | suspend fun signApk( 33 | keystoreInfo: KeystoreInfo, 34 | outputApkBacking: Backing, 35 | apkBacking: Backing, 36 | onFinish: (success: Boolean) -> Unit 37 | ) = withContext(Dispatchers.Default) { 38 | try { 39 | val keystoreFile = keystoreInfo.keystoreFile 40 | val keyAlias = keystoreInfo.keyAlias 41 | val keystorePassword = keystoreInfo.keystorePassword 42 | 43 | val ks = KeyStore.getInstance("JKS").apply { 44 | load(keystoreFile.inputStream(), keystorePassword) 45 | } 46 | 47 | val privateKey = ks.getKey(keyAlias, keystorePassword) as? PrivateKey 48 | ?: throw IllegalArgumentException("Private key not found or wrong password") 49 | 50 | val certs = ks.getCertificateChain(keyAlias) 51 | ?.map { it as X509Certificate } 52 | ?.toTypedArray() 53 | ?: throw IllegalArgumentException("Certificate chain missing") 54 | 55 | val apk = apkBacking.getFileOrCreateTemp(".apk") 56 | zipAlignIfNeeded(apk) 57 | 58 | val tempOutputApk = File.createTempFile("signed_", ".apk") 59 | 60 | val signer = ApkSigner.Builder( 61 | listOf( 62 | ApkSigner.SignerConfig.Builder( 63 | "signer", 64 | KeyConfig.Jca(privateKey), 65 | listOf(*certs) 66 | ).build() 67 | ) 68 | ) 69 | .setInputApk(apk) 70 | .setOutputApk(tempOutputApk) 71 | .setV1SigningEnabled(true) 72 | .setV2SigningEnabled(true) 73 | .setV3SigningEnabled(true) 74 | .build() 75 | 76 | signer.sign() 77 | 78 | val verifier = ApkVerifier.Builder(tempOutputApk).build() 79 | val result = verifier.verify() 80 | 81 | if (result.isVerified) { 82 | outputApkBacking.write(tempOutputApk.readBytes()) 83 | onFinish(true) 84 | } else { 85 | Logger.e("APK signature verification failed: ${result.errors}") 86 | onFinish(false) 87 | } 88 | 89 | if (tempOutputApk.exists()) { 90 | tempOutputApk.delete() 91 | } 92 | } catch (e: Exception) { 93 | Logger.e("Error signing APK: ${e.message}", e) 94 | snackbarManager?.showError(e) 95 | onFinish(false) 96 | } 97 | } 98 | 99 | 100 | private suspend fun zipAlignIfNeeded(apk: File) { 101 | val zipAlignPath = dalvikusSettings["zipalign_path"] as? File 102 | ?: throw IllegalStateException("Zipalign path is not set in settings.") 103 | 104 | if (!zipAlignPath.exists() || !zipAlignPath.canExecute()) { 105 | throw IllegalStateException("Zipalign executable not found or not executable: ${zipAlignPath.absolutePath}.") 106 | } 107 | 108 | val alignedApk = File.createTempFile("aligned", ".apk", apk.parentFile) 109 | 110 | try { 111 | withTimeout(10_000) { 112 | val command = listOf( 113 | zipAlignPath.absolutePath, 114 | "-v", "-f", 115 | "4", 116 | apk.absolutePath, 117 | alignedApk.absolutePath 118 | ) 119 | val process = ProcessBuilder(command) 120 | .redirectErrorStream(true) 121 | .start() 122 | 123 | val output = process.inputStream.bufferedReader().use { it.readText() } 124 | val result = process.waitFor() 125 | 126 | if (result != 0) { 127 | throw RuntimeException("zipalign failed: $output") 128 | } 129 | } 130 | 131 | Logger.i("APK zipaligned successfully: ${alignedApk.absolutePath}") 132 | 133 | alignedApk.copyTo(apk, overwrite = true) 134 | alignedApk.delete() 135 | } catch (e: Exception) { 136 | Logger.e("Failed to zipalign APK: ${e.message}", e) 137 | snackbarManager.showError(e) 138 | } 139 | } 140 | } 141 | 142 | fun ByteArray.sha256Fingerprint(): String { 143 | val digest = MessageDigest.getInstance("SHA-256").digest(this) 144 | return digest.joinToString(":") { "%02X".format(it) } 145 | } -------------------------------------------------------------------------------- /composeApp/src/jvmMain/kotlin/me/lkl/dalvikus/lexer/JavaHighlightParserVisitor.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.lexer 2 | 3 | import me.lkl.dalvikus.lexer.java.JavaParser 4 | import me.lkl.dalvikus.lexer.java.JavaParserBaseVisitor 5 | import org.antlr.v4.runtime.ParserRuleContext 6 | import org.antlr.v4.runtime.tree.TerminalNode 7 | 8 | class JavaHighlightParserVisitor : JavaParserBaseVisitor() { 9 | val methodNames = mutableSetOf>() 10 | val classNames = mutableSetOf>() 11 | val fieldNames = mutableSetOf>() 12 | 13 | // Method declarations and invocations 14 | override fun visitMethodDeclaration(ctx: JavaParser.MethodDeclarationContext) { 15 | ctx.identifier()?.let { identifier -> 16 | addRange(methodNames, identifier) 17 | } 18 | 19 | // Handle throws clauses 20 | ctx.qualifiedNameList()?.qualifiedName()?.forEach { name -> 21 | addRange(classNames, name) 22 | } 23 | 24 | super.visitMethodDeclaration(ctx) 25 | } 26 | 27 | override fun visitConstructorDeclaration(ctx: JavaParser.ConstructorDeclarationContext) { 28 | ctx.identifier()?.let { identifier -> 29 | addRange(methodNames, identifier) 30 | } 31 | 32 | ctx.qualifiedNameList()?.qualifiedName()?.forEach { name -> 33 | addRange(classNames, name) 34 | } 35 | 36 | super.visitConstructorDeclaration(ctx) 37 | } 38 | 39 | override fun visitMethodCall(ctx: JavaParser.MethodCallContext) { 40 | when { 41 | ctx.identifier() != null -> { 42 | addRange(methodNames, ctx.identifier()!!) 43 | } 44 | ctx.THIS() != null -> addRange(methodNames, ctx.THIS()!!) 45 | ctx.SUPER() != null -> addRange(methodNames, ctx.SUPER()!!) 46 | } 47 | super.visitMethodCall(ctx) 48 | } 49 | 50 | override fun visitInterfaceCommonBodyDeclaration(ctx: JavaParser.InterfaceCommonBodyDeclarationContext) { 51 | ctx.identifier()?.let { identifier -> 52 | addRange(methodNames, identifier) 53 | } 54 | ctx.qualifiedNameList()?.qualifiedName()?.forEach { name -> 55 | addRange(classNames, name) 56 | } 57 | return super.visitInterfaceCommonBodyDeclaration(ctx) 58 | } 59 | 60 | // Class/interface/record/enum declarations and references 61 | override fun visitClassDeclaration(ctx: JavaParser.ClassDeclarationContext) { 62 | ctx.identifier()?.let { identifier -> 63 | addRange(classNames, identifier) 64 | } 65 | super.visitClassDeclaration(ctx) 66 | } 67 | 68 | override fun visitInterfaceDeclaration(ctx: JavaParser.InterfaceDeclarationContext) { 69 | ctx.identifier()?.let { identifier -> 70 | addRange(classNames, identifier) 71 | } 72 | super.visitInterfaceDeclaration(ctx) 73 | } 74 | 75 | override fun visitRecordDeclaration(ctx: JavaParser.RecordDeclarationContext) { 76 | ctx.identifier()?.let { identifier -> 77 | addRange(classNames, identifier) 78 | } 79 | super.visitRecordDeclaration(ctx) 80 | } 81 | 82 | override fun visitEnumDeclaration(ctx: JavaParser.EnumDeclarationContext) { 83 | ctx.identifier()?.let { identifier -> 84 | addRange(classNames, identifier) 85 | } 86 | super.visitEnumDeclaration(ctx) 87 | } 88 | 89 | override fun visitClassOrInterfaceType(ctx: JavaParser.ClassOrInterfaceTypeContext) { 90 | ctx.typeIdentifier()?.let { identifier -> 91 | addRange(classNames, identifier) 92 | } 93 | super.visitClassOrInterfaceType(ctx) 94 | } 95 | 96 | override fun visitCreator(ctx: JavaParser.CreatorContext) { 97 | ctx.createdName()?.identifier()?.forEach { identifier -> 98 | addRange(classNames, identifier) 99 | } 100 | super.visitCreator(ctx) 101 | } 102 | 103 | // Field declarations and accesses 104 | override fun visitFieldDeclaration(ctx: JavaParser.FieldDeclarationContext) { 105 | ctx.variableDeclarators().variableDeclarator().forEach { declarator -> 106 | declarator.variableDeclaratorId().identifier()?.let { identifier -> 107 | addRange(fieldNames, identifier) 108 | } 109 | } 110 | super.visitFieldDeclaration(ctx) 111 | } 112 | 113 | override fun visitMemberReferenceExpression(ctx: JavaParser.MemberReferenceExpressionContext) { 114 | when { 115 | ctx.methodCall() != null -> { 116 | // This is a method call, handled elsewhere 117 | } 118 | ctx.identifier() != null -> { 119 | addRange(fieldNames, ctx.identifier()!!) 120 | } 121 | } 122 | super.visitMemberReferenceExpression(ctx) 123 | } 124 | 125 | // Helper functions 126 | private fun addRange(collection: MutableSet>, node: TerminalNode) { 127 | val start = node.symbol.startIndex 128 | val end = node.symbol.stopIndex + 1 129 | collection.add(Pair(start, end)) 130 | } 131 | 132 | private fun addRange(collection: MutableSet>, ctx: ParserRuleContext) { 133 | val start = ctx.start.startIndex 134 | val end = ctx.stop.stopIndex + 1 135 | collection.add(Pair(start, end)) 136 | } 137 | 138 | private fun isClassified(identifier: JavaParser.IdentifierContext): Boolean { 139 | val start = identifier.start.startIndex 140 | val end = identifier.stop.stopIndex + 1 141 | return methodNames.any { it.first == start && it.second == end } || 142 | classNames.any { it.first == start && it.second == end } || 143 | fieldNames.any { it.first == start && it.second == end } 144 | } 145 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/Node.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.tree 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.vector.ImageVector 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.StateFlow 7 | import kotlinx.coroutines.flow.asStateFlow 8 | import me.lkl.dalvikus.tabs.contentprovider.ContentProvider 9 | import me.lkl.dalvikus.tabs.TabElement 10 | import me.lkl.dalvikus.tree.root.HiddenRoot 11 | import me.lkl.dalvikus.theme.getFileExtensionMeta 12 | 13 | enum class Metadata { 14 | FILE_SIZE, 15 | LAST_EDITED, 16 | METHOD_COUNT, 17 | FIELD_COUNT, 18 | } 19 | 20 | sealed interface Node { 21 | val name: String 22 | val icon: ImageVector // path or identifier 23 | val parent: ContainerNode? 24 | 25 | val isRoot: Boolean 26 | get() = parent == null 27 | 28 | val color: Color? 29 | get() = getFileExtensionMeta(name).color // Default color, can be overridden 30 | 31 | suspend fun notifyChanged() 32 | 33 | fun getMetadata(): Set> { 34 | return setOf() 35 | } 36 | } 37 | 38 | abstract class ContainerNode : Node { 39 | protected val _childrenFlow = MutableStateFlow>(emptyList()) 40 | val childrenFlow: StateFlow> = _childrenFlow.asStateFlow() 41 | abstract val changesWithChildren: Boolean 42 | 43 | protected abstract suspend fun loadChildrenInternal(): List 44 | 45 | suspend fun loadChildren() { 46 | val rawChildren = loadChildrenInternal() 47 | _childrenFlow.value = rawChildren.sortedTree() 48 | } 49 | 50 | open suspend fun replaceChild(old: Node, new: Node) { 51 | val updated = _childrenFlow.value.map { if (it == old) new else it } 52 | _childrenFlow.value = updated 53 | onChildChanged(new) 54 | } 55 | 56 | protected open suspend fun onChildChanged(child: Node) { 57 | // Default behavior: propagate upward 58 | if (changesWithChildren) { 59 | rebuild() 60 | } 61 | parent?.notifyChanged() 62 | } 63 | 64 | override suspend fun notifyChanged() { 65 | rebuild() 66 | parent?.notifyChanged() 67 | } 68 | 69 | protected abstract suspend fun rebuild() 70 | 71 | suspend fun resolveChildrenPath(fullPath: String): Node? { 72 | val parts = fullPath.split('/') 73 | if (parts.isEmpty()) return null 74 | 75 | var current: Node? = this 76 | for (part in parts) { 77 | if (current is ContainerNode) { 78 | current.loadChildren() 79 | current = current.childrenFlow.value.find { it.name == part } 80 | } else { 81 | return null // Not a container, can't resolve further 82 | } 83 | } 84 | return current 85 | } 86 | } 87 | 88 | abstract class FileNode : Node, ContentProvider() { 89 | override suspend fun loadContent() { 90 | _contentFlow.value = getContent() 91 | } 92 | 93 | override suspend fun updateContent(newContent: ByteArray) { 94 | if (!isEditable()) { 95 | throw UnsupportedOperationException("This file is not editable") 96 | } 97 | _contentFlow.value = newContent 98 | writeContent(newContent) 99 | parent?.notifyChanged() 100 | } 101 | 102 | protected abstract suspend fun getContent(): ByteArray 103 | protected abstract suspend fun writeContent(newContent: ByteArray) 104 | 105 | override suspend fun notifyChanged() { 106 | parent?.notifyChanged() 107 | } 108 | 109 | override fun getFileType(): String { 110 | return name.substringAfterLast(".").lowercase() 111 | } 112 | 113 | override fun getSourcePath(): String? { 114 | return this.getPathHistory() 115 | } 116 | 117 | abstract suspend fun createTab(): TabElement 118 | 119 | } 120 | 121 | typealias PathMap = Map 122 | 123 | fun buildChildNodes( 124 | entries: PathMap, 125 | prefix: String, 126 | onFolder: (folderName: String, fullPath: String) -> Node, 127 | onFile: (fileName: String, fullPath: String, value: T) -> Node 128 | ): List { 129 | val folders = mutableSetOf() 130 | val children = mutableListOf() 131 | 132 | for ((fullPath, value) in entries) { 133 | if (!fullPath.startsWith(prefix)) continue 134 | 135 | val relative = fullPath.removePrefix(prefix) 136 | val parts = relative.split('/') 137 | 138 | when { 139 | parts.size == 1 -> { 140 | // Direct file 141 | children.add(onFile(parts[0], fullPath, value)) 142 | } 143 | parts.size > 1 -> { 144 | // Nested folder 145 | val folderName = parts[0] 146 | if (folders.add(folderName)) { 147 | val folderPath = if (prefix.isEmpty()) "$folderName/" else "$prefix$folderName/" 148 | children.add(onFolder(folderName, folderPath)) 149 | } 150 | } 151 | } 152 | } 153 | 154 | return children.sortedBy { it.name } 155 | } 156 | 157 | 158 | fun List.sortedTree(): List { 159 | return sortedWith( 160 | compareBy { it !is ContainerNode } // folders first (false < true) 161 | .thenBy { it.name.lowercase() } // then alphabetical (case-insensitive) 162 | ) 163 | } 164 | 165 | /** 166 | * Concats all parent names into a path-like string. 167 | */ 168 | fun Node.getPathHistory(): String { 169 | val path = mutableListOf() 170 | var current: Node? = this 171 | while (current != null && current !is HiddenRoot) { 172 | path.add(current.name) 173 | current = current.parent 174 | } 175 | return path.reversed().joinToString(separator = "/") 176 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/LeftPanel.kt: -------------------------------------------------------------------------------- 1 | package me.lkl.dalvikus.ui 2 | 3 | import androidx.compose.foundation.draganddrop.dragAndDropTarget 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.Add 7 | import androidx.compose.material3.* 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.ExperimentalComposeUiApi 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.unit.dp 14 | import dalvikus.composeapp.generated.resources.* 15 | import kotlinx.coroutines.launch 16 | import me.lkl.dalvikus.LocalSnackbarManager 17 | import me.lkl.dalvikus.tabManager 18 | import me.lkl.dalvikus.tree.* 19 | import me.lkl.dalvikus.tree.filesystem.FileSystemFileNode 20 | import me.lkl.dalvikus.tree.root.HiddenRoot 21 | import me.lkl.dalvikus.errorreport.crtExHandler 22 | import me.lkl.dalvikus.selectedNavItem 23 | import me.lkl.dalvikus.ui.snackbar.SnackbarManager 24 | import me.lkl.dalvikus.ui.tree.FileSelectorDialog 25 | import me.lkl.dalvikus.ui.tree.TreeDragAndDropTarget 26 | import me.lkl.dalvikus.ui.tree.TreeView 27 | import org.jetbrains.compose.resources.stringResource 28 | import java.io.File 29 | 30 | val editableFiles = listOf("apk", "dex", "odex", "apks", "aab", "jar", "zip", "xapk") 31 | 32 | var showTreeAddFileDialog by mutableStateOf(false) 33 | 34 | internal val uiTreeRoot: HiddenRoot = HiddenRoot( 35 | /*ApkNode( 36 | "sample.apk", 37 | File("/home/admin/Downloads/sample.apk"), null 38 | )*/ 39 | ) 40 | internal var currentSelection by mutableStateOf(null) 41 | internal var scrollAndExpandSelection = mutableStateOf(false) 42 | 43 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) 44 | @Composable 45 | internal fun LeftPanelContent() { 46 | val snackbarManager = LocalSnackbarManager.current 47 | val unsupportedFileText = stringResource(Res.string.tree_unsupported_file_type_msg) 48 | if (showTreeAddFileDialog) { 49 | FileSelectorDialog( 50 | title = stringResource(Res.string.dialog_select_android_archive_title), 51 | message = stringResource(Res.string.dialog_select_android_archive_message), 52 | filePredicate = { it is FileSystemFileNode && !it.file.isDirectory && it.file.extension in editableFiles }, 53 | onDismissRequest = { 54 | showTreeAddFileDialog = false 55 | }) { node -> 56 | if (node !is FileSystemFileNode) return@FileSelectorDialog 57 | addFileToTree(node.file, snackbarManager, unsupportedFileText) 58 | showTreeAddFileDialog = false 59 | } 60 | } 61 | val scope = rememberCoroutineScope() 62 | val dragAndDropTarget = remember(snackbarManager) { TreeDragAndDropTarget(snackbarManager, unsupportedFileText) } 63 | 64 | Scaffold( 65 | containerColor = Color.Transparent, 66 | floatingActionButton = { 67 | ExtendedFloatingActionButton( 68 | onClick = { showTreeAddFileDialog = true }, 69 | icon = { 70 | Icon(Icons.Default.Add, contentDescription = stringResource(Res.string.fab_load_file)) 71 | }, 72 | text = { 73 | Text(stringResource(Res.string.fab_load_file), maxLines = 1) 74 | } 75 | ) 76 | } 77 | ) { innerPadding -> 78 | val rootKids = uiTreeRoot.childrenFlow.collectAsState() 79 | Column( 80 | modifier = Modifier.fillMaxSize().padding(innerPadding) 81 | .dragAndDropTarget( 82 | shouldStartDragAndDrop = { true }, 83 | target = dragAndDropTarget 84 | ), 85 | horizontalAlignment = Alignment.CenterHorizontally 86 | ) { 87 | if (rootKids.value.isEmpty()) { 88 | Box( 89 | modifier = Modifier.fillMaxSize(), 90 | contentAlignment = Alignment.Center 91 | ) { 92 | Text( 93 | stringResource(Res.string.tree_drop_of_type_placeholder, editableFiles.joinToString(", ")), 94 | style = MaterialTheme.typography.bodySmall, 95 | color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), 96 | modifier = Modifier.padding(16.dp) 97 | ) 98 | } 99 | return@Scaffold 100 | } 101 | TreeView( 102 | uiTreeRoot, 103 | onFileSelected = { node -> 104 | currentSelection = node 105 | 106 | if (node is FileNode) { 107 | 108 | if(node.name.equals("resources.arsc", true)) { 109 | selectedNavItem = "Resources" 110 | return@TreeView 111 | } 112 | 113 | scope.launch(crtExHandler) { 114 | val newTab = node.createTab() 115 | tabManager.addOrSelectTab(newTab) 116 | } 117 | } 118 | }, 119 | selectedElement = currentSelection, 120 | scrollAndExpandSelection = scrollAndExpandSelection 121 | ) 122 | } 123 | } 124 | } 125 | 126 | fun addFileToTree(file: File, snackbarManager: SnackbarManager, unsupportedFileText: String) { 127 | val node = NodeFactory.createNode(file, uiTreeRoot) 128 | if (node !is FileSystemFileNode) { 129 | uiTreeRoot.addChild(node) 130 | } else { 131 | snackbarManager.showMessage(unsupportedFileText) 132 | } 133 | } 134 | 135 | fun selectFileTreeNode(node: Node) { 136 | currentSelection = node 137 | scrollAndExpandSelection.value = true 138 | } --------------------------------------------------------------------------------