Skip to content

Commit baf3df9

Browse files
committed
feat: Enhance deleted message recovery with improved text extraction, robust contact lookup, and a dedicated UI.
1 parent 8f34f41 commit baf3df9

9 files changed

Lines changed: 343 additions & 153 deletions

File tree

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

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class DeletedMessagesAdapter extends RecyclerView.Adapter<DeletedMessages
2525

2626
public interface OnItemClickListener {
2727
void onItemClick(DeletedMessage message);
28+
2829
void onRestoreClick(DeletedMessage message);
2930
}
3031

@@ -51,13 +52,13 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
5152
// Contact Name
5253
Context context = holder.itemView.getContext();
5354
String displayJid = message.getChatJid();
54-
55+
5556
// Priority 1: Use Persisted Contact Name (from DB)
5657
String contactName = message.getContactName();
57-
58+
5859
// Priority 2: Runtime Lookup (if not in DB or changed)
5960
if (contactName == null && displayJid != null) {
60-
contactName = com.wmods.wppenhacer.utils.ContactHelper.getContactName(context, displayJid);
61+
contactName = com.wmods.wppenhacer.utils.ContactHelper.getContactName(context, displayJid);
6162
}
6263

6364
String displayText;
@@ -68,12 +69,13 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
6869
displayText = displayJid;
6970
if (displayText != null) {
7071
displayText = displayText.replace("@s.whatsapp.net", "").replace("@g.us", "");
71-
if (displayText.contains("@")) displayText = displayText.split("@")[0];
72+
if (displayText.contains("@"))
73+
displayText = displayText.split("@")[0];
7274
} else {
7375
displayText = "Unknown";
7476
}
7577
}
76-
78+
7779
holder.contactName.setText(displayText);
7880

7981
// Timestamp
@@ -85,21 +87,76 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
8587
if (message.isFromMe()) {
8688
senderPrefix = "You: ";
8789
}
88-
90+
91+
// Sanitize text for media messages (Fix for weird strings/URLs)
92+
// If it's a media message, ignore text if it looks like a URL or Hash
93+
if (message.getMediaType() > 0 && text != null) {
94+
if (text.startsWith("http") || (text.length() > 20 && !text.contains(" "))) {
95+
text = null;
96+
}
97+
}
98+
8999
if (text == null || text.isEmpty()) {
90-
if (message.getMediaType() != -1) {
91-
text = "📷 Photo";
92-
if (message.getMediaType() == 2) text = "🔊 Audio";
93-
if (message.getMediaType() == 3) text = "🎥 Video";
100+
int type = message.getMediaType();
101+
if (type != -1 && type != 0) {
102+
switch (type) {
103+
case 1:
104+
text = "📷 Photo";
105+
break;
106+
case 2:
107+
text = "🔊 Audio";
108+
break;
109+
case 3:
110+
text = "🎥 Video";
111+
break;
112+
case 4:
113+
text = "👤 Contact";
114+
break;
115+
case 5:
116+
text = "📍 Location";
117+
break;
118+
case 9:
119+
text = "📄 Document";
120+
break;
121+
case 13:
122+
text = "👾 GIF";
123+
break;
124+
case 20:
125+
text = "💟 Sticker";
126+
break;
127+
case 42:
128+
text = "🔄 Status Reply";
129+
break; // Sometimes seen
130+
default:
131+
text = "📁 Media";
132+
break;
133+
}
94134
} else {
95-
text = "Message deleted";
135+
text = "🚫 Message deleted";
96136
}
97137
}
98138
holder.lastMessage.setText(senderPrefix + text);
99139

100140
// Avatar (Placeholder)
101141
// Holder.avatar.setImageDrawable(...)
102-
142+
143+
// App Badge
144+
String pkg = message.getPackageName();
145+
if (pkg != null) {
146+
holder.appBadge.setVisibility(View.VISIBLE);
147+
if (pkg.equals("com.whatsapp.w4b")) {
148+
// WhatsApp Business - Teal
149+
holder.appBadge.setImageTintList(
150+
android.content.res.ColorStateList.valueOf(android.graphics.Color.parseColor("#00BFA5")));
151+
} else {
152+
// WhatsApp - Green
153+
holder.appBadge.setImageTintList(
154+
android.content.res.ColorStateList.valueOf(android.graphics.Color.parseColor("#25D366")));
155+
}
156+
} else {
157+
holder.appBadge.setVisibility(View.GONE);
158+
}
159+
103160
holder.itemView.setOnClickListener(v -> listener.onItemClick(message));
104161
}
105162

