Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f0e1641
feat: implement call recording functionality and fix CI secrets
mubashardev Dec 26, 2025
5721e65
fix(ui): add missing closing brace in MediaFragment
mubashardev Dec 26, 2025
9728acf
docs: update changelog and point github links to fork
mubashardev Dec 26, 2025
8e5e064
fix: relax OriginFMessageField search criteria to resolve runtime error
mubashardev Dec 26, 2025
3d71bf2
feat: add Recordings Manager & fix: robust OriginFMessageField search
mubashardev Dec 26, 2025
24ff130
fix: replace deprecated AndroidAppHelper with FeatureLoader context
mubashardev Dec 26, 2025
52547fb
fix: debug call recording path permission and hook triggering
mubashardev Dec 26, 2025
473cf73
refactor: reimplement call recording detection using multiple VoIP cl…
mubashardev Dec 26, 2025
07673ed
feat: Implement comprehensive call recording with UI for management a…
mubashardev Dec 26, 2025
0ecbc35
ci: Enable Android workflow to trigger on feature/* branches.
mubashardev Dec 29, 2025
1e4efce
Revert project URLs to original (keeping feature attribution)
mubashardev Dec 29, 2025
1bac85f
ci: trigger build after upstream merge
mubashardev Jan 17, 2026
864ef2f
ci: trigger build with latest upstream changes
mubashardev Jan 17, 2026
807c0eb
ci: trigger release build with upstream sync
mubashardev Jan 28, 2026
6da792f
feat: Add experimental "Force Restore Backup" option to manually laun…
mubashardev Feb 9, 2026
33b09ff
feat: Implement `getPluginName()` and refactor preference variable na…
mubashardev Feb 9, 2026
b1b1d8d
docs: add explainer image
mubashardev Feb 10, 2026
29cfd4c
Add WhatsApp Business to LSPosed scope for simple variant
mubashardev Feb 12, 2026
03328bc
feat: enable signing configuration to load credentials from local.pro…
mubashardev Feb 14, 2026
0e982b9
feat: Implement Markdown rendering for update changelogs and refactor…
mubashardev Feb 14, 2026
f8b4763
feat: Add global search functionality with preference navigation to s…
mubashardev Feb 14, 2026
270f304
feat: Implement preference highlighting, refine update check timing, …
mubashardev Feb 14, 2026
675e5b1
docs: Update changelog to include new global search and app load opti…
mubashardev Feb 14, 2026
16e86cd
feat: Add a toggle for call recording toast notifications, defaulting…
mubashardev Feb 16, 2026
9f90413
chore: Remove call recording toast strings and downgrade AGP and Kotl…
mubashardev Feb 16, 2026
4f9f568
feat: Implement deleted messages recovery and display functionality w…
mubashardev Feb 17, 2026
37d1972
Implement dedicated UI for deleted messages, including distinct sent …
mubashardev Feb 18, 2026
bc6e2b6
refactor: Improve deleted message data extraction and contact name re…
mubashardev Feb 18, 2026
581a7d7
feat: Improve name resolution in RecoverDeleteForMe by adding more re…
mubashardev Feb 18, 2026
8f34f41
feat: Propagate contact name to all deleted messages within a chat an…
mubashardev Feb 18, 2026
baf3df9
feat: Enhance deleted message recovery with improved text extraction,…
mubashardev Feb 18, 2026
ab680ce
update content provider authority to use application ID.
mubashardev Feb 18, 2026
47d52e1
feat: Add chat info dialog to message list activity and ensure messag…
mubashardev Feb 18, 2026
41ae377
feat: display app icon in the avatar ImageView, hide the app badge, a…
mubashardev Feb 18, 2026
d8d1152
feat: Implement global search and enhance deleted message recovery wi…
mubashardev Feb 19, 2026
37d50d8
Fix: Show JID instead of generic app name in Deleted Messages list
mubashardev Feb 19, 2026
413e1f9
feat: Add new color themes, improve profile photo retrieval, and refi…
mubashardev Feb 20, 2026
cf09efd
feat: Overhaul README.md with detailed, categorized features and add …
mubashardev Feb 20, 2026
4c432ca
fix: Build errors after rebase (DexKit API mismatches and duplicate s…
mubashardev Feb 20, 2026
b632f43
fix: loadDialogViewClass identification logic
mubashardev Feb 21, 2026
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
8 changes: 5 additions & 3 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Android CI

on:
push:
branches: [ "master" ]
branches: [ "master", "feature/*" ]
jobs:
build:
permissions: write-all
Expand All @@ -22,13 +22,15 @@ jobs:

- name: Write key
if: github.event_name != 'pull_request'
env:
KEY_STORE: ${{ secrets.KEY_STORE }}
run: |
if [ ! -z "${{ secrets.KEY_STORE }}" ]; then
if [ ! -z "$KEY_STORE" ]; then
echo androidStorePassword='${{ secrets.KEY_STORE_PASSWORD }}' >> gradle.properties
echo androidKeyAlias='${{ secrets.ALIAS }}' >> gradle.properties
echo androidKeyPassword='${{ secrets.KEY_PASSWORD }}' >> gradle.properties
echo androidStoreFile='key.jks' >> gradle.properties
echo ${{ secrets.KEY_STORE }} | base64 --decode > key.jks
echo "$KEY_STORE" | base64 --decode > key.jks
fi

- name: Grant execute permission for gradlew
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
.cxx
local.properties
key.jks
key_base64.txt
20 changes: 17 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import java.util.Locale
import java.util.Properties
import java.io.FileInputStream

plugins {
alias(libs.plugins.androidApplication)
Expand Down Expand Up @@ -50,12 +52,23 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

signingConfigs.create("config") {
val keystorePropertiesFile = rootProject.file("local.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}

val androidStoreFile = project.findProperty("androidStoreFile") as String?
?: keystoreProperties.getProperty("androidStoreFile")

if (!androidStoreFile.isNullOrEmpty()) {
storeFile = rootProject.file(androidStoreFile)
storePassword = project.property("androidStorePassword") as String
keyAlias = project.property("androidKeyAlias") as String
keyPassword = project.property("androidKeyPassword") as String
storePassword = project.findProperty("androidStorePassword") as String?
?: keystoreProperties.getProperty("androidStorePassword")
keyAlias = project.findProperty("androidKeyAlias") as String?
?: keystoreProperties.getProperty("androidKeyAlias")
keyPassword = project.findProperty("androidKeyPassword") as String?
?: keystoreProperties.getProperty("androidKeyPassword")
}
}

Expand Down Expand Up @@ -166,6 +179,7 @@ dependencies {
implementation(libs.arscblamer)
compileOnly(libs.lombok)
annotationProcessor(libs.lombok)
implementation(libs.markwon.core)
}

configurations.all {
Expand Down
37 changes: 37 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.READ_CONTACTS" />

<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
Expand Down Expand Up @@ -57,6 +58,11 @@
</service>


<provider
android:name=".provider.DeletedMessagesProvider"
android:authorities="${applicationId}.provider"
android:exported="true" />

<activity
android:name=".activities.MainActivity"
android:exported="true"
Expand All @@ -82,6 +88,27 @@
android:name=".activities.TextEditorActivity"
android:theme="@style/AppTheme" />

<activity
android:name=".activities.CallRecordingSettingsActivity"
android:theme="@style/Theme.Material3.DynamicColors.DayNight.NoActionBar"
android:parentActivityName=".activities.MainActivity" />

<activity
android:name=".activities.SearchActivity"
android:theme="@style/AppTheme"
android:parentActivityName=".activities.MainActivity"
android:windowSoftInputMode="adjustResize" />

<activity
android:name=".activities.DeletedMessagesActivity"
android:theme="@style/AppTheme"
android:parentActivityName=".activities.MainActivity" />

<activity
android:name=".activities.MessageListActivity"
android:theme="@style/AppTheme"
android:parentActivityName=".activities.DeletedMessagesActivity" />

<activity
android:name=".activities.ForceStartActivity"
android:excludeFromRecents="true"
Expand Down Expand Up @@ -115,6 +142,16 @@
android:exported="true"
tools:ignore="ExportedService" />

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>

<provider
android:name=".xposed.bridge.providers.HookProvider"
android:authorities="${applicationId}.hookprovider"
Expand Down
162 changes: 141 additions & 21 deletions app/src/main/java/com/wmods/wppenhacer/UpdateChecker.java
Original file line number Diff line number Diff line change
@@ -1,71 +1,191 @@
package com.wmods.wppenhacer;

import android.app.Activity;
import android.util.Log;

import com.wmods.wppenhacer.xposed.core.WppCore;
import com.wmods.wppenhacer.xposed.core.components.AlertDialogWpp;
import com.wmods.wppenhacer.xposed.utils.Utils;

import org.json.JSONObject;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import de.robv.android.xposed.XposedBridge;
import okhttp3.OkHttpClient;

import io.noties.markwon.Markwon;

public class UpdateChecker implements Runnable {

private static final String TAG = "WAE_UpdateChecker";
private static final String LATEST_RELEASE_API = "https://api.github.com/repos/Dev4Mod/WaEnhancer/releases/latest";
private static final String RELEASE_TAG_PREFIX = "debug-";
private static final String TELEGRAM_UPDATE_URL = "https://t.me/waenhancher";
private static final String TELEGRAM_UPDATE_URL = "https://github.com/Dev4Mod/WaEnhancer/releases";

// Singleton OkHttpClient - expensive to create, reuse across all checks
private static OkHttpClient httpClient;

private final Activity mActivity;

public UpdateChecker(Activity activity) {
this.mActivity = activity;
}

/**
* Get or create the singleton OkHttpClient with proper timeout configuration
*/
private static synchronized OkHttpClient getHttpClient() {
if (httpClient == null) {
httpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build();
}
return httpClient;
}

