├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.properties ├── screenshots ├── ai-view.png ├── original.png └── selectOption.png ├── settings.gradle.kts └── src └── main ├── java └── jadx │ └── plugins │ └── aiview │ ├── Ai.java │ ├── AiViewFrame.java │ ├── AiViewMain.java │ ├── CodeExtractor.java │ ├── JadxAiViewAction.java │ ├── JadxAiViewOptions.java │ └── JadxAiViewPlugin.java └── resources └── META-INF └── services └── jadx.api.plugins.JadxPlugin /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Devilx86 and other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jadx AI view plugin 2 | A (Work in progress) Jadx plugin that shows AI analysis of a method/class in a separate view allowing a reverse engineer to compare code and the AI analysis to understand it better. Currently supports gpt4free gpt 3.5 turbo, Gemini 1.5 flash and OpenAI GPT 4.0 mini. 3 | 4 | ## Usage 5 | The following is a sample malware code from Jadx:
6 | 7 | 8 | Right Click on the class or method name and select the "*Show AI Analysis*" option as seen below:
9 | 10 | 11 | This opens up the jadx-ai-view window with the AI analysis output as seen below:
12 | 13 | 14 | ## Prerequisites 15 | Install gpt4free if you plan on using the gpt4free option i.e: 16 | ```bash 17 | python3 -m pip install g4f 18 | ``` 19 | 20 | ## Building 21 | Execute the following command to build the jar file: 22 | ```bash 23 | export VERSION='1.0.0' && ./gradlew build 24 | ``` 25 | ## Installation 26 | Jadx > 1.5.0 is required, please install the latest version of the Jadx unstable build. Then browse to the `Plugins > Install plugin` option and enter `file:` or enter the location id: `github:devilx86:jadx-ai-view-plugin`. Alternatively install with the following command: 27 | ```bash 28 | jadx-gui plugins --install "github:devilx86:jadx-ai-view-plugin" 29 | ``` 30 | 31 | ## Uninstallation 32 | Run the following command to uninstall the plugin: 33 | ```bash 34 | jadx-gui plugins --uninstall jadx-ai-view 35 | ``` 36 | 37 | ## Credits 38 | - Skylot - [Jadx](https://github.com/skylot/jadx), [plugin template](https://github.com/skylot/jadx/wiki/Jadx-plugins-guide) and [usage](https://github.com/skylot/jadx/issues/2305#issuecomment-2420607111). 39 | - [timschneeb](https://github.com/timschneeb/jadx-type-diagram-plugin) - Jadx API usage reference. 40 | - [MartinKayJr](https://github.com/skylot/jadx/issues/1884#issue-1727047157) - Default prompt engineering. 41 | - xtekky - [gpt4free](https://github.com/xtekky/gpt4free). 42 | - LangChain4j - [LangChain4j](https://github.com/langchain4j/langchain4j/). 43 | - bobbylight - [RSyntaxTextArea](https://github.com/bobbylight/RSyntaxTextArea). 44 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar 2 | 3 | plugins { 4 | `java-library` 5 | 6 | id("com.github.johnrengelman.shadow") version "8.1.1" 7 | 8 | // auto update dependencies with 'useLatestVersions' task 9 | id("se.patrikerdes.use-latest-versions") version "0.2.18" 10 | id("com.github.ben-manes.versions") version "0.50.0" 11 | } 12 | 13 | dependencies { 14 | // use compile only scope to exclude jadx-core and its dependencies from result jar 15 | /* compileOnly("io.github.skylot:jadx-core:1.5.0") { 16 | isChanging = true 17 | } 18 | testImplementation("io.github.skylot:jadx-smali-input:1.5.0") { 19 | isChanging = true 20 | }*/ 21 | 22 | implementation("io.github.skylot:jadx-core:1.5.1-SNAPSHOT") { 23 | isChanging = true 24 | } 25 | implementation("io.github.skylot:jadx-dex-input:1.5.1-SNAPSHOT") { 26 | isChanging = true 27 | } 28 | implementation("io.github.skylot:jadx-java-input:1.5.1-SNAPSHOT") { 29 | isChanging = true 30 | } 31 | implementation("io.github.skylot:jadx-smali-input:1.5.1-SNAPSHOT") { 32 | isChanging = true 33 | } 34 | implementation("io.github.skylot:jadx-kotlin-metadata:1.5.1-SNAPSHOT") { 35 | isChanging = true 36 | } 37 | 38 | implementation("com.fifesoft:rsyntaxtextarea:3.5.1") 39 | //implementation("io.github.lambdua:service:0.22.3") 40 | implementation("dev.langchain4j:langchain4j-core:0.35.0") 41 | implementation("dev.langchain4j:langchain4j-open-ai:0.33.0") 42 | implementation("dev.langchain4j:langchain4j-google-ai-gemini:0.34.0") 43 | 44 | 45 | //implementation("dev.langchain4j:langchain4j-open-ai:0.35.0") 46 | 47 | 48 | testImplementation("ch.qos.logback:logback-classic:1.4.14") 49 | testImplementation("org.assertj:assertj-core:3.24.2") 50 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.1") 51 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.1") 52 | } 53 | 54 | repositories { 55 | mavenCentral() 56 | maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/") 57 | google() 58 | } 59 | 60 | java { 61 | sourceCompatibility = JavaVersion.VERSION_11 62 | targetCompatibility = JavaVersion.VERSION_11 63 | } 64 | 65 | version = System.getenv("VERSION") ?: "dev" 66 | 67 | tasks { 68 | withType(Test::class) { 69 | useJUnitPlatform() 70 | } 71 | val shadowJar = withType(ShadowJar::class) { 72 | archiveClassifier.set("") // remove '-all' suffix 73 | } 74 | 75 | // copy result jar into "build/dist" directory 76 | register("dist") { 77 | dependsOn(shadowJar) 78 | dependsOn(withType(Jar::class)) 79 | 80 | from(shadowJar) 81 | into(layout.buildDirectory.dir("dist")) 82 | } 83 | } 84 | 85 | tasks.withType { 86 | // To avoid the duplicate handling strategy error 87 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 88 | 89 | // To add all of the dependencies 90 | from(sourceSets.main.get().output) 91 | 92 | dependsOn(configurations.runtimeClasspath) 93 | from({ 94 | configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) } 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.warning.mode=all 2 | org.gradle.parallel=true 3 | org.gradle.caching=true 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devilx86/jadx-ai-view-plugin/b56f260289bdacb00d23761a63c13801c91ef2c9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Wed Oct 09 19:19:44 AST 2024 8 | sdk.dir=/home/user/Android/Sdk 9 | -------------------------------------------------------------------------------- /screenshots/ai-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devilx86/jadx-ai-view-plugin/b56f260289bdacb00d23761a63c13801c91ef2c9/screenshots/ai-view.png -------------------------------------------------------------------------------- /screenshots/original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devilx86/jadx-ai-view-plugin/b56f260289bdacb00d23761a63c13801c91ef2c9/screenshots/original.png -------------------------------------------------------------------------------- /screenshots/selectOption.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devilx86/jadx-ai-view-plugin/b56f260289bdacb00d23761a63c13801c91ef2c9/screenshots/selectOption.png -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | rootProject.name = "jadx-ai-view-plugin" 3 | -------------------------------------------------------------------------------- /src/main/java/jadx/plugins/aiview/Ai.java: -------------------------------------------------------------------------------- 1 | package jadx.plugins.aiview; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.File; 5 | import java.io.InputStreamReader; 6 | import java.io.PrintWriter; 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import dev.langchain4j.data.message.AiMessage; 12 | import dev.langchain4j.data.message.ChatMessage; 13 | import dev.langchain4j.data.message.SystemMessage; 14 | import dev.langchain4j.data.message.UserMessage; 15 | import dev.langchain4j.model.chat.ChatLanguageModel; 16 | import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; 17 | import dev.langchain4j.model.openai.OpenAiChatModel; 18 | import dev.langchain4j.model.output.Response; 19 | 20 | public class Ai { 21 | 22 | public enum OpenAiChatModelName { 23 | GPT_3_5_TURBO("gpt-3.5-turbo"), 24 | GPT_3_5_TURBO_1106("gpt-3.5-turbo-1106"), 25 | GPT_3_5_TURBO_0125("gpt-3.5-turbo-0125"), 26 | GPT_3_5_TURBO_16K("gpt-3.5-turbo-16k"), 27 | GPT_4("gpt-4"), 28 | GPT_4_0613("gpt-4-0613"), 29 | GPT_4_TURBO_PREVIEW("gpt-4-turbo-preview"), 30 | GPT_4_1106_PREVIEW("gpt-4-1106-preview"), 31 | GPT_4_0125_PREVIEW("gpt-4-0125-preview"), 32 | GPT_4_32K("gpt-4-32k"), 33 | GPT_4_32K_0314("gpt-4-32k-0314"), 34 | GPT_4_32K_0613("gpt-4-32k-0613"), 35 | GPT_4_O("gpt-4o"), 36 | GPT_4_O_MINI("gpt-4o-mini"); 37 | 38 | private final String stringValue; 39 | 40 | OpenAiChatModelName(String stringValue) { 41 | this.stringValue = stringValue; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return stringValue; 47 | } 48 | } 49 | 50 | public static String getOpenAIAnalysis(String code, OpenAiChatModelName model, JadxAiViewOptions options) { 51 | if (options.OPENAI_PROMPT == null || options.OPENAI_PROMPT.isBlank()) { 52 | return ""; 53 | } 54 | 55 | ChatLanguageModel aiChatModel = OpenAiChatModel.builder() 56 | .apiKey(options.OPENAI_API_KEY) 57 | .modelName(model.toString()) 58 | .build(); 59 | 60 | ChatMessage systemMessage = new SystemMessage(options.OPENAI_PROMPT); 61 | List messages = new ArrayList<>(); 62 | messages.add(systemMessage); 63 | messages.add(new UserMessage(code)); 64 | Response messageResponse = aiChatModel.generate(messages); 65 | 66 | String response = messageResponse.toString(); 67 | if (options.showOnlyCodeOutput() && response.contains("```java")) { 68 | return response.split("```java")[1].split("```")[0].trim(); 69 | } 70 | 71 | return response; 72 | } 73 | 74 | public static String getGeminiAnalysis(String code, JadxAiViewOptions options) { 75 | if (options.GEMINI_PROMPT == null || options.GEMINI_PROMPT.isBlank()) { 76 | return ""; 77 | } 78 | 79 | ChatLanguageModel gemini = GoogleAiGeminiChatModel.builder() 80 | .apiKey(options.GEMINI_API_KEY) 81 | .modelName("gemini-1.5-flash") 82 | .temperature(0.1) 83 | .build(); 84 | 85 | ChatMessage systemMessage = new SystemMessage(options.OPENAI_PROMPT); 86 | List messages = new ArrayList<>(); 87 | messages.add(systemMessage); 88 | messages.add(new UserMessage(code)); 89 | Response messageResponse = gemini.generate(messages); 90 | 91 | String response = messageResponse.toString(); 92 | if (options.showOnlyCodeOutput() && response.contains("```java")) { 93 | return response.split("```java")[1].split("```")[0].trim(); 94 | } 95 | return response; 96 | } 97 | 98 | public static String getGPT4FreeAnalysis(String code, OpenAiChatModelName model, JadxAiViewOptions options) { 99 | if (options.GPT4FREE_PROMPT == null || options.GPT4FREE_PROMPT.isBlank()) { 100 | return ""; 101 | } 102 | 103 | String prompt = options.GPT4FREE_PROMPT; 104 | prompt += code; 105 | if (options.showOnlyCodeOutput()) { 106 | prompt += "Only share the updated/commented code"; 107 | } 108 | 109 | try { 110 | File tempFile = File.createTempFile("jadx_ai", ".py", new File(System.getProperty("java.io.tmpdir"))); 111 | PrintWriter writer = new PrintWriter(tempFile, StandardCharsets.UTF_8); 112 | writer.println("import sys"); 113 | writer.println("from g4f.client import Client"); 114 | writer.println("prompt = '''" + prompt + "'''"); 115 | writer.println("client = Client()"); 116 | writer.println("response = client.chat.completions.create(model='" + model.toString() + "', messages=[{\"role\": \"user\", \"content\": prompt}],).choices[0].message.content"); 117 | writer.println("print(response)"); 118 | writer.close(); 119 | 120 | if (tempFile.exists()) { 121 | Process process = new ProcessBuilder("python3", tempFile.getAbsolutePath()).start(); 122 | BufferedReader inputReader = 123 | new BufferedReader(new InputStreamReader(process.getInputStream())); 124 | 125 | BufferedReader errorReader = 126 | new BufferedReader(new InputStreamReader(process.getErrorStream())); 127 | 128 | StringBuilder builder = new StringBuilder(); 129 | String line = null; 130 | while ((line = inputReader.readLine()) != null) { 131 | builder.append(line); 132 | builder.append(System.lineSeparator()); 133 | } 134 | 135 | String response = builder.toString(); 136 | if (response.isBlank()) { 137 | while ((line = errorReader.readLine()) != null) { 138 | builder.append(line); 139 | builder.append(System.lineSeparator()); 140 | } 141 | } 142 | response = builder.toString(); 143 | 144 | tempFile.delete(); 145 | 146 | 147 | if (options.showOnlyCodeOutput() && response.contains("```java")) { 148 | return response.split("```java")[1].split("```")[0].trim(); 149 | } 150 | 151 | return response; 152 | } 153 | return "ERROR: GPT4Free Failed, temporary file could not be found!"; 154 | } catch (Exception e) { 155 | return e.getMessage(); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/jadx/plugins/aiview/AiViewFrame.java: -------------------------------------------------------------------------------- 1 | package jadx.plugins.aiview; 2 | 3 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; 4 | import org.fife.ui.rsyntaxtextarea.SyntaxConstants; 5 | import org.fife.ui.rtextarea.RTextScrollPane; 6 | 7 | import java.awt.BorderLayout; 8 | import java.awt.Component; 9 | import java.awt.Dimension; 10 | import java.awt.Font; 11 | import java.awt.GridLayout; 12 | import java.awt.event.ActionEvent; 13 | import java.awt.event.ActionListener; 14 | import java.awt.event.WindowAdapter; 15 | import java.awt.event.WindowEvent; 16 | import java.io.File; 17 | import java.io.FileInputStream; 18 | import java.io.FileOutputStream; 19 | import java.io.IOException; 20 | import java.io.ObjectInputStream; 21 | import java.io.ObjectOutputStream; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | import javax.swing.BoxLayout; 26 | import javax.swing.JButton; 27 | import javax.swing.JFileChooser; 28 | import javax.swing.JFrame; 29 | import javax.swing.JLabel; 30 | import javax.swing.JMenu; 31 | import javax.swing.JMenuBar; 32 | import javax.swing.JMenuItem; 33 | import javax.swing.JOptionPane; 34 | import javax.swing.JPanel; 35 | import javax.swing.JProgressBar; 36 | import javax.swing.JScrollPane; 37 | import javax.swing.JSlider; 38 | import javax.swing.JTabbedPane; 39 | import javax.swing.JTextArea; 40 | import javax.swing.filechooser.FileFilter; 41 | import javax.swing.filechooser.FileNameExtensionFilter; 42 | 43 | public class AiViewFrame extends JFrame { 44 | private HashMap tabMap; 45 | 46 | private final JadxAiViewOptions options; 47 | private JTabbedPane tabbedPane = null; 48 | private boolean isWindowClosed = false; 49 | private volatile JPanel progressPanel; 50 | 51 | public boolean isWindowClosed() { 52 | return isWindowClosed; 53 | } 54 | 55 | public void showProgressPanel() { 56 | this.progressPanel.setVisible(true); 57 | } 58 | 59 | public void hideProgressPanel() { 60 | this.progressPanel.setVisible(false); 61 | } 62 | 63 | public AiViewFrame(JadxAiViewOptions options) { 64 | this.options = options; 65 | this.addWindowListener(new WindowAdapter() { 66 | @Override 67 | public void windowClosing(WindowEvent e) { 68 | isWindowClosed = true; 69 | } 70 | }); 71 | this.setLayout(new BorderLayout()); 72 | buildMenuBar(); 73 | 74 | tabMap = new HashMap(); 75 | 76 | JProgressBar progressBar = new JProgressBar(); 77 | progressBar.setIndeterminate(true); 78 | progressPanel = new JPanel(); 79 | progressPanel.setLayout(new BorderLayout()); 80 | progressPanel.add(new JLabel("Loading..."), BorderLayout.LINE_START); 81 | progressPanel.add(progressBar, BorderLayout.LINE_END); 82 | progressPanel.setVisible(false); 83 | 84 | this.tabbedPane = new JTabbedPane(); 85 | this.add(tabbedPane, BorderLayout.CENTER); 86 | 87 | this.add(progressPanel, BorderLayout.PAGE_END); 88 | this.setTitle(JadxAiViewPlugin.PLUGIN_ID); 89 | this.setSize(800, 700); 90 | } 91 | 92 | public static String showTextAreaDialog(String text, String title) { 93 | if (title == null || title.isBlank()) { 94 | title = JadxAiViewPlugin.PLUGIN_ID; 95 | } 96 | JTextArea textArea = new JTextArea(text); 97 | textArea.setColumns(30); 98 | textArea.setRows(10); 99 | textArea.setLineWrap(true); 100 | textArea.setWrapStyleWord(true); 101 | textArea.setSize(textArea.getPreferredSize().width, textArea.getPreferredSize().height); 102 | int ret = JOptionPane.showConfirmDialog(null, new JScrollPane(textArea), title, JOptionPane.OK_OPTION); 103 | if (ret == 0) { 104 | return textArea.getText(); 105 | } 106 | return null; 107 | } 108 | 109 | private void saveTabMap(String fileName) { 110 | File file = new File(fileName); 111 | try { 112 | FileOutputStream f = new FileOutputStream(file); 113 | ObjectOutputStream s = new ObjectOutputStream(f); 114 | s.writeObject(tabMap); 115 | s.close(); 116 | } catch (IOException e) { 117 | throw new RuntimeException(e); 118 | } 119 | } 120 | 121 | private void loadTabMap(String fileName) { 122 | File file = new File(fileName); 123 | try { 124 | FileInputStream f = new FileInputStream(file); 125 | ObjectInputStream s = new ObjectInputStream(f); 126 | HashMap tabMapLoaded = (HashMap) s.readObject(); 127 | s.close(); 128 | 129 | for (Map.Entry entry : tabMapLoaded.entrySet()) { 130 | tabMap.put(entry.getKey(), entry.getValue()); 131 | addTab(entry.getKey(), entry.getValue()); 132 | } 133 | } catch (IOException | ClassNotFoundException e) { 134 | JOptionPane.showMessageDialog(null, "ERROR: Failed to load tabs from the file"); 135 | } 136 | } 137 | 138 | public void setEditorFont(int fontSize) { 139 | for (int i = 0; i < tabbedPane.getTabCount(); i++) { 140 | Component component = tabbedPane.getComponentAt(i); 141 | if (component instanceof JPanel) { 142 | JPanel panel = (JPanel) component; 143 | for (Component comp : panel.getComponents()) { 144 | if (comp instanceof RTextScrollPane) { 145 | RTextScrollPane scrollPane = ((RTextScrollPane) comp); 146 | scrollPane.getGutter().setLineNumberFont(new Font(Font.MONOSPACED, Font.PLAIN, fontSize)); 147 | if (scrollPane.getViewport().getView() instanceof RSyntaxTextArea) { 148 | RSyntaxTextArea textArea = (RSyntaxTextArea) scrollPane.getViewport().getView(); 149 | textArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, fontSize)); 150 | } 151 | } 152 | 153 | } 154 | } 155 | } 156 | } 157 | 158 | public int getFontSizePopup() { 159 | int moveCount = 0; 160 | 161 | JSlider slider = new JSlider(10, 31); 162 | slider.setMajorTickSpacing(1); 163 | slider.setPaintTicks(true); 164 | slider.setPaintLabels(true); 165 | slider.setValue(options.getEditorFontSize()); 166 | 167 | JPanel sliderPanel = new JPanel(); 168 | sliderPanel.setLayout(new GridLayout(2, 1)); 169 | sliderPanel.setPreferredSize(new Dimension(500, 115)); 170 | sliderPanel.add(slider); 171 | 172 | String title = "Set Font Size"; 173 | int dialogResponse = JOptionPane.showOptionDialog 174 | (this, 175 | sliderPanel, 176 | title, 177 | JOptionPane.OK_CANCEL_OPTION, 178 | JOptionPane.QUESTION_MESSAGE, 179 | null, null, null 180 | ); 181 | if (JOptionPane.OK_OPTION == dialogResponse) { 182 | moveCount = slider.getValue(); 183 | } else { 184 | moveCount = 0; 185 | } 186 | 187 | return moveCount; 188 | } 189 | 190 | public void buildMenuBar() { 191 | JMenuBar menuBar = new JMenuBar(); 192 | 193 | JMenu fileMenu = new JMenu("File"); 194 | JMenuItem saveItem = new JMenuItem("Save Tabs"); 195 | saveItem.addActionListener(new ActionListener() { 196 | @Override 197 | public void actionPerformed(ActionEvent e) { 198 | JFileChooser fileChooser = new JFileChooser(); 199 | fileChooser.setAcceptAllFileFilterUsed(false); 200 | fileChooser.addChoosableFileFilter(new FileNameExtensionFilter("*.jadxaiview", "jadxaiview")); 201 | fileChooser.addChoosableFileFilter(new FileFilter() { 202 | @Override 203 | public String getDescription() { 204 | return null; 205 | } 206 | 207 | @Override 208 | public boolean accept(File file) { 209 | return false; 210 | } 211 | }); 212 | int returnVal = fileChooser.showOpenDialog(null); 213 | if (returnVal == JFileChooser.APPROVE_OPTION) { 214 | File file = fileChooser.getSelectedFile(); 215 | String path = file.getPath(); 216 | if (!path.endsWith(".jadxaiview")) { 217 | path = path + ".jadxaiview"; 218 | } 219 | saveTabMap(path); 220 | repaint(); 221 | 222 | } 223 | } 224 | }); 225 | 226 | JMenuItem loadItem = new JMenuItem("Load Tabs"); 227 | loadItem.addActionListener(new ActionListener() { 228 | @Override 229 | public void actionPerformed(ActionEvent actionEvent) { 230 | JFileChooser fileChooser = new JFileChooser(); 231 | fileChooser.setAcceptAllFileFilterUsed(false); 232 | fileChooser.addChoosableFileFilter(new FileNameExtensionFilter("*.jadxaiview", "jadxaiview")); 233 | fileChooser.addChoosableFileFilter(new FileFilter() { 234 | @Override 235 | public String getDescription() { 236 | return null; 237 | } 238 | 239 | @Override 240 | public boolean accept(File file) { 241 | return false; 242 | } 243 | }); 244 | int returnVal = fileChooser.showOpenDialog(null); 245 | if (returnVal == JFileChooser.APPROVE_OPTION) { 246 | File file = fileChooser.getSelectedFile(); 247 | String path = file.getPath(); 248 | if (path.endsWith(".jadxaiview") && file.exists()) { 249 | loadTabMap(path); 250 | } 251 | repaint(); 252 | 253 | } 254 | } 255 | }); 256 | fileMenu.add(saveItem); 257 | fileMenu.add(loadItem); 258 | menuBar.add(fileMenu); 259 | 260 | 261 | JMenu settingsMenu = new JMenu("Settings"); 262 | JMenuItem editPromptItem = new JMenuItem("Edit Prompt"); 263 | editPromptItem.addActionListener(new ActionListener() { 264 | @Override 265 | public void actionPerformed(ActionEvent actionEvent) { 266 | if (options.getSelectedBackend() == JadxAiViewOptions.SupportedAIBackends.GEMINI_1_5_FLASH) { 267 | String updatedPrompt = showTextAreaDialog(options.GEMINI_PROMPT, JadxAiViewPlugin.PLUGIN_ID + " ~ Edit " + options.getSelectedBackend() + " Prompt"); 268 | if (updatedPrompt != null) { 269 | options.GEMINI_PROMPT = updatedPrompt; 270 | } 271 | } else if (options.getSelectedBackend() == JadxAiViewOptions.SupportedAIBackends.OPENAI_GPT_4_0_MINI) { 272 | String updatedPrompt = showTextAreaDialog(options.OPENAI_PROMPT, JadxAiViewPlugin.PLUGIN_ID + " ~ Edit " + options.getSelectedBackend() + " Prompt"); 273 | if (updatedPrompt != null) { 274 | options.OPENAI_PROMPT = updatedPrompt; 275 | } 276 | } else if (options.getSelectedBackend() == JadxAiViewOptions.SupportedAIBackends.GPT4FREE_GPT_3_5_TURBO) { 277 | String updatedPrompt = showTextAreaDialog(options.GPT4FREE_PROMPT, JadxAiViewPlugin.PLUGIN_ID + " ~ Edit " + options.getSelectedBackend() + " Prompt"); 278 | if (updatedPrompt != null) { 279 | options.GPT4FREE_PROMPT = updatedPrompt; 280 | } 281 | } 282 | } 283 | }); 284 | 285 | JMenuItem refreshUiItem = new JMenuItem("Refresh UI"); 286 | refreshUiItem.addActionListener(new ActionListener() { 287 | @Override 288 | public void actionPerformed(ActionEvent actionEvent) { 289 | setEditorFont(options.getEditorFontSize()); 290 | } 291 | }); 292 | 293 | JMenuItem fontSizeItem = new JMenuItem("Font Size"); 294 | fontSizeItem.addActionListener(new ActionListener() { 295 | @Override 296 | public void actionPerformed(ActionEvent actionEvent) { 297 | int fontSize = getFontSizePopup(); 298 | if (fontSize > 0) { 299 | options.setEditorFontSize(fontSize); 300 | setEditorFont(options.getEditorFontSize()); 301 | } 302 | } 303 | }); 304 | 305 | settingsMenu.add(editPromptItem); 306 | settingsMenu.add(refreshUiItem); 307 | settingsMenu.add(fontSizeItem); 308 | menuBar.add(settingsMenu); 309 | 310 | setJMenuBar(menuBar); 311 | } 312 | 313 | private JPanel createTabHead(String title) { 314 | JPanel panel = new JPanel(); 315 | panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS)); 316 | panel.setOpaque(false); 317 | JButton closeBtn = new JButton("x"); 318 | JLabel titleLabel = new JLabel(title + " "); 319 | closeBtn.setBorderPainted(false); 320 | closeBtn.setOpaque(false); 321 | 322 | closeBtn.addActionListener(new ActionListener() { 323 | public void actionPerformed(ActionEvent e) { 324 | int i; 325 | for (i = 0; i <= tabbedPane.getTabCount() - 1; i++) { 326 | if (title.equals(tabbedPane.getTitleAt(i))) 327 | break; 328 | } 329 | tabMap.remove(tabbedPane.getTitleAt(i)); 330 | tabbedPane.removeTabAt(i); 331 | } 332 | }); 333 | 334 | panel.add(titleLabel); 335 | panel.add(closeBtn); 336 | return panel; 337 | } 338 | 339 | /* To check tab name not same to not break createTabHead() */ 340 | private boolean isTabNameExists(String title) { 341 | for (int i = 0; i <= tabbedPane.getTabCount() - 1; i++) { 342 | if (title.equals(tabbedPane.getTitleAt(i))) 343 | return true; 344 | } 345 | return false; 346 | } 347 | 348 | public void addTab(String title, String body) { 349 | int count = 0; 350 | String tmp = title; 351 | while (isTabNameExists(tmp)) { 352 | count++; 353 | tmp = title + count; 354 | } 355 | title = tmp; 356 | 357 | JPanel cp = new JPanel(new BorderLayout()); 358 | 359 | RSyntaxTextArea textArea = new RSyntaxTextArea(20, 40); 360 | textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA); 361 | textArea.setCodeFoldingEnabled(true); 362 | textArea.setText(body); 363 | textArea.setEditable(false); 364 | 365 | RTextScrollPane scrollPane = new RTextScrollPane(textArea); 366 | textArea.setCaretPosition(0); 367 | cp.add(scrollPane); 368 | this.tabbedPane.addTab(title, cp); 369 | tabMap.put(title, body); 370 | 371 | int index = tabbedPane.indexOfTab(title); 372 | tabbedPane.setTabComponentAt(index, createTabHead(title)); 373 | tabbedPane.setSelectedIndex(index); 374 | setEditorFont(options.getEditorFontSize()); 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/main/java/jadx/plugins/aiview/AiViewMain.java: -------------------------------------------------------------------------------- 1 | package jadx.plugins.aiview; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | import javax.swing.JOptionPane; 6 | 7 | import jadx.api.JavaNode; 8 | 9 | public class AiViewMain { 10 | public static volatile AiViewFrame aiDialog; 11 | private static volatile AtomicInteger taskCount = new AtomicInteger(); 12 | 13 | public static void open(JadxAiViewOptions options) { 14 | if (AiViewMain.aiDialog == null || AiViewMain.aiDialog.isWindowClosed()) { 15 | AiViewMain.aiDialog = new AiViewFrame(options); 16 | AiViewMain.aiDialog.setVisible(true); 17 | AiViewMain.aiDialog.toFront(); 18 | } 19 | } 20 | 21 | public static void open(JadxAiViewOptions options, JavaNode node) { 22 | open(options); 23 | Runnable run = new Runnable() { 24 | @Override 25 | public void run() { 26 | taskCount.incrementAndGet(); 27 | AiViewMain.aiDialog.showProgressPanel(); 28 | String code = CodeExtractor.getCode(node); 29 | if (code != null && !code.isBlank()) { 30 | String response = null; 31 | 32 | if (options.getSelectedBackend() == JadxAiViewOptions.SupportedAIBackends.OPENAI_GPT_4_0_MINI) { 33 | 34 | if (options.OPENAI_API_KEY == null || options.OPENAI_API_KEY.isBlank()) { 35 | JOptionPane.showMessageDialog(null, "ERROR: Invalid API KEY, Please browse to File > Preferences > Plugins > Jadx-AI and enter your OpenAI API KEY"); 36 | } else if (options.OPENAI_PROMPT == null || options.OPENAI_PROMPT.isBlank()) { 37 | JOptionPane.showMessageDialog(null, "ERROR: OpenAI Prompt is empty, Please browse to File > Preferences > Plugins > Jadx-AI and enter an OpenAI Prompt"); 38 | } else { 39 | try { 40 | response = Ai.getOpenAIAnalysis(code, Ai.OpenAiChatModelName.GPT_4_O_MINI, options); 41 | } catch (Exception e) { 42 | response = e.getMessage(); 43 | } 44 | } 45 | 46 | } else if (options.getSelectedBackend() == JadxAiViewOptions.SupportedAIBackends.GPT4FREE_GPT_3_5_TURBO) { 47 | 48 | if (options.GPT4FREE_PROMPT == null || options.GPT4FREE_PROMPT.isBlank()) { 49 | JOptionPane.showMessageDialog(null, "ERROR: GPT4Free Prompt is empty, Please browse to File > Preferences > Plugins > Jadx-AI and enter a GPT4Free Prompt"); 50 | } else { 51 | response = Ai.getGPT4FreeAnalysis(code, Ai.OpenAiChatModelName.GPT_3_5_TURBO, options); 52 | } 53 | 54 | } else if (options.getSelectedBackend() == JadxAiViewOptions.SupportedAIBackends.GEMINI_1_5_FLASH) { 55 | 56 | if (options.GEMINI_API_KEY == null || options.GEMINI_API_KEY.isBlank()) { 57 | JOptionPane.showMessageDialog(null, "ERROR: Invalid API KEY, Please browse to File > Preferences > Plugins > Jadx-AI and enter your Gemini API KEY"); 58 | } else if (options.GEMINI_PROMPT == null || options.GEMINI_PROMPT.isBlank()) { 59 | JOptionPane.showMessageDialog(null, "ERROR: Gemini Prompt is empty, Please browse to File > Preferences > Plugins > Jadx-AI and enter a Gemini Prompt"); 60 | } else { 61 | try { 62 | response = Ai.getGeminiAnalysis(code, options); 63 | } catch (Exception e) { 64 | response = e.getMessage(); 65 | } 66 | } 67 | 68 | } 69 | 70 | if (response != null && !response.isBlank()) { 71 | AiViewMain.aiDialog.addTab(node.getFullName(), response); 72 | AiViewMain.aiDialog.toFront(); 73 | } 74 | } 75 | if (taskCount.decrementAndGet() == 0) 76 | AiViewMain.aiDialog.hideProgressPanel(); 77 | } 78 | }; 79 | new Thread(run).start(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/jadx/plugins/aiview/CodeExtractor.java: -------------------------------------------------------------------------------- 1 | package jadx.plugins.aiview; 2 | 3 | import java.util.function.BiFunction; 4 | 5 | import javax.swing.JOptionPane; 6 | 7 | import jadx.api.ICodeInfo; 8 | import jadx.api.JavaClass; 9 | import jadx.api.JavaMethod; 10 | import jadx.api.JavaNode; 11 | import jadx.api.metadata.ICodeAnnotation; 12 | import jadx.api.metadata.ICodeNodeRef; 13 | import jadx.api.metadata.annotations.NodeDeclareRef; 14 | import jadx.core.dex.nodes.ClassNode; 15 | import jadx.core.dex.nodes.MethodNode; 16 | 17 | // Credits Skylot: https://github.com/skylot/jadx/issues/2305#issuecomment-2409093335 18 | public class CodeExtractor { 19 | public static String getMethodCode(MethodNode mth, ICodeInfo codeInfo) { 20 | int startPos = getCommentStartPos(codeInfo, mth.getDefPosition()); 21 | int stopPos = getEndFrom(mth.getDefPosition(), codeInfo) + 1; 22 | return codeInfo.getCodeStr().substring(startPos, stopPos); 23 | } 24 | 25 | public static String getClassCode(ClassNode cls, ICodeInfo codeInfo) { 26 | int startPos = getCommentStartPos(codeInfo, cls.getDefPosition()); 27 | int stopPos = getEndFrom(cls.getDefPosition(), codeInfo) + 1; 28 | return codeInfo.getCodeStr().substring(startPos, stopPos); 29 | } 30 | 31 | public static String getCode(JavaNode node) { 32 | if (node instanceof JavaClass) { 33 | if (((JavaClass) node).getCode().isBlank()) { 34 | // getCode() is blank for inner class code, extract code using different method 35 | return getClassCode(((JavaClass) node).getClassNode(), node.getTopParentClass().getCodeInfo()); 36 | } 37 | return ((JavaClass) node).getCode(); 38 | } else if (node instanceof JavaMethod) { 39 | return ((JavaMethod) node).getCodeStr(); 40 | // For Jadx 1.5.0 41 | // return getMethodCode(((JavaMethod) node).getMethodNode(), node.getTopParentClass().getCodeInfo()); 42 | } 43 | return null; 44 | } 45 | 46 | protected static int getCommentStartPos(ICodeInfo codeInfo, int pos) { 47 | String emptyLine = "\n\n"; 48 | int emptyLinePos = codeInfo.getCodeStr().lastIndexOf(emptyLine, pos); 49 | return emptyLinePos == -1 ? pos : emptyLinePos + emptyLine.length(); 50 | } 51 | 52 | private static int getEndFrom(int startPosition, ICodeInfo codeInfo) { 53 | // skip nested nodes DEF/END until first unpaired END annotation (end of this method) 54 | Integer end = codeInfo.getCodeMetadata().searchDown(startPosition + 1, new BiFunction<>() { 55 | int nested = 0; 56 | 57 | @Override 58 | public Integer apply(Integer pos, ICodeAnnotation ann) { 59 | switch (ann.getAnnType()) { 60 | case DECLARATION: 61 | ICodeNodeRef node = ((NodeDeclareRef) ann).getNode(); 62 | switch (node.getAnnType()) { 63 | case CLASS: 64 | case METHOD: 65 | nested++; 66 | break; 67 | } 68 | break; 69 | 70 | case END: 71 | if (nested == 0) { 72 | return pos; 73 | } 74 | nested--; 75 | break; 76 | } 77 | return null; 78 | } 79 | }); 80 | return end != null ? end : codeInfo.getCodeStr().length(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/jadx/plugins/aiview/JadxAiViewAction.java: -------------------------------------------------------------------------------- 1 | package jadx.plugins.aiview; 2 | 3 | import jadx.api.JavaNode; 4 | import jadx.api.metadata.ICodeAnnotation; 5 | import jadx.api.metadata.ICodeNodeRef; 6 | import jadx.api.plugins.JadxPluginContext; 7 | 8 | import java.util.function.Consumer; 9 | 10 | public class JadxAiViewAction implements Consumer { 11 | private final JadxPluginContext context; 12 | private final JadxAiViewOptions options; 13 | 14 | public JadxAiViewAction(JadxPluginContext context, JadxAiViewOptions options) { 15 | this.context = context; 16 | this.options = options; 17 | } 18 | 19 | public static void addToPopupMenu(JadxPluginContext context, JadxAiViewOptions options) { 20 | if (context.getGuiContext() == null) { 21 | return; 22 | } 23 | JadxAiViewAction actionAll = new JadxAiViewAction(context, options); 24 | context.getGuiContext().addPopupMenuAction("Show AI Analysis", JadxAiViewAction::canActivate, null, actionAll); 25 | 26 | // TODO: Add to Prompt feature i.e users can right click and add a class/method to prompt so that the AI knows what the added method does 27 | // context.getGuiContext().addPopupMenuAction("Add to prompt", JadxAIAction::canActivate, null, actionAll); 28 | } 29 | 30 | public static Boolean canActivate(ICodeNodeRef ref) { 31 | return ref.getAnnType() == ICodeAnnotation.AnnType.METHOD 32 | || (ref.getAnnType() == ICodeAnnotation.AnnType.CLASS); 33 | } 34 | 35 | 36 | /* Function invoked on selecting Show AI Analysis */ 37 | @Override 38 | public void accept(ICodeNodeRef iCodeNodeRef) { 39 | JavaNode node = context.getDecompiler().getJavaNodeByRef(iCodeNodeRef); 40 | AiViewMain.open(options, node); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/jadx/plugins/aiview/JadxAiViewOptions.java: -------------------------------------------------------------------------------- 1 | package jadx.plugins.aiview; 2 | 3 | import java.io.File; 4 | import java.io.FileWriter; 5 | import java.io.IOException; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.nio.file.Paths; 9 | import java.util.Scanner; 10 | 11 | import jadx.api.plugins.options.impl.BasePluginOptionsBuilder; 12 | 13 | public class JadxAiViewOptions extends BasePluginOptionsBuilder { 14 | public String OPENAI_API_KEY = null; 15 | public String GEMINI_API_KEY = null; 16 | public String OPENAI_PROMPT = null; 17 | public String GEMINI_PROMPT = null; 18 | public String GPT4FREE_PROMPT = null; 19 | private SupportedAIBackends backendChoice; 20 | 21 | private int editorFontSize = 13; 22 | 23 | private Boolean showOnlyCodeOutput = true; 24 | 25 | public enum SupportedAIBackends { 26 | OPENAI_GPT_4_0_MINI, 27 | GEMINI_1_5_FLASH, 28 | GPT4FREE_GPT_3_5_TURBO, 29 | } 30 | 31 | ; 32 | 33 | private static void cacheFontSize(int size) { 34 | Path filePath = getCacheDirectory(); 35 | if (filePath != null) { 36 | saveToFile(filePath.toString() + File.separator + "jadx-ai.txt", String.valueOf(size)); 37 | } 38 | } 39 | 40 | private static int getFontSizeFromCache() { 41 | Path filePath = getCacheDirectory(); 42 | if (filePath != null) { 43 | return readIntFromFile(filePath.toString() + File.separator + "jadx-ai.txt"); 44 | } 45 | return 0; 46 | } 47 | 48 | private static void saveToFile(String fileName, String data) { 49 | try { 50 | FileWriter myWriter = new FileWriter(fileName); 51 | myWriter.write(data); 52 | myWriter.close(); 53 | } catch (IOException e) { 54 | e.printStackTrace(); 55 | } 56 | } 57 | 58 | private static int readIntFromFile(String fileName) { 59 | int num = 0; 60 | try { 61 | Scanner scanner = new Scanner(new File(fileName)); 62 | while (scanner.hasNextInt()) { 63 | num = scanner.nextInt(); 64 | } 65 | } catch (IOException e) { 66 | e.printStackTrace(); 67 | } 68 | return num; 69 | } 70 | 71 | private static Path getCacheDirectory() { 72 | String os = System.getProperty("os.name").toLowerCase(); 73 | Path cacheDir; 74 | 75 | if (os.contains("win")) { 76 | cacheDir = Paths.get(System.getProperty("user.home"), "AppData", "Local", "skylot", "jadx"); 77 | } else if (os.contains("mac")) { 78 | cacheDir = Paths.get(System.getProperty("user.home"), "Library", "Caches", "io.github.skylot.jadx"); 79 | } else { 80 | cacheDir = Paths.get(System.getProperty("user.home"), ".cache", "jadx"); 81 | } 82 | 83 | try { 84 | if (!Files.exists(cacheDir)) { 85 | Files.createDirectories(cacheDir); 86 | } 87 | } catch (IOException e) { 88 | return null; 89 | } 90 | 91 | return cacheDir; 92 | } 93 | 94 | @Override 95 | public void registerOptions() { 96 | strOption(JadxAiViewPlugin.PLUGIN_ID + ".OpenAI API Key") 97 | .description("OpenAI API Key") 98 | .setter(s -> OPENAI_API_KEY = s); 99 | 100 | strOption(JadxAiViewPlugin.PLUGIN_ID + ".Gemini API Key") 101 | .description("Gemini API Key") 102 | .setter(s -> GEMINI_API_KEY = s); 103 | 104 | // Default prompt credits: https://github.com/skylot/jadx/issues/1884 105 | strOption(JadxAiViewPlugin.PLUGIN_ID + ".Edit OpenAI Prompt") 106 | .description("OpenAI System Prompt") 107 | .defaultValue("Let the variable names and method names of the following code change as the name implies, the original meaning of the code cannot be changed, the order cannot be changed, and the unprocessed ones remain as they are, the number of lines of the code cannot be optimized, the code cannot be omitted, the code cannot be deleted or added, and the naming conflict cannot be allowed . The original name should be written above them in the form of a comment, keep the comment. Line comments must be added to Each line of code to explain the meaning of the code, and comments between multiple lines of code also need to be marked.") 108 | .setter(s -> OPENAI_PROMPT = s); 109 | 110 | strOption(JadxAiViewPlugin.PLUGIN_ID + ".Edit Gemini Prompt") 111 | .description("Gemini System Prompt") 112 | .defaultValue("Let the variable names and method names of the following code change as the name implies, the original meaning of the code cannot be changed, the order cannot be changed, and the unprocessed ones remain as they are, the number of lines of the code cannot be optimized, the code cannot be omitted, the code cannot be deleted or added, and the naming conflict cannot be allowed . The original name should be written above them in the form of a comment, keep the comment. Line comments must be added to Each line of code to explain the meaning of the code, and comments between multiple lines of code also need to be marked.") 113 | .setter(s -> GEMINI_PROMPT = s); 114 | 115 | strOption(JadxAiViewPlugin.PLUGIN_ID + ".Edit GPT4free Prompt") 116 | .description("GPT4free System Prompt") 117 | .defaultValue("Let the variable names and method names of the following code change as the name implies, the original meaning of the code cannot be changed, the order cannot be changed, and the unprocessed ones remain as they are, the number of lines of the code cannot be optimized, the code cannot be omitted, the code cannot be deleted or added, and the naming conflict cannot be allowed . The original name should be written above them in the form of a comment, keep the comment. Line comments must be added to Each line of code to explain the meaning of the code, and comments between multiple lines of code also need to be marked.") 118 | .setter(s -> GPT4FREE_PROMPT = s); 119 | 120 | enumOption(JadxAiViewPlugin.PLUGIN_ID + ".Select AI Backend", SupportedAIBackends.values(), SupportedAIBackends::valueOf) 121 | .description("Select AI Backend") 122 | .defaultValue(SupportedAIBackends.GPT4FREE_GPT_3_5_TURBO) 123 | .setter(v -> this.backendChoice = v); 124 | 125 | boolOption(JadxAiViewPlugin.PLUGIN_ID + ".Display only code output") 126 | .description("Display only code output") 127 | .defaultValue(true) 128 | .setter(b -> showOnlyCodeOutput = b); 129 | } 130 | 131 | public SupportedAIBackends getSelectedBackend() { 132 | return this.backendChoice; 133 | } 134 | 135 | public boolean showOnlyCodeOutput() { 136 | return this.showOnlyCodeOutput; 137 | } 138 | 139 | public int getEditorFontSize() { 140 | this.editorFontSize = getFontSizeFromCache(); 141 | if (this.editorFontSize == 0) { 142 | return 13; 143 | } 144 | return this.editorFontSize; 145 | } 146 | 147 | public void setEditorFontSize(int editorFontSize) { 148 | this.editorFontSize = editorFontSize; 149 | cacheFontSize(this.editorFontSize); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/jadx/plugins/aiview/JadxAiViewPlugin.java: -------------------------------------------------------------------------------- 1 | package jadx.plugins.aiview; 2 | 3 | import jadx.api.plugins.JadxPlugin; 4 | import jadx.api.plugins.JadxPluginContext; 5 | import jadx.api.plugins.JadxPluginInfo; 6 | import jadx.api.plugins.gui.JadxGuiContext; 7 | 8 | public class JadxAiViewPlugin implements JadxPlugin { 9 | public static final String PLUGIN_ID = "jadx-ai-view"; 10 | 11 | private final JadxAiViewOptions options = new JadxAiViewOptions(); 12 | 13 | @Override 14 | public JadxPluginInfo getPluginInfo() { 15 | return new JadxPluginInfo(PLUGIN_ID, "AI view", "Shows AI analysis of classes/methods in a seperate view"); 16 | } 17 | 18 | @Override 19 | public void init(JadxPluginContext context) { 20 | context.registerOptions(options); 21 | final JadxGuiContext guiContext = context.getGuiContext(); 22 | if (guiContext != null) { 23 | guiContext.addMenuAction("Jadx AI View", () -> { 24 | AiViewMain.open(options); 25 | }); 26 | } 27 | JadxAiViewAction.addToPopupMenu(context, options); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin: -------------------------------------------------------------------------------- 1 | jadx.plugins.aiview.JadxAiViewPlugin 2 | --------------------------------------------------------------------------------