├── .github └── workflows │ ├── build-apk.yml │ └── unit-tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── libs │ ├── jadx │ │ ├── android-29-clst.jar │ │ ├── jadx-core-v1.2.0.jar │ │ ├── jadx-dex-input-v1.2.0.jar │ │ └── jadx-plugins-api-v1.2.0.jar │ └── procyon │ │ └── javax-model.jar ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── d2j │ │ │ └── Hex.clazz │ │ └── rt.jar │ ├── java │ │ └── ma │ │ │ └── dexter │ │ │ └── ui │ │ │ └── tree │ │ │ ├── TreeNode.java │ │ │ ├── TreeView.java │ │ │ ├── TreeViewAdapter.java │ │ │ ├── base │ │ │ ├── BaseNodeViewBinder.java │ │ │ ├── BaseNodeViewFactory.java │ │ │ ├── BaseTreeAction.java │ │ │ ├── CheckableNodeViewBinder.java │ │ │ └── SelectableTreeAction.java │ │ │ └── helper │ │ │ └── TreeHelper.java │ ├── kotlin │ │ └── ma │ │ │ └── dexter │ │ │ ├── App.kt │ │ │ ├── api │ │ │ └── consumer │ │ │ │ ├── ClassConversionConsumer.kt │ │ │ │ └── ClassProgressConsumer.kt │ │ │ ├── dex │ │ │ ├── DexFactory.kt │ │ │ ├── DexGotoManager.kt │ │ │ ├── MutableClassDef.kt │ │ │ ├── MutableDexContainer.kt │ │ │ ├── MutableDexFile.kt │ │ │ └── SmaliContainer.kt │ │ │ ├── model │ │ │ └── GotoDefs.kt │ │ │ ├── parsers │ │ │ ├── java │ │ │ │ ├── JavaModels.kt │ │ │ │ └── JavaParser.kt │ │ │ └── smali │ │ │ │ ├── SmaliModels.kt │ │ │ │ └── SmaliParser.kt │ │ │ ├── project │ │ │ ├── DexProject.kt │ │ │ ├── Project.kt │ │ │ └── Workspace.kt │ │ │ ├── tasks │ │ │ ├── BaksmaliDexTask.kt │ │ │ ├── BaksmaliTask.kt │ │ │ ├── D2JTask.kt │ │ │ ├── MergeDexTask.kt │ │ │ ├── SaveDexTask.kt │ │ │ ├── Smali2JavaTask.kt │ │ │ ├── SmaliTask.kt │ │ │ ├── Tasks.kt │ │ │ └── Util.kt │ │ │ ├── tools │ │ │ ├── d2j │ │ │ │ ├── D2JExceptionHandler.kt │ │ │ │ ├── D2JFacade.kt │ │ │ │ ├── D2JInvoker.kt │ │ │ │ └── D2JOptions.kt │ │ │ ├── decompilers │ │ │ │ ├── BaseDecompiler.kt │ │ │ │ ├── BaseDexDecompiler.kt │ │ │ │ ├── BaseJarDecompiler.kt │ │ │ │ ├── cfr │ │ │ │ │ ├── CFRDecompiler.kt │ │ │ │ │ ├── CFRJarSource.kt │ │ │ │ │ └── CFROutputSink.kt │ │ │ │ ├── fernflower │ │ │ │ │ ├── FFBytecodeProvider.kt │ │ │ │ │ ├── FFResultSaver.kt │ │ │ │ │ └── FernflowerDecompiler.kt │ │ │ │ ├── jadx │ │ │ │ │ └── JADXDecompiler.kt │ │ │ │ ├── jdcore │ │ │ │ │ ├── JDCoreDecompiler.kt │ │ │ │ │ ├── JDLoader.kt │ │ │ │ │ └── JDPrinter.kt │ │ │ │ └── procyon │ │ │ │ │ └── ProcyonDecompiler.kt │ │ │ ├── jar │ │ │ │ └── JarTool.kt │ │ │ └── smali │ │ │ │ ├── BaksmaliInvoker.kt │ │ │ │ ├── SmaliInvoker.kt │ │ │ │ └── catcherr │ │ │ │ ├── SyntaxError.kt │ │ │ │ ├── smaliCatchErrFlexLexer.kt │ │ │ │ ├── smaliCatchErrParser.kt │ │ │ │ └── smaliCatchErrTreeWalker.kt │ │ │ ├── ui │ │ │ ├── BaseActivity.kt │ │ │ ├── BaseFragment.kt │ │ │ ├── activity │ │ │ │ └── MainActivity.kt │ │ │ ├── adapter │ │ │ │ └── DexPagerAdapter.kt │ │ │ ├── dialog │ │ │ │ └── ProgressDialog.kt │ │ │ ├── editor │ │ │ │ ├── lang │ │ │ │ │ └── smali │ │ │ │ │ │ ├── SmaliAnalyzer.kt │ │ │ │ │ │ ├── SmaliAutoCompleteProvider.kt │ │ │ │ │ │ ├── SmaliLanguage.kt │ │ │ │ │ │ └── model │ │ │ │ │ │ └── SmaliAutoCompleteModels.kt │ │ │ │ ├── scheme │ │ │ │ │ └── smali │ │ │ │ │ │ ├── SchemeLightSmali.kt │ │ │ │ │ │ └── SmaliBaseScheme.kt │ │ │ │ └── util │ │ │ │ │ ├── SingleEditorEventListener.kt │ │ │ │ │ ├── Util.kt │ │ │ │ │ └── smali │ │ │ │ │ └── SmaliActionPopupWindow.kt │ │ │ ├── fragment │ │ │ │ ├── BaseCodeEditorFragment.kt │ │ │ │ ├── DexEditorFragment.kt │ │ │ │ ├── JavaViewerFragment.kt │ │ │ │ └── SmaliEditorFragment.kt │ │ │ ├── model │ │ │ │ └── DexPageItems.kt │ │ │ ├── tree │ │ │ │ ├── TreeUtil.kt │ │ │ │ └── dex │ │ │ │ │ ├── DexClassNode.kt │ │ │ │ │ ├── SmaliTree.kt │ │ │ │ │ └── binder │ │ │ │ │ ├── DexItemNodeViewBinder.kt │ │ │ │ │ └── DexItemNodeViewFactory.kt │ │ │ ├── util │ │ │ │ ├── PopupMenuUtils.kt │ │ │ │ └── UiUtil.kt │ │ │ └── viewmodel │ │ │ │ └── MainViewModel.kt │ │ │ └── util │ │ │ ├── AndroidUtils.kt │ │ │ ├── AssetUtils.kt │ │ │ ├── DexUtils.kt │ │ │ ├── FileUtils.kt │ │ │ ├── LogUtils.kt │ │ │ └── SmaliUtils.kt │ └── res │ │ ├── drawable-xhdpi │ │ └── ic_launcher.png │ │ ├── drawable │ │ ├── ic_baseline_arrow_forward_ios_24.xml │ │ ├── ic_baseline_arrow_right_alt_20.xml │ │ ├── ic_baseline_copyright_24.xml │ │ ├── ic_baseline_home_24.xml │ │ ├── ic_baseline_more_vert_24.xml │ │ ├── ic_baseline_redo_24.xml │ │ ├── ic_baseline_save_24.xml │ │ ├── ic_baseline_save_alt_24.xml │ │ ├── ic_baseline_search_24.xml │ │ ├── ic_baseline_undo_24.xml │ │ ├── ic_baseline_view_stream_24.xml │ │ ├── ic_baseline_wrap_text_24.xml │ │ ├── ic_baseline_zoom_in_24.xml │ │ ├── ic_java.xml │ │ └── ic_letter_s_24.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── dialog_create_smali_file.xml │ │ ├── dialog_progress_simple.xml │ │ ├── fragment_base_code_editor.xml │ │ ├── fragment_dex_editor.xml │ │ ├── item_dex_tree_node.xml │ │ └── popup_menu_item_checkable.xml │ │ ├── menu │ │ ├── menu_base_code_editor.xml │ │ ├── menu_dex_tree_item.xml │ │ └── menu_main.xml │ │ ├── values-night │ │ ├── colors.xml │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ └── test │ ├── kotlin │ └── ma │ │ └── dexter │ │ ├── dex │ │ └── MutableDexTests.kt │ │ ├── tasks │ │ └── MergeDexTaskTest.kt │ │ ├── tools │ │ └── decompilers │ │ │ └── jadx │ │ │ └── JADXDecompilerTest.kt │ │ ├── ui │ │ └── tree │ │ │ └── TreeUtilTest.kt │ │ └── util │ │ ├── BaseTestClass.kt │ │ └── compile │ │ └── Java2Dex.kt │ └── resources │ └── Test │ └── rt.jar ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/workflows/build-apk.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | build: 9 | name: Build debug apk 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Cancel previous runs 13 | uses: styfle/cancel-workflow-action@0.9.1 14 | with: 15 | workflow_id: 14258028 # obtained from https://api.github.com/repos/MikeAndrson/Dexter/actions/workflows 16 | access_token: ${{ github.token }} 17 | 18 | - uses: actions/checkout@v2 19 | 20 | - name: set up JDK 11 21 | uses: actions/setup-java@v2 22 | with: 23 | java-version: '11' 24 | distribution: 'adopt' 25 | cache: gradle 26 | 27 | - name: Grant execute permission for gradlew 28 | run: chmod +x gradlew 29 | 30 | - name: Build debug apk 31 | uses: eskatos/gradle-command-action@v1 32 | with: 33 | arguments: assembleDebug 34 | distributions-cache-enabled: true 35 | dependencies-cache-enabled: true 36 | configuration-cache-enabled: true 37 | 38 | - name: Upload debug apk 39 | uses: actions/upload-artifact@v2 40 | if: ${{ !github.head_ref }} 41 | with: 42 | name: apk-debug 43 | path: app/build/outputs/apk/debug 44 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | name: Run unit tests 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Cancel previous runs 14 | uses: styfle/cancel-workflow-action@0.9.1 15 | with: 16 | workflow_id: 14269347 # obtained from https://api.github.com/repos/MikeAndrson/Dexter/actions/workflows 17 | access_token: ${{ github.token }} 18 | 19 | - uses: actions/checkout@v2 20 | 21 | - name: set up JDK 11 22 | uses: actions/setup-java@v2 23 | with: 24 | java-version: '11' 25 | distribution: 'adopt' 26 | cache: gradle 27 | 28 | - name: Grant execute permission for gradlew 29 | run: chmod +x gradlew 30 | 31 | - name: Run unit tests 32 | uses: eskatos/gradle-command-action@v1 33 | with: 34 | arguments: test 35 | distributions-cache-enabled: true 36 | dependencies-cache-enabled: true 37 | configuration-cache-enabled: true 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | /app/release 12 | /.backups 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dexter 2 | A work-in-progress **DEX** editor for Android, using mainly [smali](https://github.com/JesusFreke/smali) & [dexlib2](https://github.com/JesusFreke/smali/tree/master/dexlib2). 3 | 4 | ## Available decompilers 5 | - [JADX](https://github.com/skylot/jadx) 6 | - [Fernflower](https://github.com/JetBrains/intellij-community/tree/master/plugins/java-decompiler/engine) 7 | - [CFR](https://github.com/leibnitz27/cfr) 8 | - [JD-Core](https://github.com/java-decompiler/jd-core) 9 | - [Procyon](https://github.com/mstrobel/procyon) 10 | 11 | ## TO-DO 12 | - [x] Add "Goto" for fields/methods. 13 | - [x] Add smali navigation. 14 | - [x] Decompile multiple classes at a time. 15 | - [x] Underline smali syntax errors in realtime. 16 | - [ ] Implement recompilation of decompiled Java sources. 17 | - [ ] Add decompiling single method bodies. 18 | - [ ] Add a (dis-)assembler for Java bytecode as well, like [Krakatau](https://github.com/Storyyeller/Krakatau) or [raung](https://github.com/skylot/raung). 19 | 20 | ## License 21 | Dexter is licensed under **GNU General Public License v3.0**, see [LICENSE](https://github.com/MikeAndrson/Dexter/blob/master/LICENSE) for more. 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | 9 | defaultConfig { 10 | applicationId "ma.dexter" 11 | minSdk 26 12 | targetSdk 31 13 | versionCode 1 14 | versionName "1.0" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled true 20 | shrinkResources true 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | buildFeatures { 26 | viewBinding true 27 | } 28 | 29 | compileOptions { 30 | coreLibraryDesugaringEnabled true 31 | 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | packagingOptions { 41 | exclude 'org/antlr/codegen/templates/**' // useless templates taking up +1MB 42 | exclude 'org/antlr/v4/tool/templates/codegen/**' 43 | exclude 'com/googlecode/dex2jar/tools/**' // useless debug keys 44 | exclude 'smali.properties' 45 | exclude 'baksmali.properties' 46 | exclude 'export/build.gradle.tmpl' // from jadx 47 | } 48 | } 49 | 50 | tasks.withType(Test) { 51 | testLogging { 52 | events "started", "passed", "skipped", "failed", "standardOut", "standardError" 53 | } 54 | } 55 | 56 | dependencies { 57 | coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' 58 | 59 | // AndroidX 60 | implementation 'androidx.core:core-ktx:1.7.0' 61 | implementation 'androidx.appcompat:appcompat:1.4.0' 62 | implementation 'com.google.android.material:material:1.4.0' 63 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 64 | implementation "androidx.fragment:fragment-ktx:1.4.0" 65 | 66 | // For unit tests 67 | testImplementation 'junit:junit:4.13.2' 68 | testImplementation 'com.google.truth:truth:1.1.3' 69 | testImplementation 'org.eclipse.jdt.core.compiler:ecj:4.6.1' 70 | testImplementation 'com.android.tools:r8:3.0.73' 71 | 72 | implementation 'io.github.Rosemoe.sora-editor:editor:0.8.3' 73 | implementation 'io.github.Rosemoe.sora-editor:language-base:0.8.3' 74 | implementation 'io.github.Rosemoe.sora-editor:language-universal:0.8.3' 75 | implementation 'io.github.Rosemoe.sora-editor:language-java:0.8.3' 76 | implementation 'com.github.angads25:filepicker:1.1.1' 77 | implementation 'me.zhanghai.android.fastscroll:library:1.1.7' 78 | implementation 'com.github.zawadz88.materialpopupmenu:material-popup-menu:4.1.0' 79 | implementation 'com.google.guava:guava:27.1-android' 80 | 81 | // javaparser 82 | implementation 'com.github.javaparser:javaparser-core:3.23.1' 83 | 84 | // dex2jar (maintained fork) 85 | /*def d2jVersion = "v50" 86 | def withoutIcu = { exclude group: 'com.ibm.icu', module: 'icu4j' } // Adds ~10MB to the APK ! 87 | implementation "com.github.ThexXTURBOXx.dex2jar:dex-tools:$d2jVersion", withoutIcu 88 | implementation "com.github.ThexXTURBOXx.dex2jar:dex-translator:$d2jVersion", withoutIcu*/ 89 | // TODO: replace this with the above when a new version is published 90 | implementation "com.github.MikeAndrson.dex2jar:dex-tools:9c0d3ad497" 91 | implementation "com.github.MikeAndrson.dex2jar:dex-translator:9c0d3ad497" 92 | 93 | // smali & baksmali 94 | implementation "org.smali:smali:2.5.2" 95 | implementation "org.smali:baksmali:2.5.2" 96 | 97 | // cfr 98 | implementation 'org.benf:cfr:0.151' 99 | 100 | // procyon 101 | implementation 'com.github.MikeAndrson.procyon:procyon-compilertools:androidport_0.5.36' 102 | implementation files("libs/procyon/javax-model.jar") // for javax.lang.model 103 | 104 | // fernflower 105 | // from the latest commit: 106 | // https://github.com/fesh0r/fernflower/commit/e7fa2769f512cdf85063a798ba9781d4fd6e9664 107 | implementation 'com.github.MikeAndrson:fernflower:3c1d05bf23' 108 | 109 | // jd-core 110 | // from the latest commit: 111 | // https://github.com/java-decompiler/jd-core/commit/fafc65682647deee757b7698e1c872469be1a211 112 | implementation 'com.github.java-decompiler:jd-core:fafc656826' 113 | 114 | // jadx 115 | implementation files("libs/jadx/jadx-core-v1.2.0.jar") 116 | implementation files("libs/jadx/jadx-plugins-api-v1.2.0.jar") 117 | implementation files("libs/jadx/jadx-dex-input-v1.2.0.jar") 118 | implementation files("libs/jadx/android-29-clst.jar") 119 | implementation 'org.slf4j:slf4j-api:1.7.32' // 1.7.30 120 | } 121 | -------------------------------------------------------------------------------- /app/libs/jadx/android-29-clst.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omeraydindev/Dexter/afe10af982c70525c1da5a87849a30169f314b80/app/libs/jadx/android-29-clst.jar -------------------------------------------------------------------------------- /app/libs/jadx/jadx-core-v1.2.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omeraydindev/Dexter/afe10af982c70525c1da5a87849a30169f314b80/app/libs/jadx/jadx-core-v1.2.0.jar -------------------------------------------------------------------------------- /app/libs/jadx/jadx-dex-input-v1.2.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omeraydindev/Dexter/afe10af982c70525c1da5a87849a30169f314b80/app/libs/jadx/jadx-dex-input-v1.2.0.jar -------------------------------------------------------------------------------- /app/libs/jadx/jadx-plugins-api-v1.2.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omeraydindev/Dexter/afe10af982c70525c1da5a87849a30169f314b80/app/libs/jadx/jadx-plugins-api-v1.2.0.jar -------------------------------------------------------------------------------- /app/libs/procyon/javax-model.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omeraydindev/Dexter/afe10af982c70525c1da5a87849a30169f314b80/app/libs/procyon/javax-model.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # for the 'core.jcst' to properly load. 2 | -keep class jadx.core.clsp.ClsSet 3 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 10 | 11 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/assets/d2j/Hex.clazz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omeraydindev/Dexter/afe10af982c70525c1da5a87849a30169f314b80/app/src/main/assets/d2j/Hex.clazz -------------------------------------------------------------------------------- /app/src/main/assets/rt.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omeraydindev/Dexter/afe10af982c70525c1da5a87849a30169f314b80/app/src/main/assets/rt.jar -------------------------------------------------------------------------------- /app/src/main/java/ma/dexter/ui/tree/TreeNode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2017 ShineM (Xinyuan) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 5 | * file except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | * ANY KIND, either express or implied. See the License for the specific language governing 12 | * permissions and limitations under. 13 | */ 14 | 15 | package ma.dexter.ui.tree; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | import ma.dexter.ui.tree.helper.TreeHelper; 21 | 22 | /** 23 | * Created by xinyuanzhong on 2017/4/20. 24 | */ 25 | 26 | public class TreeNode { 27 | private int level; 28 | 29 | private D value; 30 | 31 | private TreeNode parent; 32 | 33 | private List> children; 34 | 35 | private int index; 36 | 37 | private boolean expanded; 38 | 39 | private boolean selected; 40 | 41 | private boolean itemClickEnable = true; 42 | 43 | public TreeNode(D value) { 44 | this.value = value; 45 | this.children = new ArrayList<>(); 46 | } 47 | 48 | public TreeNode(D value, int level) { 49 | this(value); 50 | setLevel(level); 51 | } 52 | 53 | public static TreeNode root() { 54 | return new TreeNode<>(null); 55 | } 56 | 57 | public void addChild(TreeNode treeNode) { 58 | if (treeNode == null) { 59 | return; 60 | } 61 | children.add(treeNode); 62 | treeNode.setIndex(getChildren().size()); 63 | treeNode.setParent(this); 64 | } 65 | 66 | 67 | public void removeChild(TreeNode treeNode) { 68 | if (treeNode == null || getChildren().size() < 1) { 69 | return; 70 | } 71 | getChildren().remove(treeNode); 72 | } 73 | 74 | public boolean isLeaf() { 75 | return children.size() == 0; 76 | } 77 | 78 | public boolean isLastChild() { 79 | if (parent == null) { 80 | return false; 81 | } 82 | List> children = parent.getChildren(); 83 | return children.size() > 0 && children.indexOf(this) == children.size() - 1; 84 | } 85 | 86 | public boolean isRoot() { 87 | return parent == null; 88 | } 89 | 90 | public int getLevel() { 91 | return level; 92 | } 93 | 94 | public void setLevel(int level) { 95 | this.level = level; 96 | } 97 | 98 | public D getValue() { 99 | return value; 100 | } 101 | 102 | public void setValue(D value) { 103 | this.value = value; 104 | } 105 | 106 | public TreeNode getParent() { 107 | return parent; 108 | } 109 | 110 | public void setParent(TreeNode parent) { 111 | this.parent = parent; 112 | } 113 | 114 | public List> getChildren() { 115 | if (children == null) { 116 | return new ArrayList<>(); 117 | } 118 | return children; 119 | } 120 | 121 | public List> getSelectedChildren() { 122 | List> selectedChildren = new ArrayList<>(); 123 | for (TreeNode child : getChildren()) { 124 | if (child.isSelected()) { 125 | selectedChildren.add(child); 126 | } 127 | } 128 | return selectedChildren; 129 | } 130 | 131 | public void setChildren(List> children) { 132 | if (children == null) { 133 | return; 134 | } 135 | this.children = new ArrayList<>(); 136 | for (TreeNode child : children) { 137 | addChild(child); 138 | } 139 | } 140 | 141 | /** 142 | * Updating the list of children while maintaining the tree structure 143 | */ 144 | public void updateChildren(List> children) { 145 | List expands = new ArrayList<>(); 146 | List> allNodesPre = TreeHelper.getAllNodes(this); 147 | for (TreeNode node : allNodesPre) { 148 | expands.add(node.isExpanded()); 149 | } 150 | 151 | this.children = children; 152 | List> allNodes = TreeHelper.getAllNodes(this); 153 | if (allNodes.size() == expands.size()) { 154 | for (int i = 0; i < allNodes.size(); i++) { 155 | allNodes.get(i).setExpanded(expands.get(i)); 156 | } 157 | } 158 | } 159 | 160 | public void setExpanded(boolean expanded) { 161 | this.expanded = expanded; 162 | } 163 | 164 | public boolean isExpanded() { 165 | return expanded; 166 | } 167 | 168 | public boolean hasChild() { 169 | return children.size() > 0; 170 | } 171 | 172 | public boolean isItemClickEnable() { 173 | return itemClickEnable; 174 | } 175 | 176 | public void setItemClickEnable(boolean itemClickEnable) { 177 | this.itemClickEnable = itemClickEnable; 178 | } 179 | 180 | public String getId() { 181 | return getLevel() + "," + getIndex(); 182 | } 183 | 184 | public int getIndex() { 185 | return index; 186 | } 187 | 188 | public void setIndex(int index) { 189 | this.index = index; 190 | } 191 | 192 | public boolean isSelected() { 193 | return selected; 194 | } 195 | 196 | public void setSelected(boolean selected) { 197 | this.selected = selected; 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /app/src/main/java/ma/dexter/ui/tree/TreeView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2017 ShineM (Xinyuan) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 5 | * file except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | * ANY KIND, either express or implied. See the License for the specific language governing 12 | * permissions and limitations under. 13 | */ 14 | 15 | package ma.dexter.ui.tree; 16 | 17 | import android.annotation.SuppressLint; 18 | import android.content.Context; 19 | 20 | import androidx.annotation.NonNull; 21 | import androidx.recyclerview.widget.LinearLayoutManager; 22 | import androidx.recyclerview.widget.RecyclerView; 23 | import androidx.recyclerview.widget.SimpleItemAnimator; 24 | 25 | import java.util.List; 26 | 27 | import ma.dexter.ui.tree.base.BaseNodeViewFactory; 28 | import ma.dexter.ui.tree.base.SelectableTreeAction; 29 | import ma.dexter.ui.tree.helper.TreeHelper; 30 | import me.zhanghai.android.fastscroll.FastScrollerBuilder; 31 | 32 | /** 33 | * Created by xinyuanzhong on 2017/4/20. 34 | */ 35 | 36 | public class TreeView implements SelectableTreeAction { 37 | private final TreeNode root; 38 | 39 | private final Context context; 40 | 41 | private final BaseNodeViewFactory baseNodeViewFactory; 42 | 43 | private RecyclerView rootView; 44 | private TreeViewAdapter adapter; 45 | private LinearLayoutManager layoutManager; 46 | private boolean itemSelectable = true; 47 | 48 | public TreeView(@NonNull TreeNode root, @NonNull Context context, @NonNull BaseNodeViewFactory baseNodeViewFactory) { 49 | this.root = root; 50 | this.context = context; 51 | this.baseNodeViewFactory = baseNodeViewFactory; 52 | } 53 | 54 | public TreeViewAdapter getAdapter() { 55 | return adapter; 56 | } 57 | 58 | public LinearLayoutManager getLayoutManager() { 59 | return layoutManager; 60 | } 61 | 62 | public RecyclerView getView() { 63 | if (rootView == null) { 64 | this.rootView = buildRootView(); 65 | } 66 | return rootView; 67 | } 68 | 69 | @NonNull 70 | private RecyclerView buildRootView() { 71 | RecyclerView recyclerView = new RecyclerView(context); 72 | recyclerView.setMotionEventSplittingEnabled(false); // disable multi touch event to prevent terrible data set error when calculate list. 73 | ((SimpleItemAnimator) recyclerView.getItemAnimator()) 74 | .setSupportsChangeAnimations(false); 75 | 76 | new FastScrollerBuilder(recyclerView).build(); 77 | 78 | layoutManager = new LinearLayoutManager(context); 79 | recyclerView.setLayoutManager(layoutManager); 80 | 81 | adapter = new TreeViewAdapter<>(context, root, baseNodeViewFactory); 82 | adapter.setTreeView(this); 83 | 84 | recyclerView.setAdapter(adapter); 85 | return recyclerView; 86 | } 87 | 88 | @Override 89 | public void expandAll() { 90 | TreeHelper.expandAll(root); 91 | 92 | refreshTreeView(); 93 | } 94 | 95 | 96 | public void refreshTreeView() { 97 | if (rootView != null) { 98 | ((TreeViewAdapter) rootView.getAdapter()).refreshView(); 99 | } 100 | } 101 | 102 | @SuppressLint("NotifyDataSetChanged") 103 | public void updateTreeView() { 104 | if (rootView != null) { 105 | rootView.getAdapter().notifyDataSetChanged(); 106 | } 107 | } 108 | 109 | @Override 110 | public void expandNode(TreeNode treeNode) { 111 | adapter.expandNode(treeNode); 112 | } 113 | 114 | @Override 115 | public void expandLevel(int level) { 116 | TreeHelper.expandLevel(root, level); 117 | 118 | refreshTreeView(); 119 | } 120 | 121 | @Override 122 | public void collapseAll() { 123 | TreeHelper.collapseAll(root); 124 | 125 | refreshTreeView(); 126 | } 127 | 128 | @Override 129 | public void collapseNode(TreeNode treeNode) { 130 | adapter.collapseNode(treeNode); 131 | } 132 | 133 | @Override 134 | public void collapseLevel(int level) { 135 | TreeHelper.collapseLevel(root, level); 136 | 137 | refreshTreeView(); 138 | } 139 | 140 | @Override 141 | public void toggleNode(TreeNode treeNode) { 142 | if (treeNode.isExpanded()) { 143 | collapseNode(treeNode); 144 | } else { 145 | expandNode(treeNode); 146 | } 147 | } 148 | 149 | @Override 150 | public void deleteNode(TreeNode node) { 151 | adapter.deleteNode(node); 152 | } 153 | 154 | @Override 155 | public void addNode(TreeNode parent, TreeNode treeNode) { 156 | parent.addChild(treeNode); 157 | 158 | refreshTreeView(); 159 | } 160 | 161 | @Override 162 | public List> getAllNodes() { 163 | return TreeHelper.getAllNodes(root); 164 | } 165 | 166 | @Override 167 | public void selectNode(TreeNode treeNode) { 168 | if (treeNode != null) { 169 | adapter.selectNode(true, treeNode); 170 | } 171 | } 172 | 173 | @Override 174 | public void deselectNode(TreeNode treeNode) { 175 | if (treeNode != null) { 176 | adapter.selectNode(false, treeNode); 177 | } 178 | } 179 | 180 | @Override 181 | public void selectAll() { 182 | TreeHelper.selectNodeAndChild(root, true); 183 | 184 | refreshTreeView(); 185 | } 186 | 187 | @Override 188 | public void deselectAll() { 189 | TreeHelper.selectNodeAndChild(root, false); 190 | 191 | refreshTreeView(); 192 | } 193 | 194 | @Override 195 | public List> getSelectedNodes() { 196 | return TreeHelper.getSelectedNodes(root); 197 | } 198 | 199 | public boolean isItemSelectable() { 200 | return itemSelectable; 201 | } 202 | 203 | public void setItemSelectable(boolean itemSelectable) { 204 | this.itemSelectable = itemSelectable; 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /app/src/main/java/ma/dexter/ui/tree/base/BaseNodeViewBinder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2017 ShineM (Xinyuan) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 5 | * file except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | * ANY KIND, either express or implied. See the License for the specific language governing 12 | * permissions and limitations under. 13 | */ 14 | 15 | package ma.dexter.ui.tree.base; 16 | 17 | import android.view.View; 18 | 19 | import androidx.recyclerview.widget.RecyclerView; 20 | import ma.dexter.ui.tree.TreeNode; 21 | import ma.dexter.ui.tree.TreeView; 22 | 23 | /** 24 | * Created by zxy on 17/4/23. 25 | */ 26 | 27 | public abstract class BaseNodeViewBinder extends RecyclerView.ViewHolder { 28 | /** 29 | * This reference of TreeView make BaseNodeViewBinder has the ability 30 | * to expand node or select node. 31 | */ 32 | protected TreeView treeView; 33 | 34 | public BaseNodeViewBinder(View itemView) { 35 | super(itemView); 36 | } 37 | 38 | public void setTreeView(TreeView treeView) { 39 | this.treeView = treeView; 40 | } 41 | 42 | /** 43 | * Bind your data to view,you can get the data from treeNode by getValue() 44 | * 45 | * @param treeNode Node data 46 | */ 47 | public abstract void bindView(TreeNode treeNode); 48 | 49 | /** 50 | * if you do not want toggle the node when click whole item view,then you can assign a view to 51 | * trigger the toggle action 52 | * 53 | * @return The assigned view id to trigger expand or collapse. 54 | */ 55 | public int getToggleTriggerViewId() { 56 | return 0; 57 | } 58 | 59 | /** 60 | * Callback when a toggle action happened (only by clicked) 61 | * 62 | * @param treeNode The toggled node 63 | * @param expand Expanded or collapsed 64 | */ 65 | public void onNodeToggled(TreeNode treeNode, boolean expand) { 66 | //empty 67 | } 68 | 69 | /** 70 | * Callback for when a node is long clicked. 71 | */ 72 | public boolean onNodeLongClicked(View view, TreeNode treeNode, boolean expanded) { 73 | return false; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/ma/dexter/ui/tree/base/BaseNodeViewFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2017 ShineM (Xinyuan) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 5 | * file except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | * ANY KIND, either express or implied. See the License for the specific language governing 12 | * permissions and limitations under. 13 | */ 14 | 15 | package ma.dexter.ui.tree.base; 16 | 17 | import android.view.View; 18 | 19 | import ma.dexter.ui.tree.TreeNode; 20 | 21 | /** 22 | * Created by zxy on 17/4/23. 23 | */ 24 | 25 | public abstract class BaseNodeViewFactory { 26 | 27 | /** 28 | * The default implementation below behaves as in previous version when TreeViewAdapter.getItemViewType always returned the level, 29 | * but you can override it if you want some other viewType value to become the parameter to the method getNodeViewBinder. 30 | */ 31 | public int getViewType(TreeNode treeNode) { 32 | return treeNode.getLevel(); 33 | } 34 | 35 | /** 36 | * If you want build a tree view,you must implement this factory method 37 | * 38 | * @param view The parameter for BaseNodeViewBinder's constructor, do not use this for other 39 | * purpose! 40 | * @param viewType The viewType value is the treeNode level in the default implementation. 41 | * @return BaseNodeViewBinder 42 | */ 43 | public abstract BaseNodeViewBinder getNodeViewBinder(View view, int viewType); 44 | 45 | 46 | /** 47 | * If you want build a tree view,you must implement this factory method 48 | * 49 | * @param level Level of view, returned from {@link #getViewType} 50 | * @return node layout id 51 | */ 52 | public abstract int getNodeLayoutId(int level); 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/ma/dexter/ui/tree/base/BaseTreeAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2017 ShineM (Xinyuan) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 5 | * file except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | * ANY KIND, either express or implied. See the License for the specific language governing 12 | * permissions and limitations under. 13 | */ 14 | 15 | package ma.dexter.ui.tree.base; 16 | 17 | import java.util.List; 18 | 19 | import ma.dexter.ui.tree.TreeNode; 20 | 21 | /** 22 | * Created by xinyuanzhong on 2017/4/20. 23 | */ 24 | 25 | public interface BaseTreeAction { 26 | void expandAll(); 27 | 28 | void expandNode(TreeNode treeNode); 29 | 30 | void expandLevel(int level); 31 | 32 | void collapseAll(); 33 | 34 | void collapseNode(TreeNode treeNode); 35 | 36 | void collapseLevel(int level); 37 | 38 | void toggleNode(TreeNode treeNode); 39 | 40 | void deleteNode(TreeNode node); 41 | 42 | void addNode(TreeNode parent, TreeNode treeNode); 43 | 44 | List> getAllNodes(); 45 | 46 | // 1.add node at position 47 | // 2.add slide delete or other operations 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/ma/dexter/ui/tree/base/CheckableNodeViewBinder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2017 ShineM (Xinyuan) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 5 | * file except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | * ANY KIND, either express or implied. See the License for the specific language governing 12 | * permissions and limitations under. 13 | */ 14 | 15 | package ma.dexter.ui.tree.base; 16 | 17 | import android.view.View; 18 | 19 | import ma.dexter.ui.tree.TreeNode; 20 | 21 | /** 22 | * Created by xinyuanzhong on 2017/4/27. 23 | */ 24 | 25 | public abstract class CheckableNodeViewBinder extends BaseNodeViewBinder { 26 | 27 | public CheckableNodeViewBinder(View itemView) { 28 | super(itemView); 29 | } 30 | 31 | /** 32 | * Get the checkable view id. MUST BE A Checkable type! 33 | * 34 | * @return 35 | */ 36 | public abstract int getCheckableViewId(); 37 | 38 | /** 39 | * Do something when a node select or deselect(only triggered by clicked) 40 | * 41 | * @param treeNode 42 | * @param selected 43 | */ 44 | public void onNodeSelectedChanged(TreeNode treeNode, boolean selected) { 45 | /*empty*/ 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/ma/dexter/ui/tree/base/SelectableTreeAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2017 ShineM (Xinyuan) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 5 | * file except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | * ANY KIND, either express or implied. See the License for the specific language governing 12 | * permissions and limitations under. 13 | */ 14 | 15 | package ma.dexter.ui.tree.base; 16 | 17 | import java.util.List; 18 | 19 | import ma.dexter.ui.tree.TreeNode; 20 | 21 | /** 22 | * Created by xinyuanzhong on 2017/4/27. 23 | */ 24 | 25 | public interface SelectableTreeAction extends BaseTreeAction { 26 | void selectNode(TreeNode treeNode); 27 | 28 | void deselectNode(TreeNode treeNode); 29 | 30 | void selectAll(); 31 | 32 | void deselectAll(); 33 | 34 | List> getSelectedNodes(); 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/App.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter 2 | 3 | import android.app.Application 4 | 5 | class App : Application() { 6 | 7 | override fun onCreate() { 8 | super.onCreate() 9 | context = this 10 | } 11 | 12 | companion object { 13 | lateinit var context: App 14 | private set 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/api/consumer/ClassConversionConsumer.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.api.consumer 2 | 3 | fun interface ClassConversionConsumer { 4 | 5 | fun consume( 6 | className: String, 7 | bytes: ByteArray, 8 | ): Unit 9 | 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/api/consumer/ClassProgressConsumer.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.api.consumer 2 | 3 | fun interface ClassProgressConsumer { 4 | 5 | fun consume( 6 | className: String, 7 | current: Int, 8 | total: Int, 9 | ): Unit 10 | 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/dex/DexFactory.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.dex 2 | 3 | import org.jf.dexlib2.util.DexUtil 4 | import java.io.File 5 | 6 | object DexFactory { 7 | 8 | fun fromFile( 9 | file: File 10 | ): MutableDexFile { 11 | return fromByteArray(file.readBytes(), file) 12 | } 13 | 14 | private fun fromByteArray( 15 | byteArray: ByteArray, 16 | file: File 17 | ): MutableDexFile { 18 | DexUtil.verifyDexHeader(byteArray, 0) 19 | 20 | return MutableDexFile(file, byteArray) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/dex/DexGotoManager.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.dex 2 | 3 | import androidx.activity.viewModels 4 | import androidx.fragment.app.FragmentActivity 5 | import io.github.rosemoe.sora.widget.CodeEditor 6 | import ma.dexter.model.JavaGotoDef 7 | import ma.dexter.model.SmaliGotoDef 8 | import ma.dexter.project.Workspace 9 | import ma.dexter.ui.model.JavaItem 10 | import ma.dexter.ui.model.SmaliItem 11 | import ma.dexter.ui.viewmodel.MainViewModel 12 | import ma.dexter.util.FIELD_METHOD_CALL_REGEX 13 | import ma.dexter.util.toast 14 | import ma.dexter.util.tokenizeSmali 15 | import org.jf.smali.smaliParser 16 | 17 | class DexGotoManager( 18 | fragmentActivity: FragmentActivity 19 | ) { 20 | private val viewModel: MainViewModel by fragmentActivity.viewModels() 21 | 22 | /** 23 | * Goes to the class/member definition that the cursor is at, of the given [CodeEditor]. 24 | * (The text doesn't necessarily have to be selected) 25 | * 26 | * Selected text will be stretched for more flexibility, for example 27 | * consider the following smali line: 28 | * 29 | * `sput-object v0, LsomePackage/someClass;->someField:I` 30 | * 31 | * If `somePackage` is selected, it will be stretched from both sides 32 | * to match `LsomePackage/someClass;` and will redirect to that class. 33 | * 34 | * If `someField` is selected, it will be stretched from both sides 35 | * to match `LsomePackage/someClass;->someField:I` and will redirect 36 | * to that field definition. 37 | */ 38 | fun gotoDef( 39 | editor: CodeEditor 40 | ) { 41 | val cursor = editor.cursor 42 | 43 | // look for class defs 44 | tokenizeSmali(editor.text.toString()) { token, line, startColumn -> 45 | if (token.type == smaliParser.CLASS_DESCRIPTOR && line == cursor.leftLine) { 46 | val endColumn = startColumn + token.text.length 47 | 48 | if (cursor.leftColumn in startColumn..endColumn && 49 | cursor.rightColumn in startColumn..endColumn 50 | ) { 51 | editor.setSelectionRegion( 52 | cursor.leftLine, startColumn, 53 | cursor.rightLine, endColumn 54 | ) 55 | 56 | gotoClassDef(token.text) 57 | return 58 | } 59 | } 60 | } 61 | 62 | // look for field/method calls (on a line-by-line basis) 63 | editor.text.toString().lines().forEachIndexed { lineNumber, line -> 64 | FIELD_METHOD_CALL_REGEX.matchEntire(line)?.let { 65 | val range = it.groups[1]!!.range 66 | 67 | if (cursor.leftLine == lineNumber && 68 | cursor.leftColumn in range 69 | ) { 70 | editor.setSelectionRegion( 71 | cursor.leftLine, range.first, 72 | cursor.rightLine, range.last + 1 // to make it exclusive 73 | ) 74 | 75 | val (_, definingClass, descriptor) = it.destructured 76 | gotoClassDef(definingClass, descriptor) 77 | return 78 | } 79 | } 80 | } 81 | } 82 | 83 | fun gotoClassDef( 84 | dexClassDef: String, 85 | memberDescriptorToGo: String? = null 86 | ) { 87 | Workspace.getOpenedProject() 88 | .dexContainer.findClassDef(dexClassDef)?.let { 89 | gotoClassDef(SmaliGotoDef(it, memberDescriptorToGo)) 90 | return 91 | } 92 | 93 | toast("Couldn't find class def: $dexClassDef") 94 | } 95 | 96 | fun gotoClassDef( 97 | smaliGotoDef: SmaliGotoDef 98 | ) { 99 | viewModel.gotoPageItem(SmaliItem(smaliGotoDef)) 100 | } 101 | 102 | fun gotoJavaViewer( 103 | javaGotoDef: JavaGotoDef 104 | ) { 105 | viewModel.gotoPageItem(JavaItem(javaGotoDef)) 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/dex/MutableClassDef.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.dex 2 | 3 | import org.jf.dexlib2.iface.ClassDef 4 | import org.jf.dexlib2.iface.Method 5 | import org.jf.dexlib2.iface.reference.TypeReference 6 | 7 | class MutableClassDef( 8 | val parentDex: MutableDexFile, 9 | val classDef: ClassDef 10 | ): Comparable { 11 | 12 | val accessFlags: Int 13 | get() = classDef.accessFlags 14 | 15 | val type: String // class descriptor 16 | get() = classDef.type 17 | 18 | val methods: Iterable 19 | get() = classDef.methods 20 | 21 | // add more fields when necessary 22 | 23 | /** 24 | * See [TypeReference.equals] 25 | */ 26 | override fun equals(other: Any?): Boolean { 27 | if (this === other) return true 28 | 29 | if (other !is MutableClassDef) return false 30 | 31 | return classDef.toString() == other.toString() 32 | } 33 | 34 | /** 35 | * See [TypeReference.hashCode] 36 | */ 37 | override fun hashCode(): Int { 38 | return classDef.hashCode() 39 | } 40 | 41 | /** 42 | * See [TypeReference.compareTo] 43 | */ 44 | override fun compareTo(other: String): Int { 45 | return classDef.compareTo(other) 46 | } 47 | 48 | /** 49 | * See [TypeReference.toString] 50 | */ 51 | override fun toString(): String { 52 | return classDef.toString() 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/dex/MutableDexContainer.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.dex 2 | 3 | import com.google.common.collect.ImmutableBiMap 4 | 5 | class MutableDexContainer( 6 | val entries: List 7 | ) { 8 | val biMap: ImmutableBiMap = ImmutableBiMap.copyOf( 9 | entries.associateWith { it.dexFile?.name ?: "" } 10 | ) 11 | 12 | fun getInnerClasses( 13 | classDef: MutableClassDef 14 | ): List { 15 | val classDefPrefix = classDef.type.dropLast(1) + "$" // strip the ";" in "La/b;", add $ 16 | 17 | return buildList { 18 | entries.forEach { dexEntry -> 19 | dexEntry.classes.forEach { cd -> 20 | if (cd.type.startsWith(classDefPrefix)) { 21 | add(cd) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | 28 | fun findClassDef(classDescriptor: String?): MutableClassDef? { 29 | if (classDescriptor == null) return null 30 | 31 | entries.forEach { dexEntry -> 32 | dexEntry.findClassDef(classDescriptor)?.let { 33 | return it 34 | } 35 | } 36 | 37 | return null 38 | } 39 | 40 | fun deleteClassDef(classDescriptor: String) { 41 | entries.forEach { dex -> 42 | if (dex.deleteClassDef(classDescriptor)) { 43 | return 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * Deletes [MutableClassDef]s in the given package. 50 | * 51 | * [packagePath] being in the somePackage/someClass format. 52 | */ 53 | fun deletePackage(packagePath: String) { 54 | entries.forEach { dex -> 55 | dex.deletePackage(packagePath) 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/dex/MutableDexFile.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.dex 2 | 3 | import ma.dexter.util.DEFAULT_DEX_VERSION 4 | import org.jf.dexlib2.Opcodes 5 | import org.jf.dexlib2.dexbacked.DexBackedDexFile 6 | import org.jf.dexlib2.iface.ClassDef 7 | import org.jf.dexlib2.util.DexUtil 8 | import org.jf.dexlib2.writer.io.FileDataStore 9 | import org.jf.dexlib2.writer.pool.DexPool 10 | import java.io.File 11 | 12 | class MutableDexFile { 13 | private val dexVersion: Int 14 | private val _classes: MutableList 15 | 16 | val dexFile: File? // todo 17 | val opcodes: Opcodes 18 | val classes: List 19 | get() = _classes 20 | 21 | constructor( 22 | dexFile: File?, 23 | byteArray: ByteArray 24 | ) { 25 | this.dexFile = dexFile 26 | this.dexVersion = DexUtil.verifyDexHeader(byteArray, 0) 27 | this.opcodes = Opcodes.forDexVersion(dexVersion) 28 | 29 | val dexBacked = DexBackedDexFile(opcodes, byteArray) 30 | this._classes = dexBacked.classes.map { 31 | MutableClassDef(this, it) 32 | }.toMutableList() 33 | } 34 | 35 | constructor( 36 | classDefs: List = listOf(), 37 | dexVersion: Int? = null 38 | ) { 39 | this.dexFile = null 40 | this.dexVersion = 41 | dexVersion ?: classDefs.maxOfOrNull { it.parentDex.dexVersion } ?: DEFAULT_DEX_VERSION 42 | this.opcodes = Opcodes.forDexVersion(this.dexVersion) 43 | 44 | this._classes = classDefs.toMutableList() 45 | } 46 | 47 | 48 | /** 49 | * Adds a [ClassDef] if it doesn't exist. 50 | */ 51 | fun addClassDef(classDef: ClassDef) { 52 | val def = MutableClassDef(this, classDef) 53 | 54 | if (def !in _classes) { 55 | _classes.add(def) 56 | } 57 | } 58 | 59 | /** 60 | * Replaces a [ClassDef] if it exists. 61 | */ 62 | fun replaceClassDef(classDef: ClassDef) { 63 | val def = MutableClassDef(this, classDef) 64 | val index = _classes.indexOf(def) 65 | 66 | if (index != -1) { 67 | _classes[index] = def 68 | } 69 | } 70 | 71 | /** 72 | * Finds [MutableClassDef] from the classDescriptor specified, 73 | * returns null if no match is found. 74 | */ 75 | fun findClassDef(classDescriptor: String?) = 76 | _classes.firstOrNull { it.type == classDescriptor } 77 | 78 | /** 79 | * Deletes a [ClassDef] if it exists. 80 | */ 81 | fun deleteClassDef(classDescriptor: String): Boolean { 82 | val element = _classes.find { it.type == classDescriptor } 83 | if (element != null) { 84 | return deleteClassDef(element) 85 | } 86 | return false 87 | } 88 | 89 | /** 90 | * Deletes a [ClassDef] if it exists. 91 | */ 92 | fun deleteClassDef(classDef: MutableClassDef): Boolean { 93 | return _classes.remove(classDef) 94 | } 95 | 96 | /** 97 | * Deletes [MutableClassDef]s in the given package. 98 | * 99 | * [packagePath] being in somePackage/someClass format. 100 | */ 101 | fun deletePackage(packagePath: String) { 102 | _classes.removeAll { it.type.startsWith("L$packagePath/") } 103 | } 104 | 105 | /** 106 | * Writes to the [file] specified. 107 | */ 108 | fun writeToFile( 109 | file: File 110 | ) { 111 | val dexPool = DexPool(opcodes) 112 | _classes.forEach { 113 | dexPool.internClass(it.classDef) 114 | } 115 | dexPool.writeTo(FileDataStore(file)) 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/dex/SmaliContainer.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.dex 2 | 3 | import ma.dexter.tools.smali.BaksmaliInvoker 4 | 5 | class SmaliContainer { 6 | 7 | // LsomePackage/someClass; -> Smali code 8 | private val map = mutableMapOf() 9 | 10 | fun getSmaliCode(classDef: MutableClassDef): String { 11 | val smaliCode = map[classDef.type] 12 | 13 | return if (smaliCode != null) { 14 | smaliCode 15 | } else { 16 | val newCode = BaksmaliInvoker().disassemble(classDef) 17 | putSmaliCode(classDef.type, newCode) 18 | newCode 19 | } 20 | } 21 | 22 | fun putSmaliCode(classDescriptor: String, smaliCode: String) { 23 | map[classDescriptor] = smaliCode 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/model/GotoDefs.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.model 2 | 3 | import ma.dexter.dex.MutableClassDef 4 | 5 | sealed class GotoDef 6 | 7 | data class SmaliGotoDef( 8 | val classDef: MutableClassDef, 9 | val memberDescriptorToGo: String? = null 10 | ) : GotoDef() 11 | 12 | data class JavaGotoDef( 13 | val className: String, 14 | val javaCode: String 15 | ) : GotoDef() 16 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/parsers/java/JavaModels.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.parsers.java 2 | 3 | class JavaFile( 4 | val members: List 5 | ) 6 | 7 | sealed class JavaMember( 8 | val name: String, 9 | val line: Int 10 | ) 11 | 12 | class JavaField( 13 | name: String, 14 | line: Int 15 | ) : JavaMember(name, line) 16 | 17 | class JavaMethod( 18 | name: String, 19 | line: Int, 20 | ) : JavaMember(name, line) 21 | 22 | class JavaInnerClass( 23 | name: String, 24 | line: Int 25 | ) : JavaMember(name, line) 26 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/parsers/java/JavaParser.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.parsers.java 2 | 3 | import com.github.javaparser.JavaParser 4 | import com.github.javaparser.ast.Node 5 | import com.github.javaparser.ast.body.CallableDeclaration 6 | import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration 7 | import com.github.javaparser.ast.body.FieldDeclaration 8 | import com.github.javaparser.ast.nodeTypes.NodeWithImplements 9 | 10 | /** 11 | * Parses given [javaCode] to fields/methods/inner-classes 12 | * on the first level of the first top level class found. 13 | */ 14 | fun parseJava( 15 | javaCode: String 16 | ): JavaFile { 17 | val members = mutableListOf() 18 | val javaFile = JavaFile(members) 19 | 20 | val compilationUnit = JavaParser() 21 | .parse(javaCode) 22 | .result.get() 23 | 24 | // NodeWithImplements -> ClassOrInterfaceDeclaration, EnumDeclaration, RecordDeclaration 25 | val topClass = compilationUnit.types 26 | .firstOrNull { it is NodeWithImplements<*> } 27 | ?: return javaFile 28 | 29 | topClass.childNodes.filter(Node::hasRange).forEach { node -> 30 | 31 | if (node is FieldDeclaration) { 32 | members += JavaField( 33 | name = node.getVariable(0).nameAsString, 34 | line = node.getLine() 35 | ) 36 | } 37 | 38 | // CallableDeclaration -> ConstructorDeclaration, MethodDeclaration 39 | if (node is CallableDeclaration<*>) { 40 | members += JavaMethod( 41 | name = node.nameAsString, 42 | line = node.getLine() 43 | ) 44 | } 45 | 46 | if (node is ClassOrInterfaceDeclaration) { 47 | members += JavaInnerClass( 48 | name = node.nameAsString, 49 | line = node.getLine() 50 | ) 51 | } 52 | 53 | } 54 | 55 | return javaFile 56 | } 57 | 58 | fun Node.getLine() = 59 | range.get().begin.line - 1 60 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/parsers/smali/SmaliModels.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.parsers.smali 2 | 3 | class SmaliFile( 4 | methods: List, 5 | fields: List 6 | ) { 7 | val members = fields + methods 8 | } 9 | 10 | sealed class SmaliMember( 11 | val descriptor: String, 12 | val name: String, 13 | val nameIndex: Int, 14 | val line: Int 15 | ) 16 | 17 | class SmaliMethod( 18 | descriptor: String, 19 | name: String, 20 | nameIndex: Int, 21 | line: Int, 22 | var methodBody: String = "" 23 | ): SmaliMember(descriptor, name, nameIndex, line) 24 | 25 | class SmaliField( 26 | descriptor: String, 27 | name: String, 28 | nameIndex: Int, 29 | line: Int 30 | ): SmaliMember(descriptor, name, nameIndex, line) 31 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/parsers/smali/SmaliParser.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.parsers.smali 2 | 3 | import ma.dexter.util.END_METHOD_DIRECTIVE 4 | import ma.dexter.util.FIELD_DIRECTIVE_REGEX 5 | import ma.dexter.util.METHOD_DIRECTIVE_REGEX 6 | 7 | fun parseSmali( 8 | smaliCode: String 9 | ): SmaliFile { 10 | val smaliMethods = mutableListOf() 11 | val smaliFields = mutableListOf() 12 | 13 | val lines = smaliCode.lines() 14 | lines.forEachIndexed { lineNumber, _line -> 15 | val line = _line.trimStart() 16 | 17 | // .method 18 | METHOD_DIRECTIVE_REGEX.matchEntire(line)?.let { result -> 19 | val descriptor = result.groups[1]!!.value 20 | val name = result.groups[2]!!.value 21 | val nameIndex = result.groups[2]!!.range.first 22 | 23 | smaliMethods += SmaliMethod(descriptor, name, nameIndex, lineNumber) 24 | } 25 | 26 | // .end method 27 | if (line.startsWith(END_METHOD_DIRECTIVE)) { 28 | smaliMethods.lastOrNull()?.let { last -> 29 | last.methodBody = lines 30 | .slice(last.line..lineNumber) 31 | .joinToString("\n") 32 | } 33 | } 34 | 35 | // .field 36 | FIELD_DIRECTIVE_REGEX.matchEntire(line)?.let { result -> 37 | val descriptor = result.groups[1]!!.value 38 | val name = result.groups[2]!!.value 39 | val nameIndex = result.groups[2]!!.range.first 40 | 41 | smaliFields += SmaliField(descriptor, name, nameIndex, lineNumber) 42 | } 43 | } 44 | 45 | return SmaliFile(smaliMethods, smaliFields) 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/project/DexProject.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.project 2 | 3 | import ma.dexter.dex.MutableDexContainer 4 | import ma.dexter.dex.MutableDexFile 5 | import ma.dexter.dex.SmaliContainer 6 | 7 | class DexProject( 8 | dexEntries: List 9 | ): Project { 10 | val dexContainer = MutableDexContainer(dexEntries) 11 | 12 | val smaliContainer = SmaliContainer() 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/project/Project.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.project 2 | 3 | sealed interface Project 4 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/project/Workspace.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.project 2 | 3 | object Workspace { 4 | private var project: DexProject? = null 5 | 6 | fun getOpenedProject(): DexProject { 7 | return project 8 | ?: throw IllegalStateException("No project is opened!") 9 | } 10 | 11 | fun openProject(project: DexProject) { 12 | this.project = project 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tasks/BaksmaliDexTask.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tasks 2 | 3 | import ma.dexter.tools.smali.BaksmaliInvoker 4 | import java.io.File 5 | import kotlin.system.measureTimeMillis 6 | 7 | // TODO: execute in parallel to fasten the process 8 | class BaksmaliDexTask( 9 | private val dexFile: File, 10 | private val zipFile: File, 11 | ): ProgressTask() { 12 | 13 | override fun run(progress: (String) -> Unit): Result { 14 | val invoker = BaksmaliInvoker() 15 | 16 | val time = measureTimeMillis { 17 | invoker.disassemble( 18 | dexFile, 19 | zipFile, 20 | ) { className, current, total -> 21 | progress("[$current/$total]\n$className") 22 | } 23 | } 24 | 25 | return Result.success(""" 26 | Took: $time ms 27 | 28 | Saved to ${zipFile.absolutePath}""".trimIndent()) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tasks/BaksmaliTask.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tasks 2 | 3 | import ma.dexter.dex.MutableClassDef 4 | import ma.dexter.project.Workspace 5 | 6 | class BaksmaliTask( 7 | private val classDef: MutableClassDef 8 | ) : Task() { 9 | 10 | override fun run(): Result { 11 | val smaliCode = Workspace.getOpenedProject() 12 | .smaliContainer.getSmaliCode(classDef) 13 | 14 | return Result.success(smaliCode) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tasks/D2JTask.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tasks 2 | 3 | import ma.dexter.tools.d2j.D2JInvoker 4 | import java.io.File 5 | import kotlin.system.measureTimeMillis 6 | 7 | class D2JTask( 8 | private val dexFiles: List, 9 | private val jarFile: File, 10 | ): ProgressTask() { 11 | 12 | override fun run( 13 | progress: (String) -> Unit 14 | ): Result { 15 | 16 | val time = measureTimeMillis { 17 | progress("Initializing...") 18 | 19 | val d2j = D2JInvoker(dexFiles, jarFile) { currentProgress -> 20 | progress(currentProgress) 21 | } 22 | val d2jResult = d2j.invoke() 23 | 24 | if (!d2jResult.success) { 25 | return Result.failure("Dex2Jar", d2jResult.error) 26 | } 27 | } 28 | 29 | return Result.success(""" 30 | Took: $time ms 31 | 32 | Saved to ${jarFile.absolutePath}""".trimIndent()) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tasks/MergeDexTask.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tasks 2 | 3 | import ma.dexter.util.normalizeSmaliPath 4 | import org.jf.dexlib2.DexFileFactory 5 | import org.jf.dexlib2.Opcodes 6 | import org.jf.dexlib2.writer.io.FileDataStore 7 | import org.jf.dexlib2.writer.pool.DexPool 8 | import java.io.File 9 | import kotlin.system.measureTimeMillis 10 | 11 | // TODO: execute in parallel to fasten the process 12 | class MergeDexTask( 13 | private val dexPaths: Array, 14 | private val mergedDexFile: File 15 | ) : ProgressTask() { 16 | 17 | override fun run(progress: (String) -> Unit): Result { 18 | val dexPool = DexPool(Opcodes.forApi(31)) 19 | 20 | var totalTime = 0L 21 | val statistics = StringBuilder() 22 | 23 | dexPaths.forEach { dexPath -> 24 | val dex = DexFileFactory.loadDexFile(dexPath, null) 25 | statistics.append("${File(dexPath).name}:\n") 26 | 27 | val interningTime = measureTimeMillis { 28 | dex.classes.forEach { classDef -> 29 | progress("Interning: ${normalizeSmaliPath(classDef.type)}") 30 | dexPool.internClass(classDef) 31 | } 32 | } 33 | 34 | totalTime += interningTime 35 | statistics.append(" Interning: $interningTime ms\n\n") 36 | } 37 | 38 | val writingTime = measureTimeMillis { 39 | progress("Writing to ${mergedDexFile.name}") 40 | dexPool.writeTo(FileDataStore(mergedDexFile)) 41 | } 42 | totalTime += writingTime 43 | 44 | statistics.append("Writing: $writingTime ms\n") 45 | statistics.append("TOTAL: $totalTime ms") 46 | 47 | return Result.success(statistics.toString()) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tasks/SaveDexTask.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tasks 2 | 3 | import ma.dexter.project.Workspace 4 | import ma.dexter.util.normalizeSmaliPath 5 | import org.jf.dexlib2.writer.io.FileDataStore 6 | import org.jf.dexlib2.writer.pool.DexPool 7 | import kotlin.system.measureTimeMillis 8 | 9 | //TODO: execute in parallel to fasten the process 10 | class SaveDexTask : ProgressTask() { 11 | 12 | // todo: move to [MutableDexContainer] 13 | override fun run( 14 | progress: (String) -> Unit 15 | ): Result { 16 | val dexEntries = Workspace.getOpenedProject() 17 | .dexContainer.entries 18 | 19 | var totalTime = 0L 20 | val statistics = StringBuilder() 21 | 22 | dexEntries.forEach { dexEntry -> 23 | val dexPool = DexPool(dexEntry.opcodes) 24 | val dexFile = dexEntry.dexFile!! 25 | statistics.append("${dexFile.name}:\n") 26 | 27 | val interningTime = measureTimeMillis { 28 | dexEntry.classes.forEach { classDefEntry -> 29 | progress("Interning: ${normalizeSmaliPath(classDefEntry.type)}") 30 | dexPool.internClass(classDefEntry.classDef) 31 | } 32 | } 33 | 34 | val writingTime = measureTimeMillis { 35 | progress("Writing to ${dexFile.name}") 36 | dexPool.writeTo(FileDataStore(dexFile)) 37 | } 38 | 39 | totalTime += interningTime + writingTime 40 | statistics.append(" Interning: $interningTime ms\n") 41 | statistics.append(" Writing: $writingTime ms\n\n") 42 | } 43 | 44 | statistics.append("TOTAL: $totalTime ms") 45 | 46 | return Result.success(statistics.toString()) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tasks/Smali2JavaTask.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tasks 2 | 3 | import ma.dexter.App 4 | import ma.dexter.dex.MutableClassDef 5 | import ma.dexter.dex.MutableDexFile 6 | import ma.dexter.tools.d2j.D2JInvoker 7 | import ma.dexter.tools.decompilers.BaseDecompiler 8 | import ma.dexter.tools.decompilers.BaseDecompiler.Companion.decompile 9 | import ma.dexter.tools.decompilers.BaseDexDecompiler 10 | import ma.dexter.tools.decompilers.BaseJarDecompiler 11 | import java.io.File 12 | 13 | class Smali2JavaTask( 14 | private val classDefs: List, 15 | private val className: String, 16 | private val decompiler: BaseDecompiler 17 | ) : ProgressTask() { 18 | 19 | override fun run( 20 | progress: (String) -> Unit 21 | ): Result { 22 | 23 | // Clear out cache 24 | val parent = App.context.cacheDir.apply { 25 | deleteRecursively() 26 | mkdirs() 27 | } 28 | 29 | // Initialize temp files 30 | val dexFile = File(parent, "out.dex") 31 | val jarFile = File(parent, "out.jar") 32 | 33 | // Initialize temp vars 34 | val isJarDecompiler = decompiler is BaseJarDecompiler 35 | 36 | // Assemble ClassDefs to DEX 37 | progress("Assembling DEX...") 38 | MutableDexFile(classDefs).writeToFile(dexFile) 39 | 40 | /** 41 | * if the decompiler is not a [BaseJarDecompiler] (which could only mean 42 | * it's JADX, a [BaseDexDecompiler]) we don't need to run d2j since 43 | * JADX operates on dex anyway. 44 | */ 45 | if (isJarDecompiler) { 46 | // Invoke dex2jar 47 | progress("Converting DEX to JAR...") 48 | val d2jResult = D2JInvoker(listOf(dexFile), jarFile).invoke() 49 | 50 | if (!d2jResult.success) { 51 | return Result.failure("Dex2Jar", d2jResult.error) 52 | } 53 | } 54 | 55 | // Invoke the decompiler 56 | progress("Decompiling ${if (isJarDecompiler) "JAR" else "DEX"} to Java...") 57 | 58 | if (isJarDecompiler && !jarFile.exists()) { 59 | return Result.failure( 60 | decompiler.getName(), 61 | "Couldn't find generated JAR in ${jarFile.absolutePath}" 62 | ) 63 | } 64 | 65 | val javaCode = try { 66 | 67 | decompiler.decompile( 68 | className, 69 | if (isJarDecompiler) jarFile else dexFile 70 | ) 71 | 72 | } catch (e: Throwable) { 73 | return Result.failure( 74 | decompiler.getName(), 75 | "Couldn't decompile $className, logs:\n\n${e.stackTraceToString()}" 76 | ) 77 | } 78 | 79 | return Result.success(javaCode) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tasks/SmaliTask.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tasks 2 | 3 | import ma.dexter.tools.smali.SmaliInvoker 4 | import org.jf.dexlib2.iface.ClassDef 5 | 6 | class SmaliTask( 7 | private val smaliCode: String 8 | ) : Task() { 9 | 10 | override fun run(): Result { 11 | return SmaliInvoker.assemble(smaliCode) 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tasks/Tasks.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tasks 2 | 3 | sealed interface ITask 4 | 5 | abstract class Task : ITask { 6 | abstract fun run(): Result 7 | } 8 | 9 | abstract class ProgressTask : ITask { 10 | abstract fun run(progress: (String) -> Unit): Result 11 | } 12 | 13 | class Result private constructor( 14 | val value: T? = null, 15 | val success: Boolean, 16 | val error: Error = Error() 17 | ) { 18 | companion object { 19 | fun success(value: T): Result { 20 | return Result(success = true, value = value) 21 | } 22 | 23 | fun failure(title: String, message: String): Result { 24 | return Result(success = false, error = Error(title, message)) 25 | } 26 | } 27 | } 28 | 29 | class Error( 30 | val title: String = "", 31 | val message: String = "" 32 | ) 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tasks/Util.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tasks 2 | 3 | import android.content.Context 4 | import android.os.Handler 5 | import android.os.Looper 6 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 7 | import ma.dexter.ui.dialog.ProgressDialog 8 | import java.util.concurrent.Executors 9 | 10 | fun ITask.runWithDialog( 11 | context: Context, 12 | title: String, 13 | message: String, 14 | showDialogOnError: Boolean = true, 15 | callback: (Result) -> Unit 16 | ) { 17 | val dialog = ProgressDialog(context, title, message).show() 18 | val handler = Handler(Looper.getMainLooper()) 19 | 20 | Executors.newSingleThreadExecutor().execute { 21 | val result = try { 22 | when (this) { 23 | is ProgressTask -> run { 24 | handler.post { dialog.setMessage(it) } 25 | } 26 | is Task -> run() 27 | } 28 | } catch (e: Throwable) { 29 | Result.failure("Exception", e.stackTraceToString()) 30 | } 31 | 32 | handler.post { 33 | dialog.dismiss() 34 | 35 | if (showDialogOnError && !result.success) { 36 | MaterialAlertDialogBuilder(context) 37 | .setTitle("Error: " + result.error.title) 38 | .setMessage(result.error.message.trim()) 39 | .show() 40 | } 41 | callback(result) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/d2j/D2JExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.d2j 2 | 3 | import com.googlecode.d2j.Method 4 | import com.googlecode.d2j.dex.DexExceptionHandler 5 | import com.googlecode.d2j.node.DexMethodNode 6 | import org.objectweb.asm.MethodVisitor 7 | import org.objectweb.asm.Opcodes 8 | import java.io.PrintWriter 9 | import java.io.StringWriter 10 | 11 | /** 12 | * See [com.googlecode.dex2jar.tools.BaksmaliBaseDexExceptionHandler] 13 | */ 14 | class D2JExceptionHandler : DexExceptionHandler { 15 | 16 | private val exceptionMap = mutableMapOf() 17 | private var fileExceptions = mutableListOf() 18 | 19 | fun hasException(): Boolean { 20 | return exceptionMap.isNotEmpty() || fileExceptions.isNotEmpty() 21 | } 22 | 23 | fun getExceptions(): String { 24 | if (!hasException()) return "" 25 | 26 | return buildString { 27 | append("File exceptions:\n") 28 | fileExceptions.forEach { 29 | append(it.stackTraceToString()) 30 | append("\n") 31 | } 32 | 33 | append("\n") 34 | 35 | append("Method exceptions:\n") 36 | exceptionMap.forEach { 37 | append(it.value.stackTraceToString()) 38 | append("\n") 39 | } 40 | } 41 | } 42 | 43 | override fun handleFileException(e: Exception) { 44 | fileExceptions += e 45 | } 46 | 47 | /** 48 | * Adopted from [com.googlecode.d2j.dex.BaseDexExceptionHandler.handleMethodTranslateException] 49 | */ 50 | override fun handleMethodTranslateException( 51 | method: Method, 52 | methodNode: DexMethodNode, 53 | mv: MethodVisitor, 54 | e: Exception 55 | ) { 56 | exceptionMap[methodNode] = e 57 | 58 | // replace the generated code with 59 | // 'return new RuntimeException("d2j failed to translate: exception");' 60 | val s = StringWriter() 61 | s.append("d2j failed to translate: ") 62 | e.printStackTrace(PrintWriter(s)) 63 | 64 | mv.visitTypeInsn(Opcodes.NEW, "java/lang/RuntimeException") 65 | mv.visitInsn(Opcodes.DUP) 66 | mv.visitLdcInsn(s.toString()) 67 | mv.visitMethodInsn( 68 | Opcodes.INVOKESPECIAL, 69 | "java/lang/RuntimeException", 70 | "", 71 | "(Ljava/lang/String;)V", 72 | false 73 | ) 74 | mv.visitInsn(Opcodes.ATHROW) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/d2j/D2JFacade.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.d2j 2 | 3 | import com.googlecode.d2j.converter.IR2JConverter 4 | import com.googlecode.d2j.dex.* 5 | import com.googlecode.d2j.node.DexFileNode 6 | import com.googlecode.d2j.node.DexMethodNode 7 | import com.googlecode.d2j.reader.BaseDexFileReader 8 | import com.googlecode.d2j.reader.DexFileReader 9 | import com.googlecode.dex2jar.ir.IrMethod 10 | import com.googlecode.dex2jar.ir.stmt.LabelStmt 11 | import com.googlecode.dex2jar.ir.stmt.Stmt 12 | import com.googlecode.dex2jar.tools.Constants 13 | import ma.dexter.App 14 | import ma.dexter.api.consumer.ClassConversionConsumer 15 | import ma.dexter.api.consumer.ClassProgressConsumer 16 | import org.objectweb.asm.ClassVisitor 17 | import org.objectweb.asm.ClassWriter 18 | import org.objectweb.asm.MethodVisitor 19 | import java.io.File 20 | import java.io.InputStream 21 | import java.util.concurrent.atomic.AtomicInteger 22 | import java.util.zip.ZipEntry 23 | import java.util.zip.ZipOutputStream 24 | 25 | // TODO: execute in parallel to fasten the process 26 | class D2JFacade( 27 | private val reader: BaseDexFileReader, 28 | private val options: D2JOptions, 29 | private val exceptionHandler: DexExceptionHandler, 30 | private val progressConsumer: ClassProgressConsumer, 31 | ) { 32 | 33 | fun convert( 34 | jarFile: File, 35 | ) { 36 | ZipOutputStream(jarFile.outputStream()).use { zos -> 37 | convert { className, bytes -> 38 | val zipEntry = ZipEntry("$className.class") 39 | zos.putNextEntry(zipEntry) 40 | zos.write(bytes) 41 | zos.closeEntry() 42 | } 43 | } 44 | } 45 | 46 | private fun convert( 47 | conversionConsumer: ClassConversionConsumer, 48 | ) { 49 | val currentClassCount = AtomicInteger() 50 | val totalClassCount = reader.classNames.size 51 | val v3Config = options.getV3Config() 52 | val readerConfig = options.getReaderConfig() 53 | 54 | val fileNode = DexFileNode() 55 | try { 56 | val config = readerConfig or DexFileReader.IGNORE_READ_EXCEPTION 57 | reader.accept(fileNode, config) 58 | } catch (ex: Exception) { 59 | exceptionHandler.handleFileException(ex) 60 | } 61 | 62 | val cvf = ClassVisitorFactory { 63 | val cw = ClassWriter(ClassWriter.COMPUTE_MAXS) 64 | val rca = LambadaNameSafeClassAdapter(cw) 65 | 66 | return@ClassVisitorFactory object : ClassVisitor(Constants.ASM_VERSION, rca) { 67 | override fun visitEnd() { 68 | super.visitEnd() 69 | 70 | val className = rca.className 71 | progressConsumer.consume(className, currentClassCount.incrementAndGet(), totalClassCount) 72 | 73 | val data: ByteArray 74 | try { 75 | // FIXME handle 'java.lang.RuntimeException: Method code too large!' 76 | data = cw.toByteArray() 77 | } catch (ex: java.lang.Exception) { 78 | System.err.println("ASM failed to generate .class file: $className") 79 | exceptionHandler.handleFileException(ex) 80 | return 81 | } 82 | 83 | conversionConsumer.consume(className, data) 84 | } 85 | } 86 | } 87 | 88 | val dex2Asm = object : ExDex2Asm(exceptionHandler) { 89 | override fun convertCode(methodNode: DexMethodNode, mv: MethodVisitor?, clzCtx: ClzCtx?) { 90 | if ((readerConfig and DexFileReader.SKIP_CODE) != 0 && methodNode.method.name == "") { 91 | // also skip clinit 92 | return 93 | } 94 | super.convertCode(methodNode, mv, clzCtx) 95 | } 96 | 97 | override fun getHexClassAsStream(): InputStream { 98 | return App.context.assets.open("d2j/Hex.clazz") 99 | } 100 | 101 | override fun optimize(irMethod: IrMethod) { 102 | T_CLEAN_LABEL.transform(irMethod) 103 | /*if (0 != (v3Config & V3.TOPOLOGICAL_SORT)) { 104 | // T_topologicalSort.transform(irMethod); 105 | }*/ 106 | T_DEAD_CODE.transform(irMethod) 107 | T_REMOVE_LOCAL.transform(irMethod) 108 | T_REMOVE_CONST.transform(irMethod) 109 | T_ZERO.transform(irMethod) 110 | if (T_NPE.transformReportChanged(irMethod)) { 111 | T_DEAD_CODE.transform(irMethod) 112 | T_REMOVE_LOCAL.transform(irMethod) 113 | T_REMOVE_CONST.transform(irMethod) 114 | } 115 | T_NEW.transform(irMethod) 116 | T_FILL_ARRAY.transform(irMethod) 117 | T_AGG.transform(irMethod) 118 | T_MULTI_ARRAY.transform(irMethod) 119 | T_VOID_INVOKE.transform(irMethod) 120 | if (0 != (v3Config and V3.PRINT_IR)) { 121 | var i = 0 122 | for (p in irMethod.stmts) { 123 | if (p.st == Stmt.ST.LABEL) { 124 | val labelStmt = p as LabelStmt 125 | labelStmt.displayName = "L" + i++ 126 | } 127 | } 128 | println(irMethod) 129 | } 130 | run { 131 | // https://github.com/pxb1988/dex2jar/issues/477 132 | // dead code found in unssa, clean up 133 | T_DEAD_CODE.transform(irMethod) 134 | T_REMOVE_LOCAL.transform(irMethod) 135 | T_REMOVE_CONST.transform(irMethod) 136 | } 137 | T_TYPE.transform(irMethod) 138 | T_UNSSA.transform(irMethod) 139 | T_IR_2_J_REG_ASSIGN.transform(irMethod) 140 | T_TRIM_EX.transform(irMethod) 141 | } 142 | 143 | override fun ir2j(irMethod: IrMethod?, mv: MethodVisitor?, clzCtx: ClzCtx?) { 144 | IR2JConverter() 145 | .optimizeSynchronized(0 != (V3.OPTIMIZE_SYNCHRONIZED and v3Config)) 146 | .clzCtx(clzCtx) 147 | .ir(irMethod) 148 | .asm(mv) 149 | .convert() 150 | } 151 | } 152 | 153 | dex2Asm.convertDex(fileNode, cvf) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/d2j/D2JInvoker.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.d2j 2 | 3 | import com.googlecode.d2j.dex.Dex2jar 4 | import com.googlecode.d2j.reader.DexFileReader 5 | import com.googlecode.d2j.reader.MultiDexFileReader 6 | import java.io.File 7 | import java.nio.file.Files 8 | 9 | class D2JInvoker( 10 | private val dexFiles: List, 11 | private val outJar: File, 12 | private val options: D2JOptions = D2JOptions(), 13 | private val progressCallback: (String) -> Unit = {}, 14 | ) { 15 | /** 16 | * Runs [Dex2jar] on given [dexFile] and outputs to [outJar]. 17 | */ 18 | fun invoke(): Result { 19 | val handler = D2JExceptionHandler() 20 | val dexReader = MultiDexFileReader(dexFiles.map(::DexFileReader)) 21 | 22 | val d2JFacade = D2JFacade( 23 | reader = dexReader, 24 | options = options, 25 | exceptionHandler = handler, 26 | progressConsumer = { className, current, total -> 27 | progressCallback("[$current/$total]\n$className") 28 | } 29 | ) 30 | 31 | d2JFacade.convert(outJar) 32 | 33 | return Result( 34 | success = !handler.hasException(), 35 | error = handler.getExceptions() 36 | ) 37 | } 38 | 39 | class Result(val success: Boolean, val error: String) 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/d2j/D2JOptions.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.d2j 2 | 3 | import com.googlecode.d2j.dex.V3 4 | import com.googlecode.d2j.reader.DexFileReader 5 | 6 | class D2JOptions( 7 | var reuseReg: Boolean = false, 8 | var topoLogicalSort: Boolean = false, 9 | var noCode: Boolean = false, 10 | var optimizeSynchronized: Boolean = true, 11 | var printIR: Boolean = false, 12 | var skipDebug: Boolean = true, //TODO: doesn't work? 13 | var skipExceptions: Boolean = false, 14 | ) { 15 | fun getReaderConfig(): Int { 16 | var readerConfig = 0 or DexFileReader.SKIP_DEBUG 17 | 18 | if (noCode) { 19 | readerConfig = 20 | readerConfig or (DexFileReader.SKIP_CODE or DexFileReader.KEEP_CLINIT) 21 | } else { 22 | readerConfig = 23 | readerConfig and (DexFileReader.SKIP_CODE or DexFileReader.KEEP_CLINIT).inv() 24 | } 25 | 26 | if (skipDebug) { 27 | readerConfig = readerConfig or DexFileReader.SKIP_DEBUG 28 | } else { 29 | readerConfig = readerConfig and DexFileReader.SKIP_DEBUG.inv() 30 | } 31 | 32 | if (skipExceptions) { 33 | readerConfig = readerConfig or DexFileReader.SKIP_EXCEPTION 34 | } else { 35 | readerConfig = readerConfig and DexFileReader.SKIP_EXCEPTION.inv() 36 | } 37 | 38 | return readerConfig 39 | } 40 | 41 | fun getV3Config(): Int { 42 | var v3Config = 0 43 | 44 | if (reuseReg) { 45 | v3Config = v3Config or V3.REUSE_REGISTER 46 | } else { 47 | v3Config = v3Config and V3.REUSE_REGISTER.inv() 48 | } 49 | 50 | if (topoLogicalSort) { 51 | v3Config = v3Config or V3.TOPOLOGICAL_SORT 52 | } else { 53 | v3Config = v3Config and V3.TOPOLOGICAL_SORT.inv() 54 | } 55 | 56 | if (optimizeSynchronized) { 57 | v3Config = v3Config or V3.OPTIMIZE_SYNCHRONIZED 58 | } else { 59 | v3Config = v3Config and V3.OPTIMIZE_SYNCHRONIZED.inv() 60 | } 61 | 62 | if (printIR) { 63 | v3Config = v3Config or V3.PRINT_IR 64 | } else { 65 | v3Config = v3Config and V3.PRINT_IR.inv() 66 | } 67 | 68 | return v3Config 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/decompilers/BaseDecompiler.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers 2 | 3 | import ma.dexter.tools.decompilers.cfr.CFRDecompiler 4 | import ma.dexter.tools.decompilers.fernflower.FernflowerDecompiler 5 | import ma.dexter.tools.decompilers.jadx.JADXDecompiler 6 | import ma.dexter.tools.decompilers.jdcore.JDCoreDecompiler 7 | import ma.dexter.tools.decompilers.procyon.ProcyonDecompiler 8 | import java.io.File 9 | 10 | sealed interface BaseDecompiler { 11 | 12 | fun getBanner(): String? 13 | 14 | fun getName(): String 15 | 16 | companion object { 17 | 18 | /** 19 | * Decompiles given [dexOrJarFile] and returns the decompiled Java code. 20 | */ 21 | fun BaseDecompiler.decompile(className: String, dexOrJarFile: File): String { 22 | return when (this) { 23 | is BaseJarDecompiler -> decompileJar(className, jarFile = dexOrJarFile) 24 | is BaseDexDecompiler -> decompileDex(className, dexFile = dexOrJarFile) 25 | } 26 | } 27 | 28 | /** 29 | * Returns an immutable list of available decompilers. 30 | */ 31 | fun getDecompilers() = listOf( 32 | JADXDecompiler(), 33 | JADXDecompiler(fallbackMode = true), 34 | FernflowerDecompiler(), 35 | CFRDecompiler(), 36 | JDCoreDecompiler(), 37 | ProcyonDecompiler() 38 | ) 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/decompilers/BaseDexDecompiler.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers 2 | 3 | import ma.dexter.tools.decompilers.jadx.JADXDecompiler 4 | import java.io.File 5 | 6 | /** 7 | * Currently only [JADXDecompiler]. 8 | */ 9 | interface BaseDexDecompiler: BaseDecompiler { 10 | 11 | fun decompileDex(className: String, dexFile: File): String 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/decompilers/BaseJarDecompiler.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers 2 | 3 | import ma.dexter.tools.decompilers.cfr.CFRDecompiler 4 | import ma.dexter.tools.decompilers.fernflower.FernflowerDecompiler 5 | import ma.dexter.tools.decompilers.jdcore.JDCoreDecompiler 6 | import ma.dexter.tools.decompilers.procyon.ProcyonDecompiler 7 | import java.io.File 8 | 9 | /** 10 | * [CFRDecompiler], [FernflowerDecompiler], [JDCoreDecompiler], [ProcyonDecompiler]. 11 | */ 12 | interface BaseJarDecompiler: BaseDecompiler { 13 | 14 | fun decompileJar(className: String, jarFile: File): String 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/decompilers/cfr/CFRDecompiler.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers.cfr 2 | 3 | import ma.dexter.tools.decompilers.BaseJarDecompiler 4 | import org.benf.cfr.reader.api.CfrDriver 5 | import java.io.File 6 | import java.util.jar.JarFile 7 | 8 | class CFRDecompiler : BaseJarDecompiler { 9 | private val options = defaultOptions() 10 | 11 | /* 12 | * Adapted from https://github.com/leibnitz27/cfr_client/blob/master/src/org/benf/cfr_client_example/WithBetterSink.java 13 | */ 14 | override fun decompileJar( 15 | className: String, 16 | jarFile: File 17 | ): String { 18 | val jar = JarFile(jarFile) 19 | 20 | val outputSink = CFROutputSink() 21 | 22 | val jarSource = CFRJarSource(jar) 23 | 24 | val driver = CfrDriver.Builder() 25 | .withClassFileSource(jarSource) 26 | .withOutputSink(outputSink) 27 | .withOptions(options) 28 | .build() 29 | 30 | driver.analyse(listOf(className)) 31 | return outputSink.javaCode 32 | } 33 | 34 | // CFR already appends a banner by default 35 | override fun getBanner(): String? = null 36 | 37 | override fun getName() = "CFR" 38 | 39 | private fun defaultOptions() = mapOf( 40 | "comments" to "false" // Disables 'Cannot load following classes' comments 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/decompilers/cfr/CFRJarSource.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers.cfr 2 | 3 | import org.benf.cfr.reader.api.ClassFileSource 4 | import org.benf.cfr.reader.bytecode.analysis.parse.utils.Pair 5 | import java.util.jar.JarFile 6 | 7 | class CFRJarSource( 8 | private val jarFile: JarFile 9 | ) : ClassFileSource { 10 | 11 | // path -> somePackage/SomeClass.class 12 | override fun getClassFileContent(path: String?): Pair { 13 | val entry = jarFile.getJarEntry(path) 14 | 15 | jarFile.getInputStream(entry).use { 16 | return Pair(it.readBytes(), path) 17 | } 18 | } 19 | 20 | override fun addJar(jarPath: String?) = mutableListOf() 21 | 22 | override fun informAnalysisRelativePathDetail(usePath: String?, classFilePath: String?) {} 23 | 24 | override fun getPossiblyRenamedPath(path: String?) = path 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/decompilers/cfr/CFROutputSink.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers.cfr 2 | 3 | import org.benf.cfr.reader.api.OutputSinkFactory 4 | import org.benf.cfr.reader.api.SinkReturns 5 | import java.util.* 6 | 7 | class CFROutputSink : OutputSinkFactory { 8 | private val _javaCode = StringBuilder() 9 | 10 | val javaCode: String 11 | get() = _javaCode.toString() 12 | 13 | override fun getSupportedSinks( 14 | sinkType: OutputSinkFactory.SinkType, 15 | collection: Collection 16 | ): List { 17 | 18 | return if (sinkType == OutputSinkFactory.SinkType.JAVA && OutputSinkFactory.SinkClass.DECOMPILED in collection) { 19 | listOf( 20 | OutputSinkFactory.SinkClass.DECOMPILED, 21 | OutputSinkFactory.SinkClass.STRING 22 | ) 23 | } else { 24 | Collections.singletonList(OutputSinkFactory.SinkClass.STRING) 25 | } 26 | 27 | } 28 | 29 | override fun getSink( 30 | sinkType: OutputSinkFactory.SinkType, 31 | sinkClass: OutputSinkFactory.SinkClass 32 | ): OutputSinkFactory.Sink { 33 | 34 | if (sinkType == OutputSinkFactory.SinkType.JAVA && sinkClass == OutputSinkFactory.SinkClass.DECOMPILED) { 35 | return OutputSinkFactory.Sink { 36 | (it as SinkReturns.Decompiled).run { 37 | _javaCode.append(java) 38 | } 39 | } 40 | } 41 | 42 | return OutputSinkFactory.Sink {} 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/decompilers/fernflower/FFBytecodeProvider.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers.fernflower 2 | 3 | import org.jetbrains.java.decompiler.main.extern.IBytecodeProvider 4 | import java.io.File 5 | import java.util.jar.JarFile 6 | 7 | class FFBytecodeProvider : IBytecodeProvider { 8 | 9 | override fun getBytecode(externalPath: String, internalPath: String?): ByteArray { 10 | val jar = JarFile(File(externalPath)) 11 | 12 | val entry = jar.getJarEntry(internalPath) 13 | 14 | jar.getInputStream(entry).use { 15 | return it.readBytes() 16 | } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/decompilers/fernflower/FFResultSaver.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers.fernflower 2 | 3 | import org.jetbrains.java.decompiler.main.extern.IResultSaver 4 | import java.util.jar.Manifest 5 | 6 | class FFResultSaver(val className: String) : IResultSaver { 7 | var result = "" 8 | 9 | private fun saveClass(qualifiedName: String?, content: String?) { 10 | if (result.isEmpty() && qualifiedName == className) { 11 | result = content.toString() 12 | } 13 | } 14 | 15 | override fun saveClassFile( 16 | path: String, 17 | qualifiedName: String?, 18 | entryName: String, 19 | content: String?, 20 | mapping: IntArray? 21 | ) { 22 | saveClass(qualifiedName, content) 23 | } 24 | 25 | override fun saveClassEntry( 26 | path: String?, 27 | archiveName: String?, 28 | qualifiedName: String?, 29 | entryName: String?, 30 | content: String? 31 | ) { 32 | saveClass(qualifiedName, content) 33 | } 34 | 35 | override fun saveFolder(path: String?) {} 36 | 37 | override fun copyFile(source: String?, path: String?, entryName: String?) {} 38 | 39 | override fun createArchive(path: String?, archiveName: String?, manifest: Manifest?) {} 40 | 41 | override fun saveDirEntry(path: String?, archiveName: String?, entryName: String?) {} 42 | 43 | override fun copyEntry( 44 | source: String?, 45 | path: String?, 46 | archiveName: String?, 47 | entry: String? 48 | ) { 49 | } 50 | 51 | override fun closeArchive(path: String?, archiveName: String?) {} 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/decompilers/fernflower/FernflowerDecompiler.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers.fernflower 2 | 3 | import ma.dexter.tools.decompilers.BaseJarDecompiler 4 | import org.jetbrains.java.decompiler.main.decompiler.BaseDecompiler 5 | import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger 6 | import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences 7 | import java.io.File 8 | 9 | /* 10 | * Adapted from [https://github.com/JetBrains/intellij-community/blob/master/plugins/java-decompiler/plugin/src/org/jetbrains/java/decompiler/IdeaDecompiler.kt] 11 | */ 12 | class FernflowerDecompiler : BaseJarDecompiler { 13 | private val options = defaultOptions() 14 | 15 | /** 16 | * Decompiles given [jarFile] to Java using Fernflower. 17 | * 18 | * @return Decompiled Java code 19 | */ 20 | override fun decompileJar( 21 | className: String, 22 | jarFile: File 23 | ): String { 24 | val bytecodeProvider = FFBytecodeProvider() 25 | val resultSaver = FFResultSaver(className) 26 | 27 | val logger = object : IFernflowerLogger() { 28 | override fun writeMessage(p0: String?, p1: Severity?) {} 29 | 30 | override fun writeMessage(p0: String?, p1: Severity?, p2: Throwable?) {} 31 | } 32 | 33 | val decompiler = BaseDecompiler(bytecodeProvider, resultSaver, options, logger) 34 | decompiler.addSource(jarFile) 35 | decompiler.decompileContext() 36 | 37 | return resultSaver.result.ifEmpty { 38 | "// Error: Fernflower couldn't decompile $className" 39 | } 40 | } 41 | 42 | override fun getBanner() = """ 43 | /* 44 | * Decompiled with Fernflower [e7fa2769f5]. 45 | */ 46 | """.trimIndent() + "\n" 47 | 48 | override fun getName() = "Fernflower" 49 | 50 | private fun defaultOptions() = mapOf( 51 | IFernflowerPreferences.HIDE_DEFAULT_CONSTRUCTOR to "0", 52 | IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES to "1", 53 | IFernflowerPreferences.REMOVE_SYNTHETIC to "1", 54 | IFernflowerPreferences.REMOVE_BRIDGE to "1", 55 | IFernflowerPreferences.LITERALS_AS_IS to "1", 56 | IFernflowerPreferences.NEW_LINE_SEPARATOR to "1", 57 | IFernflowerPreferences.BANNER to getBanner(), 58 | IFernflowerPreferences.MAX_PROCESSING_METHOD to 60, 59 | IFernflowerPreferences.IGNORE_INVALID_BYTECODE to "1", 60 | IFernflowerPreferences.VERIFY_ANONYMOUS_CLASSES to "1" 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/decompilers/jadx/JADXDecompiler.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers.jadx 2 | 3 | import jadx.api.JadxArgs 4 | import jadx.api.JadxDecompiler 5 | import ma.dexter.tools.decompilers.BaseDexDecompiler 6 | import java.io.File 7 | 8 | class JADXDecompiler( 9 | private val fallbackMode: Boolean = false 10 | ) : BaseDexDecompiler { 11 | 12 | override fun decompileDex( 13 | className: String, 14 | dexFile: File 15 | ): String { 16 | 17 | val jadxArgs = JadxArgs().apply { 18 | inputFiles.add(dexFile) 19 | isSkipResources = true 20 | isRespectBytecodeAccModifiers = true 21 | isShowInconsistentCode = true // to avoid errors (?) 22 | isFallbackMode = fallbackMode 23 | } 24 | 25 | JadxDecompiler(jadxArgs).also { jadx -> 26 | jadx.load() 27 | // jadx.save() // Saves decompiled classes to storage, we don't want that 28 | 29 | jadx.classes.forEach { 30 | if (it.fullName == className.replace("/", ".")) { 31 | return getBanner() + it.code 32 | } 33 | } 34 | 35 | if (jadx.classes.size > 0) { 36 | return getBanner() + jadx.classes.first().code 37 | } 38 | 39 | return "// Error: JADX couldn't decompile $className" 40 | } 41 | 42 | } 43 | 44 | override fun getBanner() = """ 45 | /* 46 | * Decompiled with JADX v1.2.0. 47 | */ 48 | """.trimIndent() + "\n" 49 | 50 | override fun getName() = if (fallbackMode) "JADX (Fallback)" else "JADX" 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/decompilers/jdcore/JDCoreDecompiler.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers.jdcore 2 | 3 | import ma.dexter.tools.decompilers.BaseJarDecompiler 4 | import org.jd.core.v1.ClassFileToJavaSourceDecompiler 5 | import java.io.File 6 | import java.util.jar.JarFile 7 | 8 | /* 9 | * Adapted from https://github.com/java-decompiler/jd-core/tree/v1.1.3 10 | */ 11 | class JDCoreDecompiler : BaseJarDecompiler { 12 | 13 | override fun decompileJar( 14 | className: String, 15 | jarFile: File 16 | ): String { 17 | 18 | val loader = JDLoader(JarFile(jarFile)) 19 | val printer = JDPrinter() 20 | 21 | ClassFileToJavaSourceDecompiler() 22 | .decompile(loader, printer, className) 23 | 24 | return getBanner() + printer.toString() 25 | } 26 | 27 | override fun getBanner() = """ 28 | /* 29 | * Decompiled with JD-Core v1.1.3. 30 | */ 31 | """.trimIndent() + "\n" 32 | 33 | override fun getName() = "JD-Core" 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/decompilers/jdcore/JDLoader.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers.jdcore 2 | 3 | import org.jd.core.v1.api.loader.Loader 4 | import java.util.jar.JarFile 5 | 6 | class JDLoader( 7 | private val jarFile: JarFile 8 | ) : Loader { 9 | 10 | override fun canLoad(internalName: String?): Boolean { 11 | return jarFile.getJarEntry("$internalName.class") != null 12 | } 13 | 14 | override fun load(internalName: String?): ByteArray? { 15 | val entry = jarFile.getJarEntry("$internalName.class") ?: return null 16 | 17 | jarFile.getInputStream(entry).use { 18 | return it.readBytes() 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/decompilers/jdcore/JDPrinter.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers.jdcore 2 | 3 | import org.jd.core.v1.api.printer.Printer 4 | 5 | class JDPrinter : Printer { 6 | private var indentationCount = 0 7 | private var sb = StringBuilder() 8 | 9 | override fun toString() = sb.toString() 10 | 11 | override fun start(maxLineNumber: Int, majorVersion: Int, minorVersion: Int) {} 12 | 13 | override fun end() {} 14 | 15 | override fun printText(text: String?) { 16 | sb.append(text) 17 | } 18 | 19 | override fun printNumericConstant(constant: String?) { 20 | sb.append(constant) 21 | } 22 | 23 | override fun printStringConstant(constant: String?, ownerInternalName: String?) { 24 | sb.append(constant) 25 | } 26 | 27 | override fun printKeyword(keyword: String?) { 28 | sb.append(keyword) 29 | } 30 | 31 | override fun printDeclaration( 32 | type: Int, 33 | internalTypeName: String?, 34 | name: String?, 35 | descriptor: String? 36 | ) { 37 | sb.append(name) 38 | } 39 | 40 | override fun printReference( 41 | type: Int, 42 | internalTypeName: String?, 43 | name: String?, 44 | descriptor: String?, 45 | ownerInternalName: String? 46 | ) { 47 | sb.append(name) 48 | } 49 | 50 | override fun indent() { 51 | indentationCount++ 52 | } 53 | 54 | override fun unindent() { 55 | indentationCount-- 56 | } 57 | 58 | override fun startLine(lineNumber: Int) { 59 | for (i in 0 until indentationCount) sb.append(" ") 60 | } 61 | 62 | override fun endLine() { 63 | sb.append("\n") 64 | } 65 | 66 | override fun extraLine(count: Int) { 67 | var n = count 68 | while (n-- > 0) sb.append("\n") 69 | } 70 | 71 | override fun startMarker(type: Int) {} 72 | 73 | override fun endMarker(type: Int) {} 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/decompilers/procyon/ProcyonDecompiler.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers.procyon 2 | 3 | import com.strobel.assembler.metadata.JarTypeLoader 4 | import com.strobel.decompiler.Decompiler 5 | import com.strobel.decompiler.DecompilerSettings 6 | import com.strobel.decompiler.DecompilerSettings.RT_JAR 7 | import com.strobel.decompiler.PlainTextOutput 8 | import ma.dexter.App 9 | import ma.dexter.tools.decompilers.BaseJarDecompiler 10 | import ma.dexter.util.extractAsset 11 | import java.io.File 12 | import java.util.jar.JarFile 13 | 14 | class ProcyonDecompiler: BaseJarDecompiler { 15 | 16 | override fun decompileJar( 17 | className: String, 18 | jarFile: File 19 | ): String { 20 | 21 | /** 22 | * For some reason, Procyon needs to process bytecodes of [java.lang.Class] 23 | * and [java.lang.Object] before decompilation. Fortunately they take up like 3 KBs 24 | */ 25 | val rtJar = File(App.context.filesDir, "rt.jar") 26 | extractAsset("rt.jar", rtJar) 27 | 28 | val options = DecompilerSettings().apply { 29 | forceExplicitImports = true 30 | showSyntheticMembers = false 31 | 32 | RT_JAR = JarFile(rtJar) 33 | typeLoader = JarTypeLoader(JarFile(jarFile)) 34 | } 35 | 36 | val output = PlainTextOutput() 37 | Decompiler.decompile(className, output, options) 38 | 39 | return getBanner() + output.toString() 40 | } 41 | 42 | override fun getBanner() = """ 43 | /* 44 | * Decompiled with Procyon 0.5.36. 45 | */ 46 | """.trimIndent() + "\n" 47 | 48 | override fun getName() = "Procyon" 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/jar/JarTool.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.jar 2 | 3 | import ma.dexter.BuildConfig 4 | import java.io.BufferedInputStream 5 | import java.io.File 6 | import java.io.FileInputStream 7 | import java.io.FileOutputStream 8 | import java.util.jar.Attributes 9 | import java.util.jar.JarEntry 10 | import java.util.jar.JarOutputStream 11 | import java.util.jar.Manifest 12 | 13 | class JarTool( 14 | private val classesDir: File, 15 | private val jarFile: File, 16 | private val attributes: Attributes = getDefAttrs() 17 | ) { 18 | 19 | fun create(): File { 20 | val manifest = buildManifest(attributes) 21 | 22 | FileOutputStream(jarFile).use { stream -> 23 | JarOutputStream(stream, manifest).use { out -> 24 | val files = classesDir.listFiles() 25 | 26 | files?.forEach { 27 | add(classesDir.path, it, out) 28 | } 29 | } 30 | } 31 | 32 | return jarFile 33 | } 34 | 35 | // TODO: clean this mess up 36 | private fun add( 37 | parentPath: String, 38 | source: File, 39 | target: JarOutputStream 40 | ) { 41 | var name = source.path.substring(parentPath.length + 1) 42 | 43 | if (source.isDirectory) { 44 | if (name.isNotEmpty()) { 45 | if (!name.endsWith("/")) name += "/" 46 | 47 | JarEntry(name).run { 48 | time = source.lastModified() 49 | target.putNextEntry(this) 50 | target.closeEntry() 51 | } 52 | } 53 | 54 | source.listFiles()!!.forEach { nestedFile -> 55 | add(parentPath, nestedFile, target) 56 | } 57 | 58 | return 59 | } 60 | 61 | JarEntry(name).run { 62 | time = source.lastModified() 63 | target.putNextEntry(this) 64 | } 65 | 66 | BufferedInputStream(FileInputStream(source)).use { 67 | val buffer = ByteArray(1024) 68 | while (true) { 69 | val count = it.read(buffer) 70 | if (count == -1) break 71 | target.write(buffer, 0, count) 72 | } 73 | 74 | target.closeEntry() 75 | } 76 | } 77 | 78 | companion object { 79 | private fun getDefAttrs(): Attributes { 80 | return Attributes().also { 81 | it[Attributes.Name("Created-By")] = BuildConfig.APPLICATION_ID 82 | } 83 | } 84 | 85 | private fun buildManifest(options: Attributes): Manifest { 86 | return Manifest().also { 87 | it.mainAttributes[Attributes.Name.MANIFEST_VERSION] = "1.0" 88 | it.mainAttributes.putAll(options) 89 | } 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/smali/BaksmaliInvoker.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.smali 2 | 3 | import ma.dexter.api.consumer.ClassProgressConsumer 4 | import ma.dexter.dex.MutableClassDef 5 | import ma.dexter.util.normalizeSmaliPath 6 | import org.jf.baksmali.Adaptors.ClassDefinition 7 | import org.jf.baksmali.BaksmaliOptions 8 | import org.jf.baksmali.formatter.BaksmaliWriter 9 | import org.jf.dexlib2.DexFileFactory 10 | import org.jf.dexlib2.iface.ClassDef 11 | import java.io.File 12 | import java.io.StringWriter 13 | import java.util.concurrent.atomic.AtomicInteger 14 | import java.util.zip.ZipEntry 15 | import java.util.zip.ZipOutputStream 16 | 17 | class BaksmaliInvoker( 18 | private val baksmaliOptions: BaksmaliOptions = BaksmaliOptions(), 19 | ) { 20 | 21 | fun disassemble( 22 | classDef: MutableClassDef 23 | ): String { 24 | return disassemble(classDef.classDef) 25 | } 26 | 27 | private fun disassemble( 28 | classDef: ClassDef 29 | ): String { 30 | val writer = StringWriter() 31 | val classDefinition = ClassDefinition(baksmaliOptions, classDef) 32 | classDefinition.writeTo(BaksmaliWriter(writer)) 33 | return writer.toString() 34 | } 35 | 36 | fun disassemble( 37 | dexFile: File, 38 | outZip: File, 39 | progressConsumer: ClassProgressConsumer, 40 | ) { 41 | val classDefs = DexFileFactory.loadDexFile(dexFile, null) 42 | .classes.toSet().filterNotNull() 43 | 44 | val currentClassCount = AtomicInteger() 45 | val totalClassCount = classDefs.size 46 | 47 | ZipOutputStream(outZip.outputStream()).use { zos -> 48 | classDefs.forEach { classDef -> 49 | val smaliPath = normalizeSmaliPath(classDef.type) 50 | progressConsumer.consume(smaliPath, currentClassCount.incrementAndGet(), totalClassCount) 51 | 52 | val zipEntry = ZipEntry("$smaliPath.smali") 53 | zos.putNextEntry(zipEntry) 54 | zos.write(disassemble(classDef).toByteArray()) 55 | zos.closeEntry() 56 | } 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/smali/SmaliInvoker.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.smali 2 | 3 | import ma.dexter.tasks.Result 4 | import ma.dexter.tools.smali.catcherr.smaliCatchErrFlexLexer 5 | import ma.dexter.tools.smali.catcherr.smaliCatchErrParser 6 | import ma.dexter.tools.smali.catcherr.smaliCatchErrTreeWalker 7 | import org.antlr.runtime.CommonTokenStream 8 | import org.antlr.runtime.tree.CommonTreeNodeStream 9 | import org.jf.dexlib2.Opcodes 10 | import org.jf.dexlib2.iface.ClassDef 11 | import org.jf.dexlib2.writer.builder.DexBuilder 12 | import org.jf.smali.SmaliOptions 13 | import java.io.StringReader 14 | 15 | object SmaliInvoker { 16 | 17 | /** 18 | * Assembles given [smaliCode] into a [ClassDef]. 19 | */ 20 | fun assemble( 21 | smaliCode: String, 22 | options: SmaliOptions = SmaliOptions() 23 | ): Result { 24 | 25 | val dexBuilder = DexBuilder(Opcodes.forApi(options.apiLevel)) 26 | 27 | val lexer = smaliCatchErrFlexLexer(StringReader(smaliCode), options.apiLevel) 28 | val tokens = CommonTokenStream(lexer) 29 | 30 | val parser = smaliCatchErrParser(tokens).apply { 31 | setVerboseErrors(options.verboseErrors) 32 | setAllowOdex(options.allowOdexOpcodes) 33 | setApiLevel(options.apiLevel) 34 | } 35 | 36 | val result = parser.smali_file() 37 | 38 | if (lexer.numberOfSyntaxErrors > 0) { 39 | return Result.failure("Lexer", lexer.getErrorsString()) 40 | } 41 | 42 | if (parser.numberOfSyntaxErrors > 0) { 43 | return Result.failure("Parser", parser.getErrorsString()) 44 | } 45 | 46 | val treeStream = CommonTreeNodeStream(result.tree) 47 | treeStream.tokenStream = tokens 48 | 49 | val treeWalker = smaliCatchErrTreeWalker(treeStream).apply { 50 | setApiLevel(options.apiLevel) 51 | setVerboseErrors(options.verboseErrors) 52 | setDexBuilder(dexBuilder) 53 | } 54 | val classDef = treeWalker.smali_file() 55 | 56 | if (treeWalker.numberOfSyntaxErrors > 0) { 57 | return Result.failure("Tree walker", treeWalker.getErrorsString()) 58 | } 59 | 60 | return Result.success(classDef) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/smali/catcherr/SyntaxError.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.smali.catcherr 2 | 3 | class SyntaxError( 4 | val startLine: Int, 5 | val startColumn: Int, 6 | val endLine: Int, 7 | val endColumn: Int, 8 | val message: String 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/smali/catcherr/smaliCatchErrFlexLexer.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.smali.catcherr 2 | 3 | import org.jf.smali.InvalidToken 4 | import org.jf.smali.smaliFlexLexer 5 | import java.io.Reader 6 | 7 | /** 8 | * A sub class of [smaliFlexLexer] that catches errors 9 | * and provides methods to retrieve them. 10 | * 11 | * Doesn't start with an uppercase letter to conform with [smaliFlexLexer]'s name. 12 | */ 13 | class smaliCatchErrFlexLexer( 14 | reader: Reader, 15 | apiLevel: Int 16 | ) : smaliFlexLexer(reader, apiLevel) { 17 | 18 | private val errors = StringBuilder() 19 | private val syntaxErrors = mutableListOf() 20 | 21 | /** 22 | * This is kind of a hack, since [getErrorHeader] only ever gets called 23 | * from [nextToken] (and we can't override [nextToken] since it accesses 24 | * private members), this gives us the offending InvalidToken. 25 | */ 26 | override fun getErrorHeader(invalidToken: InvalidToken): String { 27 | 28 | errors.append("[${invalidToken.line},${invalidToken.charPositionInLine}]") 29 | errors.append(" Error for input '${invalidToken.text}': ${invalidToken.message}") 30 | errors.append("\n") 31 | 32 | syntaxErrors += SyntaxError( 33 | startLine = invalidToken.line, 34 | startColumn = invalidToken.charPositionInLine, 35 | endLine = invalidToken.line, 36 | endColumn = invalidToken.charPositionInLine + invalidToken.text.length, 37 | invalidToken.message 38 | ) 39 | 40 | return super.getErrorHeader(invalidToken) 41 | } 42 | 43 | fun getErrorsString() = errors.toString() 44 | 45 | fun getErrors(): List = syntaxErrors 46 | 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/smali/catcherr/smaliCatchErrParser.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.smali.catcherr 2 | 3 | import org.antlr.runtime.CommonTokenStream 4 | import org.antlr.runtime.RecognitionException 5 | import org.jf.smali.smaliParser 6 | 7 | /** 8 | * A sub class of [smaliParser] that catches errors 9 | * and provides methods to retrieve them. 10 | * 11 | * Doesn't start with an uppercase letter to conform with [smaliParser]'s name. 12 | */ 13 | class smaliCatchErrParser( 14 | tokens: CommonTokenStream 15 | ) : smaliParser(tokens) { 16 | 17 | private val errors = StringBuilder() 18 | private val syntaxErrors = mutableListOf() 19 | 20 | override fun emitErrorMessage(msg: String?) { 21 | errors.append(msg) 22 | errors.append("\n") 23 | } 24 | 25 | fun getErrorsString() = errors.toString() 26 | 27 | override fun displayRecognitionError(tokenNames: Array?, e: RecognitionException) { 28 | 29 | syntaxErrors += SyntaxError( 30 | startLine = e.line, 31 | startColumn = e.charPositionInLine, 32 | endLine = e.line, 33 | endColumn = e.charPositionInLine + (e.token?.text?.length ?: 1), 34 | getErrorMessage(e, tokenNames) 35 | ) 36 | 37 | super.displayRecognitionError(tokenNames, e) 38 | } 39 | 40 | fun getErrors(): List = syntaxErrors 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/tools/smali/catcherr/smaliCatchErrTreeWalker.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.smali.catcherr 2 | 3 | import org.antlr.runtime.RecognitionException 4 | import org.antlr.runtime.tree.CommonTreeNodeStream 5 | import org.jf.smali.smaliTreeWalker 6 | 7 | /** 8 | * A sub class of [smaliTreeWalker] that catches errors 9 | * and provides methods to retrieve them. 10 | * 11 | * Doesn't start with an uppercase letter to conform with [smaliTreeWalker]'s name. 12 | */ 13 | class smaliCatchErrTreeWalker( 14 | treeStream: CommonTreeNodeStream 15 | ) : smaliTreeWalker(treeStream) { 16 | 17 | private val errors = StringBuilder() 18 | private val syntaxErrors = mutableListOf() 19 | 20 | override fun emitErrorMessage(msg: String?) { 21 | errors.append(msg) 22 | errors.append("\n") 23 | } 24 | 25 | fun getErrorsString() = errors.toString() 26 | 27 | override fun displayRecognitionError(tokenNames: Array?, e: RecognitionException) { 28 | 29 | syntaxErrors += SyntaxError( 30 | startLine = e.line, 31 | startColumn = e.charPositionInLine, 32 | endLine = e.line, 33 | endColumn = e.charPositionInLine + (e.token?.text?.length ?: 1), 34 | getErrorMessage(e, tokenNames) 35 | ) 36 | 37 | super.displayRecognitionError(tokenNames, e) 38 | } 39 | 40 | fun getErrors(): List = syntaxErrors 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | 5 | open class BaseActivity: AppCompatActivity() { 6 | 7 | var subtitle: CharSequence 8 | get() = supportActionBar?.subtitle ?: "" 9 | set(value) { 10 | supportActionBar?.subtitle = value 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.core.content.ContextCompat 5 | import androidx.fragment.app.Fragment 6 | 7 | open class BaseFragment : Fragment() { 8 | 9 | fun drawable(@DrawableRes drawableRes: Int) = 10 | ContextCompat.getDrawable(requireContext(), drawableRes) 11 | 12 | override fun onResume() { 13 | super.onResume() 14 | 15 | setHasOptionsMenu(isVisible) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/adapter/DexPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.adapter 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentActivity 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.viewpager2.adapter.FragmentStateAdapter 7 | import ma.dexter.ui.fragment.DexEditorFragment 8 | import ma.dexter.ui.fragment.JavaViewerFragment 9 | import ma.dexter.ui.fragment.SmaliEditorFragment 10 | import ma.dexter.ui.model.DexPageItem 11 | import ma.dexter.ui.model.JavaItem 12 | import ma.dexter.ui.model.MainItem 13 | import ma.dexter.ui.model.SmaliItem 14 | 15 | class DexPagerAdapter( 16 | fragmentActivity: FragmentActivity 17 | ) : FragmentStateAdapter(fragmentActivity) { 18 | 19 | private val data = mutableListOf() 20 | 21 | fun updateList(newData: List) { 22 | val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() { 23 | override fun getOldListSize() = data.size 24 | 25 | override fun getNewListSize() = newData.size 26 | 27 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 28 | val areSmaliItems = 29 | data[oldItemPosition] is SmaliItem && newData[newItemPosition] is SmaliItem 30 | val areJavaItems = 31 | data[oldItemPosition] is JavaItem && newData[newItemPosition] is JavaItem 32 | 33 | return (areJavaItems || areSmaliItems) 34 | && itemId(data[oldItemPosition]) == itemId(newData[newItemPosition]) 35 | } 36 | 37 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 38 | return data[oldItemPosition].getGotoDef() == newData[newItemPosition].getGotoDef() 39 | } 40 | }) 41 | data.clear() 42 | data.addAll(newData) 43 | diffResult.dispatchUpdatesTo(this) 44 | } 45 | 46 | fun getItem(position: Int) = data[position] 47 | 48 | override fun createFragment(position: Int): Fragment { 49 | return when ( 50 | val item = getItem(position) 51 | ) { 52 | is MainItem -> DexEditorFragment() 53 | is SmaliItem -> SmaliEditorFragment(item.smaliGotoDef) 54 | is JavaItem -> JavaViewerFragment(item.javaGotoDef) 55 | } 56 | } 57 | 58 | override fun getItemId(position: Int): Long { 59 | return if (data.isEmpty() || position > data.size) { 60 | -1 61 | } else itemId(getItem(position)) 62 | } 63 | 64 | fun itemId(dexPageItem: DexPageItem): Long { 65 | return dexPageItem.typeDef.hashCode().toLong() 66 | } 67 | 68 | override fun containsItem(itemId: Long): Boolean { 69 | data.forEach { 70 | if (it.typeDef.hashCode().toLong() == itemId) { 71 | return true 72 | } 73 | } 74 | return false 75 | } 76 | 77 | override fun getItemCount() = data.size 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/dialog/ProgressDialog.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.dialog 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import androidx.appcompat.app.AlertDialog 6 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 7 | import ma.dexter.databinding.DialogProgressSimpleBinding 8 | 9 | class ProgressDialog( 10 | private val context: Context, 11 | private val title: String, 12 | private val message: String = "", 13 | private val cancelable: Boolean = false 14 | ) { 15 | private lateinit var viewBinding: DialogProgressSimpleBinding 16 | private lateinit var backingDialog: AlertDialog 17 | 18 | fun create(): ProgressDialog { 19 | viewBinding = DialogProgressSimpleBinding.inflate(LayoutInflater.from(context)) 20 | viewBinding.message.text = message 21 | 22 | backingDialog = MaterialAlertDialogBuilder(context) 23 | .setTitle(title) 24 | .setCancelable(cancelable) 25 | .setView(viewBinding.root) 26 | .create() 27 | 28 | return this 29 | } 30 | 31 | fun show(): ProgressDialog { 32 | create() 33 | backingDialog.show() 34 | return this 35 | } 36 | 37 | fun dismiss() { 38 | backingDialog.dismiss() 39 | } 40 | 41 | fun setMessage(message: String) { 42 | viewBinding.message.text = message 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/editor/lang/smali/SmaliAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.editor.lang.smali 2 | 3 | import io.github.rosemoe.sora.data.BlockLine 4 | import io.github.rosemoe.sora.data.Span 5 | import io.github.rosemoe.sora.interfaces.CodeAnalyzer 6 | import io.github.rosemoe.sora.text.TextAnalyzeResult 7 | import io.github.rosemoe.sora.text.TextAnalyzer 8 | import io.github.rosemoe.sora.widget.EditorColorScheme 9 | import ma.dexter.ui.editor.lang.smali.model.SmaliAutoCompleteModel 10 | import ma.dexter.ui.editor.lang.smali.model.SmaliClassDesc 11 | import ma.dexter.ui.editor.scheme.smali.SmaliBaseScheme 12 | import ma.dexter.tools.smali.catcherr.smaliCatchErrFlexLexer 13 | import ma.dexter.tools.smali.catcherr.smaliCatchErrParser 14 | import org.antlr.runtime.CommonTokenStream 15 | import org.antlr.runtime.Token 16 | import org.jf.smali.smaliFlexLexer 17 | import org.jf.smali.smaliParser 18 | import java.io.StringReader 19 | 20 | class SmaliAnalyzer : CodeAnalyzer { 21 | 22 | override fun analyze( 23 | content: CharSequence, 24 | colors: TextAnalyzeResult, 25 | delegate: TextAnalyzer.AnalyzeThread.Delegate 26 | ) { 27 | val text = if (content is StringBuilder) content else StringBuilder(content) 28 | 29 | // For completion TODO 30 | val classDescList = mutableSetOf() 31 | 32 | // For drawing lines between '.method' and '.end method' directives 33 | var lastBlockline: BlockLine? = null 34 | 35 | // Set the apiLevel to 31 so that 36 | // most recent dex opcodes are also supported. 37 | val lexer = smaliFlexLexer(StringReader(text.toString()), 31) 38 | lexer.setSuppressErrors(true) 39 | 40 | var token: Token 41 | var lastLine = 1 42 | 43 | while (delegate.shouldAnalyze()) { 44 | token = lexer.nextToken() 45 | 46 | if (token == null || token.type == smaliParser.EOF) break 47 | if (token.type == smaliParser.WHITE_SPACE) continue 48 | 49 | val line = token.line - 1 50 | lastLine = line 51 | val column = token.charPositionInLine 52 | 53 | when (token.type) { 54 | 55 | in directives -> { 56 | colors.addIfNeeded(line, column, SmaliBaseScheme.DIRECTIVE) 57 | 58 | if (token.type == smaliParser.METHOD_DIRECTIVE) { 59 | lastBlockline = colors.obtainNewBlock().apply { 60 | startLine = line 61 | startColumn = column 62 | } 63 | } 64 | 65 | if (token.type == smaliParser.END_METHOD_DIRECTIVE) { 66 | lastBlockline?.run { 67 | endLine = line 68 | endColumn = column 69 | 70 | if (startLine != endLine) colors.addBlockLine(this) 71 | } 72 | } 73 | } 74 | 75 | smaliParser.ANNOTATION_VISIBILITY, 76 | smaliParser.ACCESS_SPEC, 77 | in instructions -> { 78 | colors.addIfNeeded(line, column, SmaliBaseScheme.ACCESS_MODIFIER) 79 | } 80 | 81 | smaliParser.CLASS_DESCRIPTOR, 82 | smaliParser.ARRAY_TYPE_PREFIX, 83 | smaliParser.PRIMITIVE_TYPE, 84 | smaliParser.PARAM_LIST_OR_ID_PRIMITIVE_TYPE, 85 | smaliParser.VOID_TYPE -> { 86 | colors.addIfNeeded(line, column, SmaliBaseScheme.CLASS_DESCRIPTOR) 87 | 88 | if (token.type == smaliParser.CLASS_DESCRIPTOR) { 89 | classDescList += SmaliClassDesc(token.text) 90 | } 91 | } 92 | 93 | smaliParser.POSITIVE_INTEGER_LITERAL, 94 | smaliParser.NEGATIVE_INTEGER_LITERAL, 95 | smaliParser.INTEGER_LITERAL -> { 96 | colors.addIfNeeded(line, column, SmaliBaseScheme.INT_LITERAL) 97 | } 98 | 99 | smaliParser.REGISTER -> { 100 | colors.addIfNeeded(line, column, SmaliBaseScheme.REGISTER) 101 | } 102 | 103 | smaliParser.ARROW, 104 | smaliParser.MEMBER_NAME, 105 | smaliParser.SIMPLE_NAME -> { 106 | colors.addIfNeeded(line, column, SmaliBaseScheme.SIMPLE_NAME) 107 | } 108 | 109 | smaliParser.STRING_LITERAL -> { 110 | colors.addIfNeeded(line, column, SmaliBaseScheme.STRING_LITERAL) 111 | } 112 | 113 | smaliParser.LINE_COMMENT -> { 114 | colors.addIfNeeded(line, column, SmaliBaseScheme.LINE_COMMENT) 115 | } 116 | 117 | else -> { 118 | colors.addIfNeeded(line, column, EditorColorScheme.TEXT_NORMAL) 119 | } 120 | } 121 | } 122 | 123 | colors.extra = SmaliAutoCompleteModel(classDescList) 124 | colors.determine(lastLine) 125 | 126 | markSyntaxErrors(text.toString(), colors) 127 | } 128 | 129 | // todo: should we care about the errors produced by [org.jf.smali.smaliTreeWalker] ? 130 | private fun markSyntaxErrors( 131 | smaliCode: String, 132 | colors: TextAnalyzeResult 133 | ) { 134 | val lexer = smaliCatchErrFlexLexer(StringReader(smaliCode), 31) 135 | val parser = smaliCatchErrParser(CommonTokenStream(lexer)) 136 | 137 | parser.smali_file() 138 | 139 | // todo: show error message somehow 140 | (lexer.getErrors() + parser.getErrors()).forEach { 141 | colors.markProblemRegion( 142 | Span.FLAG_ERROR, 143 | it.startLine - 1, 144 | it.startColumn, 145 | it.endLine - 1, 146 | it.endColumn 147 | ) 148 | } 149 | } 150 | 151 | companion object { 152 | private val instructions = 44..94 // see smaliParser 153 | 154 | private val directives by lazy { // see smaliParser 155 | buildList { 156 | smaliParser.tokenNames.forEachIndexed { index, s -> 157 | if (s.endsWith("_DIRECTIVE")) { 158 | add(index) 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/editor/lang/smali/SmaliLanguage.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.editor.lang.smali 2 | 3 | import io.github.rosemoe.sora.interfaces.EditorLanguage 4 | import io.github.rosemoe.sora.interfaces.NewlineHandler 5 | import io.github.rosemoe.sora.langs.internal.MyCharacter 6 | import io.github.rosemoe.sora.widget.SymbolPairMatch 7 | 8 | class SmaliLanguage: EditorLanguage { 9 | override fun getAnalyzer() = SmaliAnalyzer() 10 | 11 | override fun getAutoCompleteProvider() = SmaliAutoCompleteProvider() 12 | 13 | override fun isAutoCompleteChar(p0: Char): Boolean { 14 | return MyCharacter.isJavaIdentifierPart(p0.code) 15 | 16 | // for instructions like const-wide/high16 17 | || p0 in "0123456789-/" 18 | 19 | // for directives 20 | || p0 == '.' 21 | } 22 | 23 | override fun format(p0: CharSequence) = p0 24 | 25 | override fun getIndentAdvance(p0: String?) = 0 26 | 27 | override fun useTab() = true 28 | 29 | override fun getSymbolPairs() = SymbolPairMatch.DefaultSymbolPairs() 30 | 31 | override fun getNewlineHandlers() = arrayOf() 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/editor/lang/smali/model/SmaliAutoCompleteModels.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.editor.lang.smali.model 2 | 3 | data class SmaliAutoCompleteModel( 4 | val classDescs: Set 5 | ) 6 | 7 | data class SmaliClassDesc( 8 | val name: String 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/editor/scheme/smali/SchemeLightSmali.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.editor.scheme.smali 2 | 3 | import io.github.rosemoe.sora.widget.EditorColorScheme 4 | 5 | class SchemeLightSmali : EditorColorScheme() { 6 | 7 | override fun applyDefault() { 8 | super.applyDefault() 9 | 10 | // Smali specific colors 11 | run { 12 | color(SmaliBaseScheme.DIRECTIVE, 0xff800000) 13 | color(SmaliBaseScheme.ACCESS_MODIFIER, 0xff001BA3) 14 | color(SmaliBaseScheme.CLASS_DESCRIPTOR, 0xff808000) 15 | color(SmaliBaseScheme.INT_LITERAL, 0xff1750EB) 16 | color(SmaliBaseScheme.REGISTER, 0xff1750EB) 17 | color(SmaliBaseScheme.STRING_LITERAL, 0xff067D17) 18 | color(SmaliBaseScheme.SIMPLE_NAME, 0xff205060) 19 | color(SmaliBaseScheme.LINE_COMMENT, 0xff8C8C8C) 20 | } 21 | 22 | color(WHOLE_BACKGROUND, 0xffffffff) 23 | color(TEXT_NORMAL, 0xff000000) 24 | 25 | color(LINE_NUMBER_BACKGROUND, 0xffF0F0F0) 26 | color(LINE_NUMBER, 0xffB0B0B0) 27 | color(LINE_DIVIDER, 0xffB0B0B0) 28 | 29 | color(SCROLL_BAR_THUMB, 0xffa6a6a6) 30 | color(SCROLL_BAR_THUMB_PRESSED, 0xff565656) 31 | 32 | color(SELECTED_TEXT_BACKGROUND, 0xff3676b8) 33 | color(MATCHED_TEXT_BACKGROUND, 0xff32593d) 34 | 35 | color(CURRENT_LINE, 0xffFFFAE3) 36 | color(SELECTION_INSERT, 0xff42A5F5) 37 | color(SELECTION_HANDLE, 0xff42A5F5) 38 | 39 | color(BLOCK_LINE, 0xffdddddd) 40 | color(BLOCK_LINE_CURRENT, 0xff999999) 41 | color(NON_PRINTABLE_CHAR, 0xffdddddd) 42 | 43 | color(PROBLEM_ERROR, 0xFFFF0000) 44 | } 45 | 46 | // Quick workaround for Kotlin's naiveness 47 | private fun color(type: Int, color: Long) { 48 | setColor(type, color.toInt()) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/editor/scheme/smali/SmaliBaseScheme.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.editor.scheme.smali 2 | 3 | class SmaliBaseScheme { 4 | 5 | companion object { 6 | const val DIRECTIVE = 101 7 | const val ACCESS_MODIFIER = 102 8 | const val CLASS_DESCRIPTOR = 103 9 | const val INT_LITERAL = 104 10 | const val REGISTER = 105 11 | const val SIMPLE_NAME = 106 12 | const val STRING_LITERAL = 107 13 | const val LINE_COMMENT = 108 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/editor/util/SingleEditorEventListener.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.editor.util 2 | 3 | import io.github.rosemoe.sora.interfaces.EditorEventListener 4 | import io.github.rosemoe.sora.text.Cursor 5 | import io.github.rosemoe.sora.widget.CodeEditor 6 | 7 | open class SingleEditorEventListener: EditorEventListener { 8 | override fun onRequestFormat(editor: CodeEditor) = false 9 | 10 | override fun onFormatFail(editor: CodeEditor, cause: Throwable?) = false 11 | 12 | override fun onFormatSucceed(editor: CodeEditor) {} 13 | 14 | override fun onNewTextSet(editor: CodeEditor) {} 15 | 16 | override fun afterDelete( 17 | editor: CodeEditor, 18 | content: CharSequence, 19 | startLine: Int, 20 | startColumn: Int, 21 | endLine: Int, 22 | endColumn: Int, 23 | deletedContent: CharSequence? 24 | ) {} 25 | 26 | override fun afterInsert( 27 | editor: CodeEditor, 28 | content: CharSequence, 29 | startLine: Int, 30 | startColumn: Int, 31 | endLine: Int, 32 | endColumn: Int, 33 | insertedContent: CharSequence? 34 | ) {} 35 | 36 | override fun beforeReplace(editor: CodeEditor, content: CharSequence) {} 37 | 38 | override fun onSelectionChanged(editor: CodeEditor, cursor: Cursor) {} 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/editor/util/Util.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.editor.util 2 | 3 | import android.view.View 4 | import android.view.inputmethod.EditorInfo 5 | import io.github.rosemoe.sora.widget.CodeEditor 6 | 7 | fun CodeEditor.setDefaults() { 8 | isLigatureEnabled = true 9 | isOverScrollEnabled = false 10 | inputType = 11 | EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS or EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE or EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD 12 | importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO 13 | 14 | setTextSize(12f) 15 | } 16 | 17 | fun CodeEditor.setOnTextChangedListener( 18 | listener: () -> Unit 19 | ) { 20 | setSingleEventListener(object : SingleEditorEventListener() { 21 | override fun onNewTextSet(editor: CodeEditor) { 22 | listener() 23 | } 24 | 25 | override fun afterDelete( 26 | editor: CodeEditor, content: CharSequence, 27 | startLine: Int, startColumn: Int, 28 | endLine: Int, endColumn: Int, deletedContent: CharSequence? 29 | ) { 30 | listener() 31 | } 32 | 33 | override fun afterInsert( 34 | editor: CodeEditor, content: CharSequence, 35 | startLine: Int, startColumn: Int, 36 | endLine: Int, endColumn: Int, insertedContent: CharSequence? 37 | ) { 38 | listener() 39 | } 40 | }) 41 | } 42 | 43 | fun CodeEditor.setSingleEventListener( 44 | listener: SingleEditorEventListener 45 | ) { 46 | setEventListener(listener) 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/editor/util/smali/SmaliActionPopupWindow.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.editor.util.smali 2 | 3 | import androidx.core.content.ContextCompat 4 | import androidx.fragment.app.FragmentActivity 5 | import com.google.android.material.button.MaterialButton 6 | import io.github.rosemoe.sora.widget.CodeEditor 7 | import io.github.rosemoe.sora.widget.TextActionPopupWindow 8 | import ma.dexter.R 9 | import ma.dexter.dex.DexGotoManager 10 | 11 | class SmaliActionPopupWindow( 12 | private val fragmentActivity: FragmentActivity, 13 | private val editor: CodeEditor 14 | ) : TextActionPopupWindow(editor) { 15 | 16 | init { 17 | 18 | val moreButton = contentView.findViewById( 19 | io.github.rosemoe.sora.R.id.tcpw_material_button_save_to_project 20 | ) 21 | 22 | moreButton.run { 23 | text = context.getString(R.string.smali_goto) 24 | icon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_arrow_right_alt_20) 25 | 26 | setOnClickListener { 27 | DexGotoManager(fragmentActivity) 28 | .gotoDef(editor) 29 | } 30 | } 31 | 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/fragment/BaseCodeEditorFragment.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.fragment 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.view.* 7 | import androidx.activity.result.contract.ActivityResultContracts 8 | import androidx.annotation.CallSuper 9 | import androidx.fragment.app.activityViewModels 10 | import com.github.zawadz88.materialpopupmenu.MaterialPopupMenuBuilder 11 | import io.github.rosemoe.sora.widget.CodeEditor 12 | import ma.dexter.R 13 | import ma.dexter.databinding.FragmentBaseCodeEditorBinding 14 | import ma.dexter.ui.BaseFragment 15 | import ma.dexter.ui.editor.util.setDefaults 16 | import ma.dexter.ui.editor.util.setOnTextChangedListener 17 | import ma.dexter.ui.util.checkableItem 18 | import ma.dexter.ui.viewmodel.MainViewModel 19 | 20 | open class BaseCodeEditorFragment : BaseFragment() { 21 | private lateinit var binding: FragmentBaseCodeEditorBinding 22 | private val viewModel: MainViewModel by activityViewModels() 23 | 24 | protected lateinit var codeEditor: CodeEditor 25 | private var isEdited = false 26 | 27 | private val exportResultLauncher = 28 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> 29 | if (result.resultCode == Activity.RESULT_OK) { 30 | val uri = result.data?.data ?: return@registerForActivityResult 31 | val contentResolver = activity?.contentResolver ?: return@registerForActivityResult 32 | 33 | contentResolver.openOutputStream(uri)?.use { 34 | val code = codeEditor.text.toString() 35 | it.write(code.toByteArray()) 36 | } 37 | } 38 | } 39 | 40 | @CallSuper 41 | override fun onCreateView( 42 | inflater: LayoutInflater, 43 | container: ViewGroup?, 44 | savedInstanceState: Bundle? 45 | ): View { 46 | binding = FragmentBaseCodeEditorBinding.inflate(inflater, container, false) 47 | codeEditor = binding.codeEditor 48 | return binding.root 49 | } 50 | 51 | @CallSuper 52 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 53 | codeEditor.setDefaults() 54 | 55 | codeEditor.setOnTextChangedListener { 56 | isEdited = true 57 | } 58 | 59 | viewModel.viewPagerScrolled.observe(viewLifecycleOwner) { 60 | codeEditor.hideAutoCompleteWindow() 61 | codeEditor.textActionPresenter.onExit() 62 | } 63 | } 64 | 65 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 66 | inflater.inflate(R.menu.menu_base_code_editor, menu) 67 | } 68 | 69 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 70 | when (item.itemId) { 71 | R.id.it_undo -> codeEditor.undo() 72 | R.id.it_redo -> codeEditor.redo() 73 | R.id.it_save -> save() 74 | R.id.it_more -> showMoreMenu(requireActivity().findViewById(R.id.it_more)) 75 | } 76 | 77 | return true 78 | } 79 | 80 | // @CallSuper 81 | protected open fun save() { 82 | 83 | } 84 | 85 | private fun export() { 86 | val currentTabTitle = viewModel.currentPosition.value?.let { pos -> 87 | val pageItems = viewModel.getPageItems().value 88 | pageItems?.get(pos)?.getTitle() 89 | } 90 | 91 | val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { 92 | addCategory(Intent.CATEGORY_OPENABLE) 93 | type = "*/*" 94 | putExtra(Intent.EXTRA_TITLE, currentTabTitle.orEmpty()) 95 | } 96 | 97 | exportResultLauncher.launch(intent) 98 | } 99 | 100 | private fun showMoreMenu(anchorView: View) { 101 | val builder = MaterialPopupMenuBuilder() 102 | 103 | beforeBuildMoreMenu(builder) 104 | builder.section { 105 | title = "File" 106 | 107 | item { 108 | label = "Export.." 109 | iconDrawable = drawable(R.drawable.ic_baseline_save_alt_24) 110 | callback = ::export 111 | } 112 | } 113 | builder.section { 114 | title = "Editor" 115 | 116 | item { 117 | label = "Search" 118 | iconDrawable = drawable(R.drawable.ic_baseline_search_24) 119 | callback = codeEditor::beginSearchMode 120 | } 121 | 122 | customItem { 123 | checkableItem { 124 | label = "Word wrap" 125 | iconDrawable = drawable(R.drawable.ic_baseline_wrap_text_24) 126 | checked = codeEditor.isWordwrap 127 | callback = { 128 | codeEditor.isWordwrap = it 129 | } 130 | } 131 | } 132 | 133 | customItem { 134 | checkableItem { 135 | label = "Auto complete" 136 | iconDrawable = drawable(R.drawable.ic_baseline_copyright_24) 137 | checked = codeEditor.isAutoCompletionEnabled 138 | callback = { 139 | codeEditor.isAutoCompletionEnabled = it 140 | } 141 | } 142 | } 143 | 144 | customItem { 145 | checkableItem { 146 | label = "Magnifier" 147 | iconDrawable = drawable(R.drawable.ic_baseline_zoom_in_24) 148 | checked = codeEditor.isMagnifierEnabled 149 | callback = { 150 | codeEditor.isMagnifierEnabled = it 151 | } 152 | } 153 | } 154 | } 155 | afterBuildMoreMenu(builder) 156 | 157 | builder.build() 158 | .show(requireContext(), anchorView) 159 | } 160 | 161 | protected open fun beforeBuildMoreMenu(popupMenuBuilder: MaterialPopupMenuBuilder) { 162 | // no-op 163 | } 164 | 165 | protected open fun afterBuildMoreMenu(popupMenuBuilder: MaterialPopupMenuBuilder) { 166 | // no-op 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/fragment/DexEditorFragment.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.fragment 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ArrayAdapter 8 | import android.widget.PopupMenu 9 | import androidx.fragment.app.activityViewModels 10 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 11 | import ma.dexter.R 12 | import ma.dexter.databinding.DialogCreateSmaliFileBinding 13 | import ma.dexter.databinding.FragmentDexEditorBinding 14 | import ma.dexter.dex.DexGotoManager 15 | import ma.dexter.project.Workspace 16 | import ma.dexter.ui.BaseActivity 17 | import ma.dexter.ui.BaseFragment 18 | import ma.dexter.ui.tree.TreeNode 19 | import ma.dexter.ui.tree.TreeView 20 | import ma.dexter.ui.tree.dex.DexClassNode 21 | import ma.dexter.ui.tree.dex.SmaliTree 22 | import ma.dexter.ui.tree.dex.binder.DexItemNodeViewFactory 23 | import ma.dexter.ui.viewmodel.MainViewModel 24 | import ma.dexter.util.createClassDef 25 | import ma.dexter.util.getClassDescriptor 26 | import ma.dexter.util.getPath 27 | import ma.dexter.util.toast 28 | 29 | class DexEditorFragment : BaseFragment() { 30 | private lateinit var binding: FragmentDexEditorBinding 31 | private lateinit var treeView: TreeView 32 | 33 | private val viewModel: MainViewModel by activityViewModels() 34 | 35 | override fun onCreateView( 36 | inflater: LayoutInflater, 37 | container: ViewGroup?, 38 | savedInstanceState: Bundle? 39 | ): View { 40 | binding = FragmentDexEditorBinding.inflate(inflater, container, false) 41 | return binding.root 42 | } 43 | 44 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 45 | viewModel.dexProject.observe(viewLifecycleOwner) { 46 | Workspace.openProject(it) 47 | structureTree() 48 | } 49 | } 50 | 51 | private fun structureTree() { 52 | val dexTree = SmaliTree() 53 | .addDexEntries( 54 | Workspace.getOpenedProject() 55 | .dexContainer.entries 56 | ) 57 | 58 | val binder = DexItemNodeViewFactory( 59 | toggleListener = { treeNode -> 60 | if (treeNode.isLeaf) { 61 | DexGotoManager(requireActivity()) 62 | .gotoClassDef(treeNode.getClassDescriptor()) 63 | } 64 | }, 65 | 66 | longClickListener = { view, treeNode -> 67 | val popupMenu = PopupMenu(context, view) 68 | popupMenu.menuInflater.inflate(R.menu.menu_dex_tree_item, popupMenu.menu) 69 | popupMenu.setOnMenuItemClickListener { 70 | when (it.itemId) { 71 | R.id.it_add -> addClass(treeNode) 72 | R.id.it_delete -> deleteClass(treeNode) 73 | } 74 | 75 | true 76 | } 77 | popupMenu.show() 78 | 79 | true 80 | } 81 | ) 82 | 83 | treeView = TreeView( 84 | dexTree.createTree(), 85 | requireContext(), 86 | binder 87 | ) 88 | 89 | val treeRecyclerView = treeView.view 90 | 91 | binding.root.removeAllViews() 92 | binding.root.addView(treeRecyclerView, ViewGroup.LayoutParams(-1, -1)) 93 | 94 | // TODO: clean-up 95 | treeRecyclerView.setOnScrollChangeListener { _, _, _, _, _ -> 96 | val currentPackage = buildString { 97 | val pos = treeView.layoutManager.findFirstVisibleItemPosition() 98 | var treeNode = treeView.adapter.expandedNodeList[pos] 99 | 100 | while (!treeNode.isRoot) { 101 | treeNode = treeNode.parent 102 | 103 | if (!treeNode.isLeaf && !treeNode.isRoot) { 104 | insert(0, treeNode.value.toString() + ".") 105 | } 106 | } 107 | 108 | if (length != 0) setLength(length - 1) // strip the last "." 109 | } 110 | 111 | (requireActivity() as BaseActivity).subtitle = currentPackage 112 | } 113 | } 114 | 115 | private fun deleteClass( 116 | treeNode: TreeNode 117 | ) { 118 | val dexContainer = Workspace.getOpenedProject() 119 | .dexContainer 120 | 121 | if (treeNode.isLeaf) { 122 | dexContainer.deleteClassDef(treeNode.getClassDescriptor()) 123 | } else { 124 | dexContainer.deletePackage(treeNode.getPath()) 125 | } 126 | 127 | treeNode.parent.removeChild(treeNode) 128 | refreshTreeView() 129 | } 130 | 131 | private fun addClass( 132 | treeNode: TreeNode? 133 | ) { 134 | if (treeNode == null) return 135 | 136 | if (treeNode.isLeaf) { 137 | addClass(treeNode.parent) 138 | return 139 | } 140 | 141 | val dialogBinding = DialogCreateSmaliFileBinding.inflate(layoutInflater) 142 | val biMap = Workspace.getOpenedProject() 143 | .dexContainer.biMap 144 | 145 | dialogBinding.etDexFile.setAdapter( 146 | ArrayAdapter( 147 | requireContext(), android.R.layout.simple_list_item_1, 148 | biMap.values.toList() 149 | ) 150 | ) 151 | 152 | dialogBinding.etPackageName.setText(treeNode.getPath()) 153 | 154 | MaterialAlertDialogBuilder(requireContext()) 155 | .setTitle("Create class") 156 | .setView(dialogBinding.root) 157 | .setPositiveButton("OK") { _, _ -> 158 | val className = dialogBinding.etClassName.text.toString() 159 | var packagePath = dialogBinding.etPackageName.text.toString() 160 | 161 | // "" -> "" 162 | // "/" -> "" 163 | // "a" -> "a/" 164 | // "a/" -> "a/" 165 | if (packagePath == "/") { 166 | packagePath = "" 167 | } else if (packagePath.isNotEmpty() && !packagePath.endsWith("/")) { 168 | packagePath += "/" 169 | } 170 | 171 | val dexFile = dialogBinding.etDexFile.text.toString() 172 | 173 | if (className.isEmpty() || dexFile.isEmpty()) { 174 | toast("Please fill in all fields") 175 | return@setPositiveButton 176 | } 177 | 178 | val classDef = createClassDef("L$packagePath$className;") 179 | val dex = biMap.inverse()[dexFile]!! 180 | 181 | dex.addClassDef(classDef) 182 | 183 | // costly operation but ensures that the tree is structured correctly 184 | // TODO: optimize 185 | structureTree() 186 | } 187 | .show() 188 | } 189 | 190 | private fun refreshTreeView() { 191 | treeView.refreshTreeView() 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/fragment/JavaViewerFragment.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.fragment 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import com.github.zawadz88.materialpopupmenu.MaterialPopupMenuBuilder 6 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 7 | import io.github.rosemoe.sora.langs.java.JavaLanguage 8 | import ma.dexter.R 9 | import ma.dexter.model.JavaGotoDef 10 | import ma.dexter.parsers.java.JavaMember 11 | import ma.dexter.parsers.java.parseJava 12 | import ma.dexter.ui.editor.scheme.smali.SchemeLightSmali 13 | import ma.dexter.util.hideKeyboard 14 | 15 | class JavaViewerFragment( 16 | private val javaGotoDef: JavaGotoDef 17 | ) : BaseCodeEditorFragment() { 18 | 19 | // parse the Java code only once since it won't change 20 | private val javaFile by lazy { 21 | parseJava(codeEditor.text.toString()) 22 | } 23 | 24 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 25 | super.onViewCreated(view, savedInstanceState) 26 | 27 | with(codeEditor) { 28 | colorScheme = SchemeLightSmali() 29 | isEditable = false 30 | 31 | setText(javaGotoDef.javaCode) 32 | setEditorLanguage(JavaLanguage()) 33 | } 34 | } 35 | 36 | override fun beforeBuildMoreMenu(popupMenuBuilder: MaterialPopupMenuBuilder) { 37 | popupMenuBuilder.section { 38 | item { 39 | label = "Navigation" 40 | iconDrawable = drawable(R.drawable.ic_baseline_view_stream_24) 41 | callback = ::showNavigationDialog 42 | } 43 | } 44 | } 45 | 46 | private fun showNavigationDialog() { 47 | val navItems = javaFile.members 48 | 49 | MaterialAlertDialogBuilder(requireContext()) 50 | .setTitle("Navigation") 51 | .setItems(navItems.map { it.name }.toTypedArray()) { _, pos -> 52 | gotoMemberDefinition(navItems[pos]) 53 | } 54 | .show() 55 | } 56 | 57 | private fun gotoMemberDefinition(member: JavaMember) { 58 | hideKeyboard(codeEditor) 59 | 60 | codeEditor.jumpToLine(member.line) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/model/DexPageItems.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.model 2 | 3 | import ma.dexter.R 4 | import ma.dexter.model.GotoDef 5 | import ma.dexter.model.JavaGotoDef 6 | import ma.dexter.model.SmaliGotoDef 7 | import ma.dexter.util.getClassNameFromSmaliPath 8 | 9 | sealed class DexPageItem(val typeDef: String?) { 10 | abstract fun getTitle(): String 11 | abstract fun getIconResId(): Int 12 | abstract fun getGotoDef(): GotoDef? 13 | } 14 | 15 | // guaranteed to be at position 0 (for now) 16 | class MainItem : DexPageItem(null) { 17 | override fun getTitle() = "TREE" 18 | override fun getIconResId() = R.drawable.ic_baseline_home_24 19 | override fun getGotoDef(): GotoDef? = null 20 | } 21 | 22 | class SmaliItem( 23 | val smaliGotoDef: SmaliGotoDef 24 | ) : DexPageItem(smaliGotoDef.classDef.type) { 25 | 26 | override fun getTitle() = 27 | getClassNameFromSmaliPath(smaliGotoDef.classDef.type) + ".smali" 28 | 29 | override fun getIconResId() = R.drawable.ic_letter_s_24 30 | 31 | override fun getGotoDef() = smaliGotoDef 32 | } 33 | 34 | class JavaItem( 35 | val javaGotoDef: JavaGotoDef 36 | ) : DexPageItem(javaGotoDef.className) { 37 | 38 | override fun getTitle() = 39 | javaGotoDef.className.substringAfterLast("/") + ".java" 40 | 41 | override fun getIconResId() = R.drawable.ic_java 42 | 43 | override fun getGotoDef() = javaGotoDef 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/tree/TreeUtil.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.tree 2 | 3 | /** 4 | * Sort children recursively in the TreeNode. 5 | */ 6 | fun TreeNode.sort(comparator: Comparator>) { 7 | children.forEach { 8 | it.sort(comparator) 9 | } 10 | 11 | children = children.sortedWith(comparator) 12 | } 13 | 14 | /** 15 | * Find child by value in the TreeNode. 16 | * 17 | * Doesn't do a recursive search. 18 | */ 19 | fun TreeNode.findChildByValue(value: D): TreeNode? { 20 | this.children.forEach { 21 | if (it.value == value) { 22 | return it 23 | } 24 | } 25 | 26 | return null 27 | } 28 | 29 | /** 30 | * Compacts middle packages/folders in the tree structure. Example: 31 | * 32 | * Input Output 33 | * a a 34 | * b b 35 | * c c.d.e 36 | * - d - f 37 | * - - e g 38 | * - - - f - h.i 39 | * g - - j 40 | * - h - k 41 | * - - i 42 | * - - - j 43 | * - k 44 | * 45 | * See [TreeUtilTest#compactMiddlePackages] for an example usage. 46 | */ 47 | fun TreeNode.compactMiddlePackages( 48 | pathGetter: (T) -> String, 49 | pathSetter: (T, path: String) -> Unit 50 | ) { 51 | this.children.forEach { child -> 52 | if (child.children.size == 1 && !child.children.first().isLeaf) { 53 | /*child.value.path += "." + child.children.first().value.path*/ 54 | pathSetter(child.value, 55 | pathGetter(child.value) + "." + pathGetter(child.children.first().value)) 56 | 57 | child.children = child.children.first().children 58 | 59 | child.parent.compactMiddlePackages(pathGetter, pathSetter) 60 | } else { 61 | child.compactMiddlePackages(pathGetter, pathSetter) 62 | } 63 | } 64 | } 65 | 66 | /** 67 | * Reassigns levels to all nodes recursively from their depth. 68 | */ 69 | fun TreeNode.reassignLevels( 70 | level: Int = -1 71 | ) { 72 | this.level = level 73 | this.children.forEach { child -> 74 | if (child.isLeaf) { 75 | child.level = this.level + 1 76 | } else { 77 | child.reassignLevels(this.level + 1) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/tree/dex/DexClassNode.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.tree.dex 2 | 3 | import ma.dexter.ui.tree.compactMiddlePackages 4 | 5 | /** 6 | * Base class to represent nodes in a Dex tree. 7 | * 8 | * For example, [name] could be "android" in "android/app/Activity" 9 | * (or "android.app" if [compactMiddlePackages] was run.) 10 | */ 11 | class DexClassNode( 12 | var name: String 13 | ) { 14 | 15 | override fun equals(other: Any?): Boolean { 16 | if (this === other) return true 17 | if (javaClass != other?.javaClass) return false 18 | 19 | other as DexClassNode 20 | 21 | if (name != other.name) return false 22 | 23 | return true 24 | } 25 | 26 | override fun hashCode(): Int { 27 | return name.hashCode() 28 | } 29 | 30 | override fun toString() = name 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/tree/dex/SmaliTree.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.tree.dex 2 | 3 | import ma.dexter.dex.MutableDexFile 4 | import ma.dexter.ui.tree.* 5 | import ma.dexter.util.normalizeSmaliPath 6 | 7 | class SmaliTree { 8 | private val dexList = mutableListOf() 9 | 10 | fun createTree(): TreeNode { 11 | val rootTreeNode = TreeNode.root() 12 | 13 | dexList.forEach { 14 | addToTree(rootTreeNode, it) 15 | } 16 | 17 | rootTreeNode.sort { node1, node2 -> 18 | if (node1.isLeaf != node2.isLeaf) { 19 | node1.isLeaf.compareTo(node2.isLeaf) 20 | } else { 21 | node1.value.name.compareTo(node2.value.name) 22 | } 23 | } 24 | 25 | rootTreeNode.compactMiddlePackages( 26 | pathGetter = DexClassNode::name, 27 | pathSetter = { it, path -> it.name = path } 28 | ) 29 | rootTreeNode.reassignLevels() 30 | 31 | return rootTreeNode 32 | } 33 | 34 | // TODO: use a faster algorithm 35 | private fun addToTree( 36 | rootTreeNode: TreeNode, 37 | dex: MutableDexFile 38 | ) { 39 | dex.classes.forEach { classDef -> 40 | var currentNode = rootTreeNode 41 | 42 | val classDefSegments = normalizeSmaliPath(classDef.type).split("/") 43 | 44 | classDefSegments.forEachIndexed { level, segment -> 45 | val subNode: TreeNode 46 | val toFind = currentNode.findChildByValue(DexClassNode(segment)) 47 | 48 | if (toFind == null) { 49 | subNode = TreeNode(DexClassNode(segment)) 50 | 51 | currentNode.addChild(subNode) 52 | } else { 53 | subNode = toFind 54 | } 55 | 56 | subNode.level = level 57 | currentNode = subNode 58 | } 59 | } 60 | } 61 | 62 | private fun addDex(dex: MutableDexFile): SmaliTree { 63 | dexList += dex 64 | return this 65 | } 66 | 67 | fun addDexEntries(dexEntries: List): SmaliTree { 68 | dexEntries.forEach { addDex(it) } 69 | return this 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/tree/dex/binder/DexItemNodeViewBinder.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.tree.dex.binder 2 | 3 | import android.view.View 4 | import androidx.core.content.ContextCompat 5 | import ma.dexter.App 6 | import ma.dexter.R 7 | import ma.dexter.databinding.ItemDexTreeNodeBinding 8 | import ma.dexter.ui.tree.TreeNode 9 | import ma.dexter.ui.tree.base.BaseNodeViewBinder 10 | import ma.dexter.ui.tree.dex.DexClassNode 11 | import ma.dexter.ui.util.dp 12 | import ma.dexter.ui.util.setMargins 13 | 14 | class DexItemNodeViewBinder( 15 | itemView: View, 16 | private val level: Int, 17 | private val toggleListener: DexItemNodeToggleListener, 18 | private val longClickListener: DexItemNodeLongClickListener 19 | ) : BaseNodeViewBinder(itemView) { 20 | 21 | private lateinit var binding: ItemDexTreeNodeBinding 22 | 23 | override fun bindView(treeNode: TreeNode) { 24 | binding = ItemDexTreeNodeBinding.bind(itemView) 25 | 26 | binding.root.setMargins(left = level * 20.dp) 27 | binding.title.text = treeNode.value.name 28 | 29 | binding.icExpand.rotation = if (treeNode.isExpanded) 90F else 0F 30 | 31 | if (treeNode.isLeaf) { 32 | binding.icExpand.visibility = View.GONE 33 | binding.cvClassDef.visibility = View.VISIBLE 34 | 35 | val colorClass = ContextCompat.getColor(App.context, R.color.colorClass) 36 | 37 | binding.txClassDef.setBackgroundColor(colorClass) 38 | binding.txClassDef.text = "C" 39 | } else { 40 | binding.icExpand.visibility = View.VISIBLE 41 | binding.cvClassDef.visibility = View.GONE 42 | } 43 | } 44 | 45 | override fun onNodeToggled(treeNode: TreeNode, expand: Boolean) { 46 | if (binding.icExpand.visibility == View.VISIBLE) { 47 | binding.icExpand.animate() 48 | .rotation(if (expand) 90F else 0F) 49 | .setDuration(150) 50 | .start() 51 | } 52 | 53 | toggleListener.onNodeToggled(treeNode) 54 | } 55 | 56 | override fun onNodeLongClicked( 57 | view: View, 58 | treeNode: TreeNode, 59 | expanded: Boolean 60 | ): Boolean { 61 | return longClickListener.onNodeLongClicked(view, treeNode) 62 | } 63 | 64 | fun interface DexItemNodeToggleListener { 65 | fun onNodeToggled(treeNode: TreeNode) 66 | } 67 | 68 | fun interface DexItemNodeLongClickListener { 69 | fun onNodeLongClicked(view: View, treeNode: TreeNode): Boolean 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/tree/dex/binder/DexItemNodeViewFactory.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.tree.dex.binder 2 | 3 | import android.view.View 4 | import ma.dexter.R 5 | import ma.dexter.ui.tree.base.BaseNodeViewFactory 6 | import ma.dexter.ui.tree.dex.binder.DexItemNodeViewBinder.* 7 | import ma.dexter.ui.tree.dex.DexClassNode 8 | 9 | class DexItemNodeViewFactory ( 10 | private val toggleListener: DexItemNodeToggleListener, 11 | private val longClickListener: DexItemNodeLongClickListener 12 | ): BaseNodeViewFactory() { 13 | 14 | override fun getNodeViewBinder(view: View, level: Int) = 15 | DexItemNodeViewBinder(view, level, toggleListener, longClickListener) 16 | 17 | override fun getNodeLayoutId(level: Int) = R.layout.item_dex_tree_node 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/util/PopupMenuUtils.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.util 2 | 3 | import android.graphics.drawable.Drawable 4 | import com.github.zawadz88.materialpopupmenu.MaterialPopupMenuBuilder.CustomItemHolder 5 | import com.github.zawadz88.materialpopupmenu.ViewBoundCallback 6 | import ma.dexter.R 7 | import ma.dexter.databinding.PopupMenuItemCheckableBinding 8 | 9 | fun CustomItemHolder.checkableItem(init: CheckableItemHolder.() -> Unit) { 10 | val holder = CheckableItemHolder() 11 | init(holder) 12 | 13 | dismissOnSelect = false 14 | layoutResId = R.layout.popup_menu_item_checkable 15 | viewBoundCallback = ViewBoundCallback { v -> 16 | PopupMenuItemCheckableBinding.bind(v).apply { 17 | label.text = holder.label 18 | icon.setImageDrawable(holder.iconDrawable) 19 | 20 | checkbox.isChecked = holder.checked 21 | checkbox.setOnCheckedChangeListener { _, isChecked -> 22 | holder.callback(isChecked) 23 | dismissPopup() 24 | } 25 | } 26 | } 27 | } 28 | 29 | class CheckableItemHolder { 30 | var label = "" 31 | var iconDrawable: Drawable? = null 32 | var checked = false 33 | var callback: (checked: Boolean) -> Unit = {} 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/util/UiUtil.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.util 2 | 3 | import android.util.TypedValue 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import ma.dexter.App 7 | 8 | val Int.dp: Int 9 | get() = TypedValue.applyDimension( 10 | TypedValue.COMPLEX_UNIT_DIP, 11 | this.toFloat(), 12 | App.context.resources.displayMetrics 13 | ).toInt() 14 | 15 | fun View.setMargins(left: Int? = null, top: Int? = null, right: Int? = null, bottom: Int? = null) { 16 | val params = (layoutParams as? ViewGroup.MarginLayoutParams) 17 | params?.setMargins( 18 | left ?: params.leftMargin, 19 | top ?: params.topMargin, 20 | right ?: params.rightMargin, 21 | bottom ?: params.bottomMargin 22 | ) 23 | layoutParams = params 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/ui/viewmodel/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.viewmodel 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import ma.dexter.project.DexProject 7 | import ma.dexter.ui.model.DexPageItem 8 | import ma.dexter.ui.model.MainItem 9 | 10 | class MainViewModel : ViewModel() { 11 | private val dexPageItems = MutableLiveData(mutableListOf()) 12 | 13 | val currentPosition = MutableLiveData(0) 14 | val dexProject = MutableLiveData() 15 | val viewPagerScrolled = MutableLiveData(0) 16 | 17 | fun getPageItems(): LiveData> { 18 | return dexPageItems 19 | } 20 | 21 | fun addMainItem() { 22 | val items = dexPageItems.value ?: return 23 | items.add(MainItem()) 24 | dexPageItems.value = items 25 | } 26 | 27 | fun gotoPageItem(dexPageItem: DexPageItem) { 28 | val items = dexPageItems.value ?: return 29 | 30 | items.forEachIndexed { position, item -> 31 | if (item.typeDef == dexPageItem.typeDef) { 32 | items[position] = dexPageItem 33 | 34 | dexPageItems.value = items 35 | currentPosition.value = position 36 | return 37 | } 38 | } 39 | 40 | items.add(dexPageItem) 41 | dexPageItems.value = items 42 | currentPosition.value = items.lastIndex 43 | } 44 | 45 | fun removePageItem(position: Int) { 46 | val items = dexPageItems.value ?: return 47 | items.removeAt(position) 48 | dexPageItems.value = items 49 | } 50 | 51 | fun removeAllPageItems( 52 | excludePos: Int = -1 53 | ) { 54 | val items = dexPageItems.value ?: return 55 | var index = 0 56 | val iterator = items.iterator() 57 | while (iterator.hasNext()) { 58 | val item = iterator.next() 59 | if (index != excludePos && item !is MainItem) { 60 | iterator.remove() 61 | } 62 | index++ 63 | } 64 | dexPageItems.value = items 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/util/AndroidUtils.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.util 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.net.Uri 8 | import android.view.View 9 | import android.view.inputmethod.InputMethodManager 10 | import android.widget.Toast 11 | import ma.dexter.App 12 | 13 | fun toast( 14 | message: String, 15 | duration: Int = Toast.LENGTH_SHORT 16 | ) { 17 | Toast.makeText(App.context, message, duration).show() 18 | } 19 | 20 | fun openUrl( 21 | context: Context, 22 | url: String 23 | ) { 24 | context.startActivity( 25 | Intent( 26 | Intent.ACTION_VIEW, 27 | Uri.parse(url) 28 | ) 29 | ) 30 | } 31 | 32 | fun hideKeyboard(view: View?) { 33 | if (view == null) return 34 | 35 | try { 36 | val imm = view.context 37 | .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 38 | if (imm.isActive) { 39 | imm.hideSoftInputFromWindow(view.windowToken, 0) 40 | } 41 | } catch (ignored: Exception) {} 42 | } 43 | 44 | fun copyToClipboard(text: String) { 45 | val clipboard = App.context 46 | .getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 47 | val clip = ClipData.newPlainText("", text) 48 | clipboard.setPrimaryClip(clip) 49 | } 50 | 51 | fun copyToClipboard(text: String, showToast: Boolean) { 52 | copyToClipboard(text) 53 | 54 | if (showToast) toast("Copied \"$text\" to clipboard") 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/util/AssetUtils.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.util 2 | 3 | import ma.dexter.App 4 | import java.io.File 5 | import java.io.FileOutputStream 6 | 7 | /** 8 | * Extracts an asset to the given [destinationFile]. 9 | * 10 | * Silently returns if [destinationFile] already exists. 11 | */ 12 | fun extractAsset( 13 | assetFileName: String, 14 | destinationFile: File 15 | ) { 16 | if (destinationFile.exists()) return 17 | 18 | val input = App.context.assets.open(assetFileName) 19 | val output = FileOutputStream(destinationFile) 20 | 21 | input.use { 22 | output.use { 23 | input.copyTo(output) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/util/DexUtils.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.util 2 | 3 | import ma.dexter.dex.MutableClassDef 4 | import ma.dexter.ui.tree.TreeNode 5 | import ma.dexter.ui.tree.dex.DexClassNode 6 | import org.jf.dexlib2.AccessFlags 7 | import org.jf.dexlib2.iface.ClassDef 8 | import org.jf.dexlib2.immutable.ImmutableClassDef 9 | 10 | const val DEFAULT_DEX_VERSION = 35 11 | 12 | fun MutableClassDef.isInterface(): Boolean { 13 | return (this.accessFlags and AccessFlags.INTERFACE.value) != 0 14 | } 15 | 16 | fun MutableClassDef.isEnum(): Boolean { 17 | return (this.accessFlags and AccessFlags.ENUM.value) != 0 18 | } 19 | 20 | fun MutableClassDef.isAnnotation(): Boolean { 21 | return (this.accessFlags and AccessFlags.ANNOTATION.value) != 0 22 | } 23 | 24 | fun createClassDef( 25 | classDescriptor: String, 26 | superClassDescriptor: String = "Ljava/lang/Object;" 27 | ): ClassDef { 28 | return ImmutableClassDef( 29 | classDescriptor, 30 | 0, 31 | superClassDescriptor, 32 | null, null, null, null, null 33 | ) 34 | } 35 | 36 | fun TreeNode.getPath(): String { 37 | val treeNode = this 38 | 39 | val list = buildList { 40 | var node = treeNode 41 | while (!node.isRoot) { 42 | add(node.value.name) 43 | node = node.parent 44 | } 45 | } 46 | 47 | return list 48 | .reversed() 49 | .joinToString(".") 50 | .replace(".", "/") 51 | } 52 | 53 | fun TreeNode.getClassDescriptor(): String { 54 | return "L" + getPath() + ";" 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/util/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.util 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.os.Environment 9 | import android.provider.Settings 10 | import java.io.File 11 | 12 | val storagePath: File 13 | @Suppress("DEPRECATION") 14 | get() = Environment.getExternalStorageDirectory() 15 | 16 | fun requestAllFilesAccessPermission(context: Context) { 17 | if (Build.VERSION.SDK_INT >= 30 && !Environment.isExternalStorageManager()) { 18 | Intent().run { 19 | action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION 20 | data = Uri.parse("package:${context.packageName}") 21 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 22 | 23 | try { 24 | context.startActivity(this) 25 | } catch (ignored: ActivityNotFoundException) {} 26 | } 27 | } 28 | } 29 | 30 | /** 31 | * Get [File] with a unique name, based on [baseName] and 32 | * [extension]. Makes the File unique by the Windows way, that is: 33 | * 34 | * If "[baseName].[extension]" doesn't exist, just returns that. 35 | * Otherwise, returns "[baseName] (N).[extension]" where **N** is 36 | * some number >= 2. 37 | */ 38 | fun getFileWithUniqueName( 39 | baseName: String, 40 | extension: String, 41 | directory: File, 42 | ): File { 43 | var count = 1 44 | while (true) { 45 | val file = File( 46 | directory, 47 | baseName + (if (count > 1) " ($count)" else "") + "." + extension 48 | ) 49 | 50 | if (file.exists()) { 51 | count++ 52 | } else { 53 | return file 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/util/LogUtils.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.util 2 | 3 | import java.io.OutputStream 4 | import java.io.PrintStream 5 | 6 | /** 7 | * Listens for System.out/err logs and invokes the callback 8 | * when received. 9 | * 10 | * @param errLogsEnabled Specifies whether it should listen for System.err logs as well 11 | * @param callback Callback to be invoked when a log is received 12 | */ 13 | fun listenForSystemLogs( 14 | errLogsEnabled: Boolean = true, 15 | callback: (String) -> Unit 16 | ) { 17 | val outStream = PrintStream(object : OutputStream() { 18 | private val cache = StringBuilder() 19 | 20 | override fun write(b: Int) { 21 | // print line by line 22 | if (b.toChar() == '\n') { 23 | callback(cache.toString()) 24 | 25 | cache.clear() 26 | } else { 27 | cache.append(b.toChar()) 28 | } 29 | } 30 | }) 31 | 32 | System.setOut(outStream) 33 | if (errLogsEnabled) System.setErr(outStream) 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/kotlin/ma/dexter/util/SmaliUtils.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.util 2 | 3 | import org.antlr.runtime.Token 4 | import org.jf.smali.smaliFlexLexer 5 | import org.jf.smali.smaliParser 6 | import java.io.StringReader 7 | 8 | /** 9 | * group 1: full method descriptor 10 | * group 2: method name 11 | * group 3: method params + return type 12 | */ 13 | val METHOD_DIRECTIVE_REGEX = Regex( 14 | """^\.method (?:(?:[a-z\-]*) )*((.*?)\((.*))${'$'}""" 15 | ) 16 | 17 | /** 18 | * group 1: full field descriptor 19 | * group 2: field name 20 | * group 3: field return type 21 | */ 22 | val FIELD_DIRECTIVE_REGEX = Regex( 23 | """^\.field (?:(?:[a-z\-]*) )*((.*?):(.*))${'$'}""" 24 | ) 25 | 26 | val FIELD_METHOD_CALL_REGEX = Regex( 27 | """^.*?((L.*?;)\s*->\s*(.*))${'$'}""", RegexOption.DOT_MATCHES_ALL 28 | ) 29 | 30 | const val END_METHOD_DIRECTIVE = ".end method" 31 | 32 | /** 33 | * Ltest/aaa; -> test/aaa 34 | */ 35 | fun normalizeSmaliPath(classDefType: String) = 36 | if (classDefType.startsWith("L") && classDefType.endsWith(";")) { 37 | classDefType.substring(1, classDefType.length - 1) 38 | } else { 39 | classDefType 40 | } 41 | 42 | /** 43 | * Ltest/aaa; -> aaa 44 | */ 45 | fun getClassNameFromSmaliPath(classDefType: String) = 46 | normalizeSmaliPath(classDefType).substringAfterLast("/") 47 | 48 | /** 49 | * Utility method to tokenize smali. 50 | */ 51 | inline fun tokenizeSmali( 52 | smaliCode: String, 53 | callback: (token: Token, line: Int, column: Int) -> Unit 54 | ) { 55 | val lexer = smaliFlexLexer(StringReader(smaliCode), 31) 56 | lexer.setSuppressErrors(true) 57 | 58 | var token: Token 59 | 60 | while (true) { 61 | token = lexer.nextToken() 62 | 63 | if (token == null || token.type == smaliParser.EOF) break 64 | if (token.type == smaliParser.WHITE_SPACE) continue 65 | 66 | val line = token.line - 1 67 | val column = token.charPositionInLine 68 | 69 | callback(token, line, column) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omeraydindev/Dexter/afe10af982c70525c1da5a87849a30169f314b80/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_arrow_forward_ios_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_arrow_right_alt_20.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_copyright_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_home_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_more_vert_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_redo_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_save_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_save_alt_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_search_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_undo_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_view_stream_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_wrap_text_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_zoom_in_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_java.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_letter_s_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_create_smali_file.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 23 | 24 | 29 | 30 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_progress_simple.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | 27 | 28 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_base_code_editor.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_dex_editor.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_dex_tree_node.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 22 | 23 | 30 | 31 | 41 | 42 | 43 | 44 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/layout/popup_menu_item_checkable.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 25 | 26 | 38 | 39 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_base_code_editor.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 18 | 19 | 24 | 25 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_dex_tree_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #131313 5 | #71aaeb 6 | #ffffff 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #131313 5 | #71aaeb 6 | #101010 7 | 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | #FF67bed9 12 | #FF487c39 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Dexter 3 | 4 | Goto 5 | LOAD DEX/APK 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/test/kotlin/ma/dexter/dex/MutableDexTests.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.dex 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import ma.dexter.tools.smali.BaksmaliInvoker 5 | import ma.dexter.util.compile.Java2Dex 6 | import org.jf.smali.SmaliTestUtils 7 | import org.junit.Test 8 | 9 | class MutableDexTests { 10 | 11 | @Test 12 | fun `find & delete ClassDefs`() { 13 | val dexFile = Java2Dex.compile( 14 | fileName = "A.java", 15 | javaCode = """ 16 | class A { static class B {} } 17 | """ 18 | ) 19 | 20 | val dex = DexFactory.fromFile(dexFile) 21 | 22 | val cd = dex.findClassDef("LA;") 23 | assert(cd != null) 24 | 25 | dex.deleteClassDef(cd!!) 26 | assert(dex.findClassDef("LA;") == null) 27 | } 28 | 29 | @Test 30 | fun `disassemble smali and edit it, then reassemble the dex`() { 31 | val dexFile = Java2Dex.compile( 32 | fileName = "A.java", 33 | javaCode = """ 34 | package m; 35 | 36 | class A { 37 | static class B { 38 | void replaceMe() {} 39 | } 40 | } 41 | """ 42 | ) 43 | 44 | val dex = DexFactory.fromFile(dexFile) 45 | 46 | val newClassDef = SmaliTestUtils.compileSmali( 47 | dex.getSmali("Lm/A\$B;") 48 | .replace("replaceMe", "replacedYou") 49 | ) 50 | dex.replaceClassDef(newClassDef) 51 | 52 | val otherClassDef = SmaliTestUtils.compileSmali(".class Lm/C; .super Lm/A;") 53 | dex.addClassDef(otherClassDef) 54 | 55 | dex.writeToFile(dex.dexFile!!) 56 | 57 | 58 | // check if changes are applied 59 | val overwrittenDex = DexFactory.fromFile(dexFile) 60 | 61 | assertThat(overwrittenDex.findClassDef("Lm/A\$B;")) 62 | .isNotNull() 63 | 64 | assertThat(overwrittenDex.findClassDef("Lm/A\$B;")!!.methods.find { it.name == "replaceMe" }) 65 | .isNull() 66 | 67 | assertThat(overwrittenDex.findClassDef("Lm/A\$B;")!!.methods.find { it.name == "replacedYou" }) 68 | .isNotNull() 69 | 70 | assertThat(overwrittenDex.findClassDef("Lm/C;")) 71 | .isNotNull() 72 | } 73 | 74 | private fun MutableDexFile.getSmali(classDescriptor: String): String { 75 | val classDef = findClassDef(classDescriptor)!! 76 | 77 | return BaksmaliInvoker().disassemble(classDef) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /app/src/test/kotlin/ma/dexter/tasks/MergeDexTaskTest.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tasks 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import ma.dexter.dex.DexFactory 5 | import ma.dexter.dex.MutableDexFile 6 | import ma.dexter.util.BaseTestClass 7 | import ma.dexter.util.createClassDef 8 | import org.jf.dexlib2.iface.ClassDef 9 | import org.junit.Test 10 | import java.io.File 11 | 12 | class MergeDexTaskTest : BaseTestClass() { 13 | 14 | @Test 15 | fun `merge DEX files`() { 16 | val dex1 = createDex("dex1.dex", createClassDef("La;")) 17 | val dex2 = createDex("dex2.dex", createClassDef("Lb;")) 18 | val mergedDex = File(testFolder, "merged.dex") 19 | 20 | MergeDexTask(arrayOf(dex1.absolutePath, dex2.absolutePath), mergedDex) 21 | .run {} 22 | assert(mergedDex.exists()) 23 | 24 | val mergedDexFile = DexFactory.fromFile(mergedDex) 25 | assertThat(mergedDexFile.findClassDef("La;")) 26 | .isNotNull() 27 | assertThat(mergedDexFile.findClassDef("Lb;")) 28 | .isNotNull() 29 | } 30 | 31 | private fun createDex( 32 | name: String, 33 | vararg classDefs: ClassDef 34 | ): File { 35 | val dex = MutableDexFile() 36 | classDefs.forEach(dex::addClassDef) 37 | 38 | val file = File(testFolder, name) 39 | dex.writeToFile(file) 40 | 41 | return file 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/src/test/kotlin/ma/dexter/tools/decompilers/jadx/JADXDecompilerTest.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.tools.decompilers.jadx 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import ma.dexter.util.compile.Java2Dex 5 | import org.junit.Test 6 | 7 | class JADXDecompilerTest { 8 | 9 | @Test 10 | fun `JADX decompile class basic`() { 11 | val dexFile = Java2Dex.compile( 12 | fileName = "Test.java", 13 | javaCode = """ 14 | package test.aaa; 15 | 16 | public class Test {} 17 | """ 18 | ) 19 | 20 | val result = JADXDecompiler().decompileDex("test.aaa.Test", dexFile) 21 | assertThat(result).contains("Decompiled") 22 | println(result) 23 | 24 | val result2 = JADXDecompiler().decompileDex("test/aaa/Test", dexFile) 25 | assertThat(result2).contains("Decompiled") 26 | println(result2) 27 | } 28 | 29 | @Test 30 | fun `JADX decompile class from defpackage`() { 31 | val dexFile = Java2Dex.compile( 32 | fileName = "Test.java", 33 | javaCode = """ 34 | public class Test {} 35 | """ 36 | ) 37 | 38 | val result = JADXDecompiler().decompileDex("Test", dexFile) 39 | assertThat(result).contains("Decompiled") 40 | println(result) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/test/kotlin/ma/dexter/ui/tree/TreeUtilTest.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.ui.tree 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import ma.dexter.ui.tree.dex.DexClassNode 5 | import org.junit.Test 6 | 7 | class TreeUtilTest { 8 | 9 | @Test 10 | fun compactMiddlePackages() { 11 | val tree = root { 12 | child(DexClassNode("a")) 13 | child(DexClassNode("b")) 14 | child(DexClassNode("c")) { 15 | child(DexClassNode("d")) { 16 | child(DexClassNode("e")) { 17 | child(DexClassNode("f")) 18 | } 19 | } 20 | } 21 | child(DexClassNode("g")) { 22 | child(DexClassNode("h")) { 23 | child(DexClassNode("i")) { 24 | child(DexClassNode("j")) 25 | } 26 | } 27 | child(DexClassNode("k")) 28 | } 29 | } 30 | 31 | assertThat(treeToString(tree)).isEqualTo( 32 | """ 33 | a 34 | b 35 | c 36 | - d 37 | - - e 38 | - - - f 39 | g 40 | - h 41 | - - i 42 | - - - j 43 | - k 44 | """.trimIndent()) 45 | 46 | tree.compactMiddlePackages( 47 | pathGetter = DexClassNode::name, 48 | pathSetter = { it, path -> it.name = path } 49 | ) 50 | assertThat(treeToString(tree)).isEqualTo( 51 | """ 52 | a 53 | b 54 | c.d.e 55 | - f 56 | g 57 | - h.i 58 | - - j 59 | - k 60 | """.trimIndent()) 61 | } 62 | 63 | private fun root( 64 | block: TreeNode.() -> Unit 65 | ): TreeNode { 66 | val root = TreeNode.root() 67 | block(root) 68 | return root 69 | } 70 | 71 | private fun TreeNode.child( 72 | t: T, 73 | block: TreeNode.() -> Unit = {} 74 | ) { 75 | addChild(TreeNode(t).apply(block)) 76 | } 77 | 78 | private fun treeToString( 79 | tree: TreeNode, 80 | prefix: String = "", 81 | map: (TreeNode) -> String = { it.value.toString() } 82 | ): String = buildString { 83 | 84 | tree.children.forEach { child -> 85 | append(prefix + map(child) + "\n") 86 | 87 | if (!child.isLeaf) { 88 | append(treeToString(child, "$prefix- ", map) + "\n") 89 | } 90 | } 91 | 92 | }.trim() 93 | } 94 | -------------------------------------------------------------------------------- /app/src/test/kotlin/ma/dexter/util/BaseTestClass.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.util 2 | 3 | import org.junit.After 4 | import org.junit.Before 5 | import java.io.File 6 | 7 | open class BaseTestClass { 8 | protected lateinit var testFolder: File 9 | 10 | @Before 11 | fun setUp() { 12 | testFolder = File(javaClass.getResource("/")!!.file, javaClass.name) 13 | 14 | testFolder.deleteRecursively() 15 | testFolder.mkdirs() 16 | println("Used test folder: ${testFolder.absolutePath}") 17 | } 18 | 19 | @After 20 | fun tearDown() { 21 | testFolder.deleteRecursively() 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/test/kotlin/ma/dexter/util/compile/Java2Dex.kt: -------------------------------------------------------------------------------- 1 | package ma.dexter.util.compile 2 | 3 | import com.android.tools.r8.CompilationMode 4 | import com.android.tools.r8.D8 5 | import com.android.tools.r8.D8Command 6 | import com.android.tools.r8.OutputMode 7 | import org.eclipse.jdt.core.compiler.batch.BatchCompiler 8 | import java.io.File 9 | import java.io.PrintWriter 10 | 11 | /** 12 | * Utility class to compile Java source to DEX easily. 13 | */ 14 | object Java2Dex { 15 | 16 | /** 17 | * Compiles given [javaCode] and returns the generated dex file. 18 | * 19 | * Java -`ECJ`> Java bytecode -`D8`> Dalvik bytecode 20 | */ 21 | fun compile( 22 | fileName: String, 23 | javaCode: String 24 | ): File { 25 | val testFolder = File(javaClass.getResource("/Test")!!.file) 26 | 27 | // Init files 28 | val rtJar = File(testFolder, "rt.jar") 29 | val javaFile = File(testFolder, fileName).apply { delete() } 30 | val classDir = File(testFolder, "classes/").apply { deleteRecursively(); mkdirs() } 31 | val dexFile = File(testFolder, "classes.dex").apply { delete() } 32 | 33 | // Write the Java code to file 34 | javaFile.writeText(javaCode) 35 | 36 | // Java -> Java bytecode 37 | runECJ(javaFile, classDir, rtJar) 38 | 39 | // Java bytecode -> Dalvik bytecode 40 | runD8(dexFile, classDir, testFolder, rtJar) 41 | 42 | return dexFile 43 | } 44 | 45 | private fun runECJ( 46 | javaFile: File, 47 | classDir: File, 48 | rtJar: File 49 | ) { 50 | // -g -> keep debug info 51 | // -nowarn -> disable warnings 52 | // -proc:none -> disable annotation processors 53 | 54 | val ecjResult = BatchCompiler.compile( 55 | "-1.8 -g -nowarn -proc:none ${javaFile.absolutePath} -d ${classDir.absolutePath} -cp ${rtJar.absolutePath}", 56 | PrintWriter(System.out), PrintWriter(System.err), null 57 | ) 58 | 59 | if (!ecjResult) throw Java2DexException("ECJ failed to compile Java") 60 | } 61 | 62 | private fun runD8( 63 | dexFile: File, 64 | classDir: File, 65 | outputDir: File, 66 | rtJar: File 67 | ) { 68 | val d8Command = D8Command.builder() 69 | .addClasspathFiles(rtJar.toPath()) 70 | .addProgramFiles(classDir 71 | .walk() 72 | .filter { it.name.endsWith(".class") } 73 | .map(File::toPath) 74 | .toList() 75 | ) 76 | .setMinApiLevel(21) 77 | .setMode(CompilationMode.DEBUG) 78 | .setOutput(outputDir.toPath(), OutputMode.DexIndexed) 79 | .build() 80 | 81 | D8.run(d8Command) 82 | 83 | if (!dexFile.exists()) throw Java2DexException("D8 failed to convert JAR to DEX") 84 | } 85 | 86 | class Java2DexException(name: String): Exception(name) 87 | } 88 | -------------------------------------------------------------------------------- /app/src/test/resources/Test/rt.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omeraydindev/Dexter/afe10af982c70525c1da5a87849a30169f314b80/app/src/test/resources/Test/rt.jar -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | mavenCentral() 6 | maven { url 'https://jitpack.io' } 7 | } 8 | 9 | dependencies { 10 | classpath "com.android.tools.build:gradle:7.0.3" 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.0-RC2" 12 | } 13 | } 14 | 15 | task clean(type: Delete) { 16 | delete rootProject.buildDir 17 | } 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 4 | # org.gradle.parallel=true 5 | 6 | android.useAndroidX=true 7 | android.enableJetifier=true 8 | 9 | kotlin.code.style=official 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omeraydindev/Dexter/afe10af982c70525c1da5a87849a30169f314b80/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Sep 02 18:12:08 TRT 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /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 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | google() 5 | mavenCentral() 6 | jcenter() 7 | maven { url 'https://jitpack.io' } 8 | } 9 | 10 | } 11 | rootProject.name = "Dexter" 12 | 13 | include ':app' 14 | --------------------------------------------------------------------------------