@Override
public void run() {
XposedBridge.log("[" + TAG + "] UpdateChecker.run() started");
try {
var client = new OkHttpClient();
XposedBridge.log("[" + TAG + "] Starting update check...");

var request = new okhttp3.Request.Builder()
.url(LATEST_RELEASE_API)
.build();

String hash;
String changelog;
try (var response = client.newCall(request).execute()) {
if (!response.isSuccessful()) return;
String publishedAt;

try (var response = getHttpClient().newCall(request).execute()) {
if (!response.isSuccessful()) {
XposedBridge.log("[" + TAG + "] Update check failed: HTTP " + response.code());
return;
}

var body = response.body();
if (body == null) {
XposedBridge.log("[" + TAG + "] Update check failed: Empty response body");
return;
}

var content = body.string();
var release = new JSONObject(content);
var tagName = release.optString("tag_name", "");
XposedBridge.log("[UPDATE]" +tagName);
if (tagName.isBlank() || !tagName.startsWith(RELEASE_TAG_PREFIX)) return;

XposedBridge.log("[" + TAG + "] Latest release tag: " + tagName);

if (tagName.isBlank() || !tagName.startsWith(RELEASE_TAG_PREFIX)) {
XposedBridge.log("[" + TAG + "] Invalid or non-debug release tag");
return;
}

hash = tagName.substring(RELEASE_TAG_PREFIX.length()).trim();
changelog = release.optString("body", "No changelog available.").trim();
publishedAt = release.optString("published_at", "");

XposedBridge.log("[" + TAG + "] Release hash: " + hash + ", published: " + publishedAt);
}

if (hash.isBlank()) return;
if (hash.isBlank()) {
XposedBridge.log("[" + TAG + "] Empty hash, skipping");
return;
}

var appInfo = mActivity.getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, 0);
if (!appInfo.versionName.toLowerCase().contains(hash.toLowerCase().trim()) && !Objects.equals(WppCore.getPrivString("ignored_version", ""), hash)) {
boolean isNewVersion = !appInfo.versionName.toLowerCase().contains(hash.toLowerCase().trim());
boolean isIgnored = Objects.equals(WppCore.getPrivString("ignored_version", ""), hash);

if (isNewVersion && !isIgnored) {
XposedBridge.log("[" + TAG + "] New version available, showing dialog");

final String finalHash = hash;
final String finalChangelog = changelog;
final String finalPublishedAt = publishedAt;

mActivity.runOnUiThread(() -> {
var dialog = new AlertDialogWpp(mActivity);
dialog.setTitle("WAE - New version available!");
dialog.setMessage("Changelog:\n\n" + changelog);
dialog.setNegativeButton("Ignore", (dialog1, which) -> {
WppCore.setPrivString("ignored_version", hash);
dialog1.dismiss();
});
dialog.setPositiveButton("Update", (dialog1, which) -> {
Utils.openLink(mActivity, TELEGRAM_UPDATE_URL);
dialog1.dismiss();
});
dialog.show();
showUpdateDialog(finalHash, finalChangelog, finalPublishedAt);
});
} else {
XposedBridge.log("[" + TAG + "] No update needed (isNew=" + isNewVersion + ", isIgnored=" + isIgnored + ")");
}
} catch (java.net.SocketTimeoutException e) {
XposedBridge.log("[" + TAG + "] Update check timeout: " + e.getMessage());
} catch (java.io.IOException e) {
XposedBridge.log("[" + TAG + "] Network error during update check: " + e.getMessage());
} catch (Exception e) {
XposedBridge.log("[" + TAG + "] Unexpected error during update check: " + e.getMessage());
XposedBridge.log(e);
}
}

private void showUpdateDialog(String hash, String changelog, String publishedAt) {
XposedBridge.log("[" + TAG + "] Attempting to show update dialog");
try {
var markwon = Markwon.create(mActivity);
var dialog = new AlertDialogWpp(mActivity);

// Format the published date
String formattedDate = formatPublishedDate(publishedAt);

// Build simple message with version and date
StringBuilder message = new StringBuilder();
message.append("📦 **Version:** `").append(hash).append("`\n");
if (!formattedDate.isEmpty()) {
message.append("📅 **Released:** ").append(formattedDate).append("\n");
}
message.append("\n### What's New\n\n").append(changelog);

dialog.setTitle("🎉 New Update Available!");
dialog.setMessage(markwon.toMarkdown(message.toString()));
dialog.setNegativeButton("Ignore", (dialog1, which) -> {
WppCore.setPrivString("ignored_version", hash);
dialog1.dismiss();
});
dialog.setPositiveButton("Update Now", (dialog1, which) -> {
Utils.openLink(mActivity, TELEGRAM_UPDATE_URL);
dialog1.dismiss();
});
dialog.show();

XposedBridge.log("[" + TAG + "] Update dialog shown successfully");
} catch (Exception e) {
XposedBridge.log("[" + TAG + "] Error showing update dialog: " + e.getMessage());
e.printStackTrace();
}
}

/**
* Format ISO 8601 date to human-readable format
* @param isoDate ISO 8601 date string (e.g., "2024-02-14T12:34:56Z")
* @return Formatted date (e.g., "Feb 14, 2024" or "February 14, 2024")
*/
private String formatPublishedDate(String isoDate) {
if (isoDate == null || isoDate.isEmpty()) {
return "";
}

try {
// Parse ISO 8601 date
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
Date date = isoFormat.parse(isoDate);

if (date != null) {
// Format to readable date
SimpleDateFormat displayFormat = new SimpleDateFormat("MMM dd, yyyy", Locale.US);
return displayFormat.format(date);
}
} catch (Exception ignored) {
} catch (Exception e) {
XposedBridge.log("[" + TAG + "] Error parsing date: " + e.getMessage());
}

return "";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
binding.btnGithub.setOnClickListener(view -> {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://github.com/Dev4Mod/waenhancer"));
intent.setData(Uri.parse("https://github.com/Dev4Mod/WaEnhancer"));
startActivity(intent);
});
binding.btnDonate.setOnClickListener(view -> {
Expand Down
Loading