Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
440e701
feat: implement call recording functionality and fix CI secrets
mubashardev Dec 26, 2025
7690b65
fix(ui): add missing closing brace in MediaFragment
mubashardev Dec 26, 2025
aa0dd09
docs: update changelog and point github links to fork
mubashardev Dec 26, 2025
4f32623
fix: relax OriginFMessageField search criteria to resolve runtime error
mubashardev Dec 26, 2025
baa2d0d
feat: add Recordings Manager & fix: robust OriginFMessageField search
mubashardev Dec 26, 2025
ac4151e
chore: update supported whatsapp versions list to include 2.25.x
mubashardev Dec 26, 2025
f2976be
fix: replace deprecated AndroidAppHelper with FeatureLoader context
mubashardev Dec 26, 2025
1d397ed
fix: debug call recording path permission and hook triggering
mubashardev Dec 26, 2025
11900df
refactor: reimplement call recording detection using multiple VoIP cl…
mubashardev Dec 26, 2025
2233e2d
feat: Implement comprehensive call recording with UI for management a…
mubashardev Dec 26, 2025
0a28640
Merge upstream/master and resolve conflicts
mubashardev Dec 29, 2025
780b2f0
ci: Enable Android workflow to trigger on feature/* branches.
mubashardev Dec 29, 2025
b095467
Merge pull request #1 from mubashardev/feature/call-recording-fixes
mubashardev Dec 29, 2025
6d9387b
Revert project URLs to original (keeping feature attribution)
mubashardev Dec 29, 2025
5facbaf
Merge upstream/master: Add new features + preserve Call Recording
mubashardev Jan 17, 2026
1513c5a
ci: trigger build after upstream merge
mubashardev Jan 17, 2026
00a3834
ci: trigger build with latest upstream changes
mubashardev Jan 17, 2026
d25a631
Merge upstream/master: Sync with latest upstream changes (17 commits)
mubashardev Jan 28, 2026
81eab24
ci: trigger release build with upstream sync
mubashardev Jan 28, 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/*" ]
Comment on lines 3 to +5
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workflow now runs on push to feature/*. Since the signing keystore is still written for any non-PR event, this broadens where secrets are usable/exposable (anyone with push access can create a feature/* branch and modify the workflow to exfiltrate). Consider limiting secret-using steps to protected branches/tags or GitHub Environments with required reviewers.

Copilot uses AI. Check for mistakes.
jobs:
build:
permissions: write-all
Expand All @@ -19,13 +19,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
15 changes: 15 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@
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.ForceStartActivity"
android:excludeFromRecents="true"
Expand Down Expand Up @@ -115,6 +120,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
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package com.wmods.wppenhacer.activities;

import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;

import com.google.android.material.appbar.MaterialToolbar;
import com.wmods.wppenhacer.R;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;

public class CallRecordingSettingsActivity extends AppCompatActivity {

private static final String TAG = "WaEnhancer";
private SharedPreferences prefs;
private RadioGroup radioGroupMode;
private RadioButton radioRoot;
private RadioButton radioNonRoot;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_call_recording_settings);

MaterialToolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.call_recording_settings);
}

prefs = PreferenceManager.getDefaultSharedPreferences(this);

radioGroupMode = findViewById(R.id.radio_group_mode);
radioRoot = findViewById(R.id.radio_root);
radioNonRoot = findViewById(R.id.radio_non_root);

// Load saved preference
boolean useRoot = prefs.getBoolean("call_recording_use_root", false);
Log.d(TAG, "Loaded call_recording_use_root: " + useRoot);

if (useRoot) {
radioRoot.setChecked(true);
} else {
radioNonRoot.setChecked(true);
}

// Direct click listeners on radio buttons
radioRoot.setOnClickListener(v -> {
Log.d(TAG, "Root mode clicked");
radioRoot.setChecked(true);
radioNonRoot.setChecked(false);
Toast.makeText(this, "Checking root access...", Toast.LENGTH_SHORT).show();
checkRootAccess();
});

radioNonRoot.setOnClickListener(v -> {
Log.d(TAG, "Non-root mode clicked");
radioNonRoot.setChecked(true);
radioRoot.setChecked(false);
boolean saved = prefs.edit().putBoolean("call_recording_use_root", false).commit();
Log.d(TAG, "Saved non-root preference: " + saved);
Toast.makeText(this, R.string.non_root_mode_enabled, Toast.LENGTH_SHORT).show();
});
}

private void checkRootAccess() {
new Thread(() -> {
boolean hasRoot = false;
String rootOutput = "";

try {
Log.d(TAG, "Executing su command...");
Process process = Runtime.getRuntime().exec("su");
DataOutputStream os = new DataOutputStream(process.getOutputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

os.writeBytes("id\n");
os.writeBytes("exit\n");
os.flush();

// Read output
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
rootOutput = sb.toString();

int exitCode = process.waitFor();
Log.d(TAG, "Root check exit code: " + exitCode + ", output: " + rootOutput);

Comment on lines +84 to +101
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This InputStreamReader is not always closed on method exit.

Suggested change
DataOutputStream os = new DataOutputStream(process.getOutputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
os.writeBytes("id\n");
os.writeBytes("exit\n");
os.flush();
// Read output
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
rootOutput = sb.toString();
int exitCode = process.waitFor();
Log.d(TAG, "Root check exit code: " + exitCode + ", output: " + rootOutput);
try (DataOutputStream os = new DataOutputStream(process.getOutputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
os.writeBytes("id\n");
os.writeBytes("exit\n");
os.flush();
// Read output
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
rootOutput = sb.toString();
}
int exitCode = process.waitFor();
Log.d(TAG, "Root check exit code: " + exitCode + ", output: " + rootOutput);

Copilot uses AI. Check for mistakes.
hasRoot = (exitCode == 0 && rootOutput.contains("uid=0"));
Comment on lines +84 to +102
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This DataOutputStream is not always closed on method exit.

Suggested change
DataOutputStream os = new DataOutputStream(process.getOutputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
os.writeBytes("id\n");
os.writeBytes("exit\n");
os.flush();
// Read output
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
rootOutput = sb.toString();
int exitCode = process.waitFor();
Log.d(TAG, "Root check exit code: " + exitCode + ", output: " + rootOutput);
hasRoot = (exitCode == 0 && rootOutput.contains("uid=0"));
try (DataOutputStream os = new DataOutputStream(process.getOutputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
os.writeBytes("id\n");
os.writeBytes("exit\n");
os.flush();
// Read output
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
rootOutput = sb.toString();
int exitCode = process.waitFor();
Log.d(TAG, "Root check exit code: " + exitCode + ", output: " + rootOutput);
hasRoot = (exitCode == 0 && rootOutput.contains("uid=0"));
}

Copilot uses AI. Check for mistakes.
} catch (Exception e) {
Log.e(TAG, "Root check exception: " + e.getMessage());
hasRoot = false;
}

final boolean rootGranted = hasRoot;
final String output = rootOutput;

runOnUiThread(() -> {
if (rootGranted) {
boolean saved = prefs.edit().putBoolean("call_recording_use_root", true).commit();
Log.d(TAG, "Root granted, saved preference: " + saved);
Toast.makeText(this, R.string.root_access_granted, Toast.LENGTH_SHORT).show();
} else {
boolean saved = prefs.edit().putBoolean("call_recording_use_root", false).commit();
Log.d(TAG, "Root denied, saved preference: " + saved + ", output: " + output);
radioNonRoot.setChecked(true);
Toast.makeText(this, R.string.root_access_denied, Toast.LENGTH_LONG).show();
}
});
}).start();
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ protected void onCreate(Bundle savedInstanceState) {
MainPagerAdapter pagerAdapter = new MainPagerAdapter(this);
binding.viewPager.setAdapter(pagerAdapter);

var prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this);
if (!prefs.getBoolean("call_recording_enable", false)) {
binding.navView.getMenu().findItem(R.id.navigation_recordings).setVisible(false);
}
binding.viewPager.setPageTransformer(new DepthPageTransformer());

binding.navView.setOnItemSelectedListener(new NavigationBarView.OnItemSelectedListener() {
Expand Down Expand Up @@ -70,6 +74,10 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) {
binding.viewPager.setCurrentItem(4, true);
yield true;
}
case R.id.navigation_recordings -> {
binding.viewPager.setCurrentItem(5);
yield true;
}
default -> false;
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.PreferenceManager;
import androidx.viewpager2.adapter.FragmentStateAdapter;

import com.wmods.wppenhacer.ui.fragments.CustomizationFragment;
import com.wmods.wppenhacer.ui.fragments.GeneralFragment;
import com.wmods.wppenhacer.ui.fragments.HomeFragment;
import com.wmods.wppenhacer.ui.fragments.MediaFragment;
import com.wmods.wppenhacer.ui.fragments.PrivacyFragment;
import com.wmods.wppenhacer.ui.fragments.RecordingsFragment;

public class MainPagerAdapter extends FragmentStateAdapter {

private final boolean isRecordingEnabled;

public MainPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
var prefs = PreferenceManager.getDefaultSharedPreferences(fragmentActivity);
isRecordingEnabled = prefs.getBoolean("call_recording_enable", false);
}

@NonNull
Expand All @@ -25,12 +31,13 @@ public Fragment createFragment(int position) {
case 1 -> new PrivacyFragment();
case 3 -> new MediaFragment();
case 4 -> new CustomizationFragment();
case 5 -> new RecordingsFragment();
default -> new HomeFragment();
};
}

@Override
public int getItemCount() {
return 5; // Number of fragments
return isRecordingEnabled ? 6 : 5;
}
}
Loading