Skip to content

Commit baa2d0d

Browse files
committed
feat: add Recordings Manager & fix: robust OriginFMessageField search
1 parent 4f32623 commit baa2d0d

File tree

12 files changed

+463
-11
lines changed

12 files changed

+463
-11
lines changed

app/src/main/java/com/wmods/wppenhacer/activities/MainActivity.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ protected void onCreate(Bundle savedInstanceState) {
4646
MainPagerAdapter pagerAdapter = new MainPagerAdapter(this);
4747
binding.viewPager.setAdapter(pagerAdapter);
4848

49+
var prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this);
50+
if (!prefs.getBoolean("call_recording_enable", false)) {
51+
binding.navView.getMenu().findItem(R.id.navigation_recordings).setVisible(false);
52+
}
53+
4954
binding.navView.setOnItemSelectedListener(new NavigationBarView.OnItemSelectedListener() {
5055
@SuppressLint("NonConstantResourceId")
5156
@Override
@@ -71,6 +76,10 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) {
7176
binding.viewPager.setCurrentItem(4);
7277
yield true;
7378
}
79+
case R.id.navigation_recordings -> {
80+
binding.viewPager.setCurrentItem(5);
81+
yield true;
82+
}
7483
default -> false;
7584
};
7685
}

app/src/main/java/com/wmods/wppenhacer/adapter/MainPagerAdapter.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,24 @@
33
import androidx.annotation.NonNull;
44
import androidx.fragment.app.Fragment;
55
import androidx.fragment.app.FragmentActivity;
6+
import androidx.preference.PreferenceManager;
67
import androidx.viewpager2.adapter.FragmentStateAdapter;
78

89
import com.wmods.wppenhacer.ui.fragments.CustomizationFragment;
910
import com.wmods.wppenhacer.ui.fragments.GeneralFragment;
1011
import com.wmods.wppenhacer.ui.fragments.HomeFragment;
1112
import com.wmods.wppenhacer.ui.fragments.MediaFragment;
1213
import com.wmods.wppenhacer.ui.fragments.PrivacyFragment;
14+
import com.wmods.wppenhacer.ui.fragments.RecordingsFragment;
1315

1416
public class MainPagerAdapter extends FragmentStateAdapter {
1517

18+
private final boolean isRecordingEnabled;
19+
1620
public MainPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
1721
super(fragmentActivity);
22+
var prefs = PreferenceManager.getDefaultSharedPreferences(fragmentActivity);
23+
isRecordingEnabled = prefs.getBoolean("call_recording_enable", false);
1824
}
1925

2026
@NonNull
@@ -25,12 +31,13 @@ public Fragment createFragment(int position) {
2531
case 1 -> new PrivacyFragment();
2632
case 3 -> new MediaFragment();
2733
case 4 -> new CustomizationFragment();
34+
case 5 -> new RecordingsFragment();
2835
default -> new HomeFragment();
2936
};
3037
}
3138