@@ -110,13 +167,15 @@ public int getItemCount() {
110167

111168
static class ViewHolder extends RecyclerView.ViewHolder {
112169
ImageView avatar;
170+
ImageView appBadge;
113171
TextView contactName;
114172
TextView timestamp; // Date
115173
TextView lastMessage;
116174

117175
ViewHolder(View itemView) {
118176
super(itemView);
119177
avatar = itemView.findViewById(R.id.avatar);
178+
appBadge = itemView.findViewById(R.id.app_badge);
120179
contactName = itemView.findViewById(R.id.contact_name);
121180
timestamp = itemView.findViewById(R.id.timestamp);
122181
lastMessage = itemView.findViewById(R.id.last_message);

app/src/main/java/com/wmods/wppenhacer/ui/fragments/DeletedMessagesFragment.java

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -38,82 +38,97 @@ public static DeletedMessagesFragment newInstance(boolean isGroup) {
3838
return fragment;
3939
}
4040

41+
private int currentFilter = R.id.filter_all;
42+
4143
@Override
4244
public void onCreate(@Nullable Bundle savedInstanceState) {
4345
super.onCreate(savedInstanceState);
4446
if (getArguments() != null) {
4547
isGroup = getArguments().getBoolean("is_group");
4648
}
49+
setHasOptionsMenu(true);
4750
}
4851

4952
@Nullable
5053
@Override
51-
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
54+
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
55+
@Nullable Bundle savedInstanceState) {
5256
return inflater.inflate(R.layout.fragment_deleted_messages, container, false);
5357
}
5458

55-
private View permissionBanner;
56-
private android.widget.Button btnGrantPermission;
59+
@Override
60+
public void onCreateOptionsMenu(@NonNull android.view.Menu menu, @NonNull android.view.MenuInflater inflater) {
61+
inflater.inflate(R.menu.menu_deleted_messages, menu);
62+
super.onCreateOptionsMenu(menu, inflater);
63+
}
5764

58-
private static final int PERMISSION_REQUEST_CONTACTS = 1001;
65+
@Override
66+
public boolean onOptionsItemSelected(@NonNull android.view.MenuItem item) {
67+
if (item.getItemId() == R.id.filter_all || item.getItemId() == R.id.filter_whatsapp
68+
|| item.getItemId() == R.id.filter_whatsapp_business) {
69+
currentFilter = item.getItemId();
70+
item.setChecked(true);
71+
loadMessages();
72+
return true;
73+
}
74+
return super.onOptionsItemSelected(item);
75+
}
5976

6077
@Override
6178
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
6279
super.onViewCreated(view, savedInstanceState);
6380

6481
recyclerView = view.findViewById(R.id.recyclerView);
6582
emptyView = view.findViewById(R.id.empty_view);
66-
permissionBanner = view.findViewById(R.id.permission_banner);
67-
btnGrantPermission = view.findViewById(R.id.btn_grant_permission);
6883

6984
delMessageStore = DelMessageStore.getInstance(requireContext());
7085
adapter = new DeletedMessagesAdapter(this);
7186

7287
recyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
7388
recyclerView.setAdapter(adapter);
7489

75-
btnGrantPermission.setOnClickListener(v -> requestPermissions(new String[]{android.Manifest.permission.READ_CONTACTS}, PERMISSION_REQUEST_CONTACTS));
90+
// Check for updates to names if permission is already granted, but don't ask
91+
if (requireContext().checkSelfPermission(
92+
android.Manifest.permission.READ_CONTACTS) == android.content.pm.PackageManager.PERMISSION_GRANTED) {
93+
adapter.notifyDataSetChanged();
94+
}
7695

77-
checkPermissions();
7896
loadMessages();
7997
}
8098

8199
@Override
82100
public void onResume() {
83101
super.onResume();
84-
checkPermissions();
85-
}
86-
87-
private void checkPermissions() {
88-
if (requireContext().checkSelfPermission(android.Manifest.permission.READ_CONTACTS) == android.content.pm.PackageManager.PERMISSION_GRANTED) {
89-
permissionBanner.setVisibility(View.GONE);
90-
if (adapter != null) adapter.notifyDataSetChanged(); // Refresh names
91-
} else {
92-
permissionBanner.setVisibility(View.VISIBLE);
93-
}
94-
}
95-
96-
@Override
97-
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
98-
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
99-
if (requestCode == PERMISSION_REQUEST_CONTACTS) {
100-
if (grantResults.length > 0 && grantResults[0] == android.content.pm.PackageManager.PERMISSION_GRANTED) {
101-
checkPermissions();
102-
}
102+
if (requireContext().checkSelfPermission(
103+
android.Manifest.permission.READ_CONTACTS) == android.content.pm.PackageManager.PERMISSION_GRANTED) {
104+
if (adapter != null)
105+
adapter.notifyDataSetChanged();
103106
}
104107
}
105108

106109
private void loadMessages() {
107110
new Thread(() -> {
108-
List<DeletedMessage> allMessages = delMessageStore.getDeletedMessages(isGroup);
111+
List<DeletedMessage> allMessages = delMessageStore.getDeletedMessages(isGroup); // Fetch ALL first, then
112+
// filter
109113
Map<String, DeletedMessage> latestMessagesMap = new HashMap<>();
110114

111115
for (DeletedMessage msg : allMessages) {
116+
// Apps Filter Logic
117+
boolean matchesFilter = true;
118+
if (currentFilter == R.id.filter_whatsapp) {
119+
matchesFilter = "com.whatsapp".equals(msg.getPackageName());
120+
} else if (currentFilter == R.id.filter_whatsapp_business) {
121+
matchesFilter = "com.whatsapp.w4b".equals(msg.getPackageName());
122+
}
123+
124+
if (!matchesFilter)
125+
continue;
126+
112127
if (!latestMessagesMap.containsKey(msg.getChatJid())) {
113128
latestMessagesMap.put(msg.getChatJid(), msg);
114129
} else {
115130
if (msg.getTimestamp() > latestMessagesMap.get(msg.getChatJid()).getTimestamp()) {
116-
latestMessagesMap.put(msg.getChatJid(), msg);
131+
latestMessagesMap.put(msg.getChatJid(), msg);
117132
}
118133
}
119134
}
@@ -136,7 +151,8 @@ private void loadMessages() {
136151

137152
@Override
138153
public void onItemClick(DeletedMessage message) {
139-
android.content.Intent intent = new android.content.Intent(requireContext(), com.wmods.wppenhacer.activities.MessageListActivity.class);
154+
android.content.Intent intent = new android.content.Intent(requireContext(),
155+
com.wmods.wppenhacer.activities.MessageListActivity.class);
140156
intent.putExtra("chat_jid", message.getChatJid());
141157
startActivity(intent);
142158
}

app/src/main/java/com/wmods/wppenhacer/xposed/core/components/WaContactWpp.java

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,21 @@ public record WaContactWpp(Object mInstance) {
3131
private static Object mInstanceGetWaContact;
3232
private static Object mInstanceGetProfilePhoto;
3333

34-
3534
public WaContactWpp(Object mInstance) {
36-
if (TYPE == null) throw new RuntimeException("WaContactWpp not initialized");
37-
if (mInstance == null) throw new RuntimeException("object is null");
38-
if (!TYPE.isInstance(mInstance)) throw new RuntimeException("object is not a WaContactWpp");
35+
if (TYPE == null)
36+
throw new RuntimeException("WaContactWpp not initialized");
37+
if (mInstance == null)
38+
throw new RuntimeException("object is null");
39+
if (!TYPE.isInstance(mInstance))
40+
throw new RuntimeException("object is not a WaContactWpp");
3941
this.mInstance = TYPE.cast(mInstance);
4042
}
4143

4244
public static void initialize(ClassLoader classLoader) {
4345
try {
4446
TYPE = Unobfuscator.loadWaContactClass(classLoader);
45-
var classPhoneUserJid = Unobfuscator.findFirstClassUsingName(classLoader, StringMatchType.EndsWith, "jid.PhoneUserJid");
47+
var classPhoneUserJid = Unobfuscator.findFirstClassUsingName(classLoader, StringMatchType.EndsWith,
48+
"jid.PhoneUserJid");
4649
var classJid = Unobfuscator.findFirstClassUsingName(classLoader, StringMatchType.EndsWith, "jid.Jid");
4750

4851
var phoneUserJid = ReflectionUtils.getFieldByExtendType(TYPE, classPhoneUserJid);
@@ -57,12 +60,37 @@ public static void initialize(ClassLoader classLoader) {
5760
fieldGetWaName = Unobfuscator.loadWaContactGetWaNameField(classLoader);
5861

5962
getWaContactMethod = Unobfuscator.loadGetWaContactMethod(classLoader);
60-
XposedBridge.hookAllConstructors(getWaContactMethod.getDeclaringClass(), new XC_MethodHook() {
61-
@Override
62-
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
63-
mInstanceGetWaContact = param.thisObject;
63+
Class<?> contactManagerClass = getWaContactMethod.getDeclaringClass();
64+
65+
// Try to find existing instance via static method (getInstance pattern)
66+
for (Method m : contactManagerClass.getDeclaredMethods()) {
67+
if (java.lang.reflect.Modifier.isStatic(m.getModifiers())
68+
&& m.getReturnType() == contactManagerClass
69+
&& m.getParameterCount() == 0) {
70+
try {
71+
Object instance = m.invoke(null);
72+
if (instance != null) {
73+
mInstanceGetWaContact = instance;
74+
XposedBridge.log("WAE: WaContactWpp: Captured instance via static method: " + m.getName());
75+
break;
76+
}
77+
} catch (Exception ignored) {
78+
}
6479
}
65-
});
80+
}
81+
82+
if (mInstanceGetWaContact == null) {
83+
XposedBridge.hookAllConstructors(contactManagerClass, new XC_MethodHook() {
84+
@Override
85+
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
86+
mInstanceGetWaContact = param.thisObject;
87+
XposedBridge.log("WAE: WaContactWpp: Captured instance via constructor");
88+
}
89+
});
90+
} else {
91+
XposedBridge.log("WAE: WaContactWpp: Instance already captured, skipping constructor hook");
92+
}
93+
6694
getProfilePhoto = Unobfuscator.loadGetProfilePhotoMethod(classLoader);
6795
getProfilePhotoHighQuality = Unobfuscator.loadGetProfilePhotoHighQMethod(classLoader);
6896

@@ -83,7 +111,6 @@ public Object getObject() {
83111
return mInstance;
84112
}
85113

86-
87114
public FMessageWpp.UserJid getUserJid() {
88115
try {
89116
if (fieldContactData != null) {
@@ -117,8 +144,27 @@ public String getWaName() {
117144
}
118145

119146
public static WaContactWpp getWaContactFromJid(FMessageWpp.UserJid userJid) {
147+
if (mInstanceGetWaContact == null) {
148+
XposedBridge.log("WAE: WaContactWpp: mInstanceGetWaContact is NULL. ContactManager not initialized?");
149+
return null;
150+
}
120151
try {
121-
return new WaContactWpp(getWaContactMethod.invoke(mInstanceGetWaContact, userJid.userJid));
152+
Object contact = null;
153+
if (userJid.userJid != null) {
154+
contact = getWaContactMethod.invoke(mInstanceGetWaContact, userJid.userJid);
155+
}
156+
157+
// Fallback to phoneJid if userJid lookup failed or userJid was null
158+
if (contact == null && userJid.phoneJid != null) {
159+
XposedBridge.log("WAE: WaContactWpp: userJid lookup failed, trying phoneJid");
160+
contact = getWaContactMethod.invoke(mInstanceGetWaContact, userJid.phoneJid);
161+
}
162+
163+
if (contact != null) {
164+
return new WaContactWpp(contact);
165+
} else {
166+
XposedBridge.log("WAE: WaContactWpp: Contact lookup returned null for " + userJid);
167+
}
122168
} catch (Exception e) {
123169
XposedBridge.log(e);
124170
}

0 commit comments

Comments
 (0)