├── .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 |
32 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_dex_tree_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
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 |
--------------------------------------------------------------------------------