3239
@Override
3340
public int getItemCount() {
34-
return 5; // Number of fragments
41+
return isRecordingEnabled ? 6 : 5;
3542
}
3643
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.wmods.wppenhacer.adapter;
2+
3+
import android.annotation.SuppressLint;
4+
import android.content.Context;
5+
import android.text.format.Formatter;
6+
import android.view.LayoutInflater;
7+
import android.view.View;
8+
import android.view.ViewGroup;
9+
import android.widget.ImageButton;
10+
import android.widget.TextView;
11+
12+
import androidx.annotation.NonNull;
13+
import androidx.recyclerview.widget.RecyclerView;
14+
15+
import com.wmods.wppenhacer.R;
16+
17+
import java.io.File;
18+
import java.text.SimpleDateFormat;
19+
import java.util.ArrayList;
20+
import java.util.Date;
21+
import java.util.List;
22+
import java.util.Locale;
23+
24+
public class RecordingsAdapter extends RecyclerView.Adapter<RecordingsAdapter.ViewHolder> {
25+
26+
private List<File> files = new ArrayList<>();
27+
private final OnRecordingActionListener listener;
28+
29+
public interface OnRecordingActionListener {
30+
void onPlay(File file);
31+
void onShare(File file);
32+
void onDelete(File file);
33+
}
34+
35+
public RecordingsAdapter(OnRecordingActionListener listener) {
36+
this.listener = listener;
37+
}
38+
39+
@SuppressLint("NotifyDataSetChanged")
40+
public void setFiles(List<File> files) {
41+
this.files = files;
42+
notifyDataSetChanged();
43+
}
44+
45+
@NonNull
46+
@Override
47+
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
48+
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_recording, parent, false);
49+
return new ViewHolder(view);
50+
}
51+
52+
@Override
53+
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
54+
File file = files.get(position);
55+
Context context = holder.itemView.getContext();
56+
57+
holder.name.setText(file.getName());
58+
59+
String size = Formatter.formatFileSize(context, file.length());
60+
String date = new SimpleDateFormat("dd MMM yyyy HH:mm", Locale.getDefault()).format(new Date(file.lastModified()));
61+
// Duration would require MediaPlayer parsing, expensive for list. Using size/date for now.
62+
63+
holder.details.setText(String.format("%s • %s", size, date));
64+
65+
holder.btnPlay.setOnClickListener(v -> listener.onPlay(file));
66+
holder.btnShare.setOnClickListener(v -> listener.onShare(file));
67+
holder.btnDelete.setOnClickListener(v -> listener.onDelete(file));
68+
}
69+
70+
@Override
71+
public int getItemCount() {
72+
return files.size();
73+
}
74+
75+
static class ViewHolder extends RecyclerView.ViewHolder {
76+
TextView name, details;
77+
ImageButton btnPlay, btnShare, btnDelete;
78+
79+
public ViewHolder(@NonNull View itemView) {
80+
super(itemView);
81+
name = itemView.findViewById(R.id.name);
82+
details = itemView.findViewById(R.id.details);
83+
btnPlay = itemView.findViewById(R.id.btn_play);
84+
btnShare = itemView.findViewById(R.id.btn_share);
85+
btnDelete = itemView.findViewById(R.id.btn_delete);
86+
}
87+
}
88+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package com.wmods.wppenhacer.ui.fragments;
2+
3+
import android.app.AlertDialog;
4+
import android.content.Intent;
5+
import android.net.Uri;
6+
import android.os.Bundle;
7+
import android.os.Environment;
8+
import android.view.LayoutInflater;
9+
import android.view.View;
10+
import android.view.ViewGroup;
11+
import android.webkit.MimeTypeMap;
12+
import android.widget.PopupMenu;
13+
import android.widget.Toast;
14+
15+
import androidx.annotation.NonNull;
16+
import androidx.annotation.Nullable;
17+
import androidx.core.content.FileProvider;
18+
import androidx.fragment.app.Fragment;
19+
import androidx.preference.PreferenceManager;
20+
import androidx.recyclerview.widget.LinearLayoutManager;
21+
22+
import com.wmods.wppenhacer.R;
23+
import com.wmods.wppenhacer.adapter.RecordingsAdapter;
24+
import com.wmods.wppenhacer.databinding.FragmentRecordingsBinding;
25+
26+
import java.io.File;
27+
import java.util.ArrayList;
28+
import java.util.Arrays;
29+
import java.util.Collections;
30+
import java.util.Comparator;
31+
import java.util.List;
32+
33+
public class RecordingsFragment extends Fragment implements RecordingsAdapter.OnRecordingActionListener {
34+
35+
private FragmentRecordingsBinding binding;
36+
private RecordingsAdapter adapter;
37+
private List<File> recordingFiles = new ArrayList<>();
38+
private File baseDir;
39+
40+
@Nullable
41+
@Override
42+
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
43+
binding = FragmentRecordingsBinding.inflate(inflater, container, false);
44+
return binding.getRoot();
45+
}
46+
47+
@Override
48+
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
49+
super.onViewCreated(view, savedInstanceState);
50+
51+
adapter = new RecordingsAdapter(this);
52+
binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
53+
binding.recyclerView.setAdapter(adapter);
54+
55+
var prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
56+
String path = prefs.getString("call_recording_path", Environment.getExternalStorageDirectory() + "/Music/WaEnhancer/Recordings");
57+
baseDir = new File(path);
58+
59+
binding.fabSort.setOnClickListener(v -> showSortMenu());
60+
61+
loadRecordings();
62+
}
63+
64+
private void loadRecordings() {
65+
recordingFiles.clear();
66+
if (baseDir.exists() && baseDir.isDirectory()) {
67+
traverseDirectory(baseDir);
68+
}
69+
70+
if (recordingFiles.isEmpty()) {
71+
binding.emptyView.setVisibility(View.VISIBLE);
72+
binding.recyclerView.setVisibility(View.GONE);
73+
} else {
74+
binding.emptyView.setVisibility(View.GONE);
75+
binding.recyclerView.setVisibility(View.VISIBLE);
76+
// Default sort by date desc
77+
recordingFiles.sort((f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified()));
78+
adapter.setFiles(recordingFiles);
79+
}
80+
}
81+
82+
private void traverseDirectory(File dir) {
83+
File[] files = dir.listFiles();
84+
if (files != null) {
85+
for (File file : files) {
86+
if (file.isDirectory()) {
87+
traverseDirectory(file);
88+
} else {
89+
if (file.getName().endsWith(".wav") || file.getName().endsWith(".mp3") || file.getName().endsWith(".aac")) {
90+
recordingFiles.add(file);
91+
}
92+
}
93+
}
94+
}
95+
}
96+
97+
private void showSortMenu() {
98+
PopupMenu popup = new PopupMenu(requireContext(), binding.fabSort);
99+
popup.getMenu().add(0, 1, 0, R.string.sort_date);
100+
popup.getMenu().add(0, 2, 0, R.string.sort_name);
101+
popup.getMenu().add(0, 3, 0, R.string.sort_duration);
102+
103+
popup.setOnMenuItemClickListener(item -> {
104+
switch (item.getItemId()) {
105+
case 1 -> recordingFiles.sort((f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified()));
106+
case 2 -> recordingFiles.sort(Comparator.comparing(File::getName));
107+
case 3 -> recordingFiles.sort((f1, f2) -> Long.compare(f2.length(), f1.length())); // Approximation by size
108+
}
109+
adapter.setFiles(recordingFiles);
110+
return true;
111+
});
112+
popup.show();
113+
}
114+
115+
@Override
116+
public void onPlay(File file) {
117+
try {
118+
Uri uri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".provider", file);
119+
Intent intent = new Intent(Intent.ACTION_VIEW);
120+
intent.setDataAndType(uri, "audio/*");
121+
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
122+
startActivity(intent);
123+
} catch (Exception e) {
124+
Toast.makeText(requireContext(), "Error playing file: " + e.getMessage(), Toast.LENGTH_SHORT).show();
125+
e.printStackTrace();
126+
}
127+
}
128+
129+
@Override
130+
public void onShare(File file) {
131+
try {
132+
Uri uri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".provider", file);
133+
Intent intent = new Intent(Intent.ACTION_SEND);
134+
intent.getType(); // check
135+
intent.setType("audio/*");
136+
intent.putExtra(Intent.EXTRA_STREAM, uri);
137+
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
138+
startActivity(Intent.createChooser(intent, getString(R.string.share_recording)));
139+
} catch (Exception e) {
140+
e.printStackTrace();
141+
}
142+
}
143+
144+
@Override
145+
public void onDelete(File file) {
146+
new AlertDialog.Builder(requireContext())
147+
.setTitle(R.string.delete_confirmation)
148+
.setMessage(file.getName())
149+
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
150+
if (file.delete()) {
151+
loadRecordings();
152+
} else {
153+
Toast.makeText(requireContext(), "Failed to delete", Toast.LENGTH_SHORT).show();
154+
}
155+
})
156+
.setNegativeButton(android.R.string.no, null)
157+
.show();
158+
}
159+
}

