Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Android CI

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest

permissions:
contents: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: gradle

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Build Debug APK
run: ./gradlew assembleDebug

- name: Collect APKs
run: |
mkdir -p apk_out
find app/build -type f -name "*.apk" -exec cp {} apk_out/ \;
ls -lh apk_out

- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: android-apk
path: apk_out/*.apk

- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: v-${{ github.run_number }}
name: Build ${{ github.run_number }}
files: apk_out/*.apk
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/AndroidProjectSystem.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/markdown.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions .idea/migrations.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions .idea/runConfigurations.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

111 changes: 111 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,112 @@
# NativeModMenu
A **custom reimplementation of the LGL Android Mod Menu** that loads the Java UI **entirely from memory** using `InMemoryDexClassLoader`, without shipping a separate `.dex` file on disk.

This version embeds a prebuilt `FloatingModMenu.dex` as a **HEX string** and dynamically loads it at runtime via JNI.

---

## ✨ Key Differences from Original LGL Mod Menu

- **No external dex file**
- The menu dex is embedded as HEX and loaded directly from memory.
- **In-memory class loading**
- Uses `dalvik.system.InMemoryDexClassLoader`
- **JNI-driven bootstrap**
- Menu initialization is triggered fully from native code
- **Cleaner injection surface**
- No file writes, no asset extraction
- **Compatible with existing LGL menu Java code**

---

## 🧠 How It Works (High-Level)

1. **Dex embedding**
- `FloatingModMenu.dex` is converted to a HEX string and stored in native code.
2. **HEX → ByteBuffer**
- HEX string is converted back into bytes at runtime.
3. **InMemoryDexClassLoader**
- Dex is loaded directly from memory.
4. **JNI native bindings**
- Native functions are registered to the Java menu class.
5. **Menu startup**
- Calls `FloatingModMenu.antik(Context)` to launch the floating menu.

---

## 📂 Important Components

### Embedded Dex
```cpp
static std::string DI = "HEX_CODE_OF_DEX";
```

- This must be the **HEX-encoded** version of `FloatingModMenu.dex`.

---

### JNI Native Interface

Registered native methods:

| Method | Description |
|------|------------|
| `Icon()` | Menu icon |
| `IconWebViewData()` | WebView icon data |
| `getFeatureList()` | Feature list |
| `settingsList()` | Settings list |
| `Changes(...)` | Feature toggle handler |
| `setTitleText(TextView)` | Title customization |
| `setHeadingText(TextView)` | Heading customization |

---

### Dex Loading Logic

Uses:
```java
InMemoryDexClassLoader(ByteBuffer[] dex, ClassLoader parent)
```

Loads:
```java
uk.lgl.modmenu.FloatingModMenu
```

---

## 🚀 Entry Point

```cpp
void binJava();
```

- Called after `JNI_OnLoad`
- Retrieves `Application` context via `ActivityThread`
- Starts menu initialization

---

## 🔧 Requirements

- Android 8.0+ (API 26+)
- ARMv7 / ARM64
- JNI-based injection environment
- Prebuilt compatible `FloatingModMenu.dex`

---

## ⚠️ Notes

- This project modifies runtime behavior of apps.
- Usage may violate application Terms of Service.
- Intended for educational and research purposes.

---

## 📜 Credits

- Original concept: LGL Android Mod Menu
- Project Creation: Aniket
- In-memory dex loader: NepMods

1 change: 1 addition & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
90 changes: 90 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import com.android.build.gradle.tasks.ExternalNativeBuildTask
import java.io.FileInputStream

plugins {
alias(libs.plugins.android.application)
}

class JavaDex {
public static String xxd_p(String filePath) {
StringBuilder hexString = new StringBuilder()
File file = new File(filePath)
try (FileInputStream fis = new FileInputStream(file)) {
byte[] data = fis.readAllBytes()
for (byte b : data) {
hexString.append(String.format("%02X", b))
}
}
return hexString.toString()
}
}

android {
namespace 'uk.lgl'
compileSdk 36

defaultConfig {
applicationId "uk.lgl"
minSdk 28
targetSdk 36
versionCode 1
versionName "1.0"

ndk {
abiFilters 'arm64-v8a'
}

multiDexEnabled false
}

buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

externalNativeBuild {
ndkBuild {
path file('src/main/jni/Android.mk')
}
}

android.applicationVariants.all { variant ->
def variantName = variant.name.capitalize()
def taskName = "Dex2CppAnd8Up${variantName}"

def customTask = tasks.register(taskName) {
outputs.upToDateWhen { false }

def mergeDexTask = tasks.findByName("mergeDex${variantName}")
if (mergeDexTask) {
dependsOn mergeDexTask
}

doLast {
def dexDir = file("${project.buildDir}/intermediates/dex/${variant.name}/mergeDex${variantName}")
def dexFiles = fileTree(dir: dexDir, include: "**/classes.dex")

if (dexFiles.isEmpty()) {
throw new GradleException("classes.dex not found for ${variant.name} in ${dexDir}")
}

def dexFile = dexFiles.first()
String hexString = JavaDex.xxd_p(dexFile.absolutePath)
def headerFile = file("${project.projectDir}/src/main/jni/JavaGPP/Interface/OreoOrMore.h")

headerFile.parentFile.mkdirs()
headerFile.write("#define OreoOrMore \"${hexString}\"\n")
println ">>> Success: Generated header for ${variantName}"
}
}

tasks.configureEach { task ->
if (task.name.startsWith("buildNdkBuild${variantName}") ||
task.name.startsWith("externalNativeBuild${variantName}")) {
task.dependsOn customTask
}
}
}
}
Loading