16 |
17 |  Smali editor with code completion. |
18 |  Comfortable dark theme and understandable errors. |
19 |
20 |
21 |  Seamless multi decompiler integration. |
22 |  Sign and deploy edited applications directly. |
23 |
24 |
25 |  Browse app resources. |
26 |  Search in many different ways. |
27 |
28 |
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