app/src/main/java/com/wmods/wppenhacer/xposed/core/devkit/Unobfuscator.java

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,15 +1543,31 @@ public synchronized static Method loadSendAudioTypeMethod(ClassLoader classLoade
15431543

15441544
public synchronized static Field loadOriginFMessageField(ClassLoader classLoader) throws Exception {
15451545
return UnobfuscatorCache.getInstance().getField(classLoader, () -> {
1546-
var result = dexkit.findMethod(new FindMethod().matcher(new MethodMatcher().addUsingString("audio/ogg; codecs=opus", StringMatchType.Contains)));
1546+
String[] commonStrings = new String[]{
1547+
"audio/ogg; codecs=opus",
1548+
"audio/ogg",
1549+
"audio/amr",
1550+
"audio/mp4",
1551+
"audio/aac"
1552+
};
1553+
15471554
var clazz = loadFMessageClass(classLoader);
1548-
if (result.isEmpty()) throw new RuntimeException("OriginFMessageField not found");
1549-
var fields = result.get(0).getUsingFields();
1550-
for (var field : fields) {
1551-
var f = field.getField().getFieldInstance(classLoader);
1552-
if (f.getDeclaringClass().equals(clazz)) {
1553-
return f;
1554-
}
1555+
1556+
for (String str : commonStrings) {
1557+
try {
1558+
var result = dexkit.findMethod(new FindMethod().matcher(new MethodMatcher().addUsingString(str, StringMatchType.Contains)));
1559+
if (result.isEmpty()) continue;
1560+
1561+
for (var m : result) {
1562+
var fields = m.getUsingFields();
1563+
for (var field : fields) {
1564+
var f = field.getField().getFieldInstance(classLoader);
1565+
if (f.getDeclaringClass().equals(clazz)) {
1566+
return f;
1567+
}
1568+
}
1569+
}
1570+
} catch (Exception ignored) {}
15551571
}
15561572
throw new RuntimeException("OriginFMessageField not found");
15571573
});

app/src/main/java/com/wmods/wppenhacer/xposed/features/general/Others.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,11 @@ public void doHook() throws Exception {
216216
}
217217

218218
if (audio_type > 0) {
219-
sendAudioType(audio_type);
219+
try {
220+
sendAudioType(audio_type);
221+
} catch (Exception e) {
222+
logDebug(e);
223+
}
220224
}
221225

222226
customPlayBackSpeed();

app/src/main/java/com/wmods/wppenhacer/xposed/features/media/CallRecording.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,18 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable {
102102
private synchronized void startRecording() {
103103
if (isRecording) return;
104104
try {
105-
File dir = new File(outputDir);
105+
String packageName = de.robv.android.xposed.AndroidAppHelper.currentPackageName();
106+
String appName = packageName.contains("w4b") ? "WA Business" : "WhatsApp";
107+
108+
File parentDir;
109+
if (Environment.isExternalStorageManager()) {
110+
parentDir = new File(Environment.getExternalStorageDirectory(), "WA Call Recordings");
111+
} else {
112+
parentDir = new File(outputDir);
113+
}
114+
115+
// Subfolders: Package Name -> Audio (Default, since type detection is complex here)
116+
File dir = new File(parentDir, appName + "/Audio");
106117
if (!dir.exists()) dir.mkdirs();
107118

108119
String fileName = "Call_" + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()) + ".wav";
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24"
6+
android:tint="?attr/colorControlNormal">
7+
<path
8+
android:fillColor="@android:color/white"
9+
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6zM12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4z"/>
10+
</vector>

0 commit comments

Comments
 (0)