Skip to content

Commit f64b2c2

Browse files
authored
Merge pull request #570 from mubashardev/pr/release-v1
fix: Call recording filter, status sharing FileProvider, anti-revoke group chats, and DB downgrade handling
2 parents 1b9d601 + 92350b3 commit f64b2c2

File tree

6 files changed

+366
-135
lines changed

6 files changed

+366
-135
lines changed

app/src/main/java/com/wmods/wppenhacer/xposed/core/db/DelMessageStore.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,22 @@ public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVers
8080
}
8181
}
8282

83+
@Override
84+
public void onDowngrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
85+
// Called when a lower-versioned fork (e.g. original Dev4Mod using version 5)
86+
// opens
87+
// a database previously created by this fork (version 10).
88+
// SQLite does not support schema downgrade natively, so we drop all tables and
89+
// recreate the base schema. This clears the deleted-message history stored by
90+
// this fork, but WhatsApp's own data is in a separate database and is
91+
// unaffected.
92+
android.util.Log.w("WaEnhancer",
93+
"delmessages.db downgrade from " + oldVersion + " to " + newVersion + ". Recreating schema.");
94+
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + TABLE_DELETED_FOR_ME);
95+
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS delmessages");
96+
onCreate(sqLiteDatabase);
97+
}
98+
8399
private void createDeletedForMeTable(SQLiteDatabase db) {
84100
db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_DELETED_FOR_ME + " (" +
85101
"_id INTEGER PRIMARY KEY AUTOINCREMENT, " +

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

Lines changed: 65 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -37,34 +37,39 @@
3737
public class AntiRevoke extends Feature {
3838

3939
private static final ConcurrentHashMap<String, Set<String>> messageRevokedMap = new ConcurrentHashMap<>();
40-
private static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = ThreadLocal.withInitial(() ->
41-
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Utils.getApplication().getResources().getConfiguration().getLocales().get(0)));
40+
private static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = ThreadLocal
41+
.withInitial(() -> DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT,
42+
Utils.getApplication().getResources().getConfiguration().getLocales().get(0)));
4243

4344
public AntiRevoke(ClassLoader loader, XSharedPreferences preferences) {
4445
super(loader, preferences);
4546
}
4647

4748
@Nullable
4849
private static Object findObjectFMessage(XC_MethodHook.MethodHookParam param) throws IllegalAccessException {
49-
if (param.args == null || param.args.length == 0) return null;
50+
if (param.args == null || param.args.length == 0)
51+
return null;
5052

5153
if (FMessageWpp.TYPE.isInstance(param.args[0]))
5254
return param.args[0];
5355

5456
if (param.args.length > 1) {
5557
if (FMessageWpp.TYPE.isInstance(param.args[1]))
5658
return param.args[1];
57-
var FMessageField = ReflectionUtils.findFieldUsingFilterIfExists(param.args[1].getClass(), f -> FMessageWpp.TYPE.isAssignableFrom(f.getType()));
59+
var FMessageField = ReflectionUtils.findFieldUsingFilterIfExists(param.args[1].getClass(),
60+
f -> FMessageWpp.TYPE.isAssignableFrom(f.getType()));
5861
if (FMessageField != null) {
5962
return FMessageField.get(param.args[1]);
6063
}
6164
}
6265

63-
var field = ReflectionUtils.findFieldUsingFilterIfExists(param.args[0].getClass(), f -> f.getType() == FMessageWpp.TYPE);
66+
var field = ReflectionUtils.findFieldUsingFilterIfExists(param.args[0].getClass(),
67+
f -> f.getType() == FMessageWpp.TYPE);
6468
if (field != null)
6569
return field.get(param.args[0]);
6670

67-
var field1 = ReflectionUtils.findFieldUsingFilter(param.args[0].getClass(), f -> f.getType() == FMessageWpp.Key.TYPE);
71+
var field1 = ReflectionUtils.findFieldUsingFilter(param.args[0].getClass(),
72+
f -> f.getType() == FMessageWpp.Key.TYPE);
6873
if (field1 != null) {
6974
var key = field1.get(param.args[0]);
7075
return WppCore.getFMessageFromKey(key);
@@ -78,15 +83,18 @@ private static void persistRevokedMessage(FMessageWpp fMessage) {
7883
var stripJID = fMessage.getKey().remoteJid.getPhoneNumber();
7984
Set<String> messages = getRevokedMessagesForJid(fMessage);
8085
messages.add(messageKey);
81-
DelMessageStore.getInstance(Utils.getApplication()).insertMessage(stripJID, messageKey, System.currentTimeMillis());
86+
DelMessageStore.getInstance(Utils.getApplication()).insertMessage(stripJID, messageKey,
87+
System.currentTimeMillis());
8288
}
8389

8490
private static Set<String> getRevokedMessagesForJid(FMessageWpp fMessage) {
8591
String stripJID = fMessage.getKey().remoteJid.getPhoneNumber();
86-
if (stripJID == null) return Collections.synchronizedSet(new java.util.HashSet<>());
92+
if (stripJID == null)
93+
return Collections.synchronizedSet(new java.util.HashSet<>());
8794
return messageRevokedMap.computeIfAbsent(stripJID, k -> {
8895
var messages = DelMessageStore.getInstance(Utils.getApplication()).getMessagesByJid(k);
89-
if (messages == null) return Collections.synchronizedSet(new java.util.HashSet<>());
96+
if (messages == null)
97+
return Collections.synchronizedSet(new java.util.HashSet<>());
9098
return Collections.synchronizedSet(messages);
9199
});
92100
}
@@ -100,13 +108,14 @@ public void doHook() throws Exception {
100108
var unknownStatusPlaybackMethod = Unobfuscator.loadUnknownStatusPlaybackMethod(classLoader);
101109
logDebug(Unobfuscator.getMethodDescriptor(unknownStatusPlaybackMethod));
102110

103-
var statusPlaybackClass = Unobfuscator.loadStatusPlaybackViewClass(classLoader);
111+
Class<?> statusPlaybackClass = Unobfuscator.loadStatusPlaybackViewClass(classLoader);
104112
logDebug(statusPlaybackClass);
105113

106114
XposedBridge.hookMethod(antiRevokeMessageMethod, new XC_MethodHook() {
107115
@Override
108116
protected void beforeHookedMethod(MethodHookParam param) throws Exception {
109-
if (param.args == null || param.args.length == 0 || param.args[0] == null) return;
117+
if (param.args == null || param.args.length == 0 || param.args[0] == null)
118+
return;
110119

111120
var fMessage = new FMessageWpp(param.args[0]);
112121
var messageKey = fMessage.getKey();
@@ -116,14 +125,20 @@ protected void beforeHookedMethod(MethodHookParam param) throws Exception {
116125
if (WppCore.getPrivBoolean(messageID + "_delpass", false)) {
117126
WppCore.removePrivKey(messageID + "_delpass");
118127
var activity = WppCore.getCurrentActivity();
119-
Class<?> StatusPlaybackActivityClass = classLoader.loadClass("com.whatsapp.status.playback.StatusPlaybackActivity");
128+
Class<?> StatusPlaybackActivityClass = classLoader
129+
.loadClass("com.whatsapp.status.playback.StatusPlaybackActivity");
120130
if (activity != null && StatusPlaybackActivityClass.isInstance(activity)) {
121131
activity.finish();
122132
}
123133
return;
124134
}
135+
// For group messages: intercept any non-self revocation regardless of
136+
// deviceJid.
137+
// Previously the deviceJid != null guard caused all group revocations where
138+
// deviceJid is null (the common case for other participants) to be silently
139+
// skipped.
125140
if (messageKey.remoteJid.isGroup()) {
126-
if (deviceJid != null && handleRevocationAttempt(fMessage) != 0) {
141+
if (!messageKey.isFromMe && handleRevocationAttempt(fMessage) != 0) {
127142
param.setResult(true);
128143
}
129144
} else if (!messageKey.isFromMe && handleRevocationAttempt(fMessage) != 0) {
@@ -132,11 +147,11 @@ protected void beforeHookedMethod(MethodHookParam param) throws Exception {
132147
}
133148
});
134149

135-
136150
ConversationItemListener.conversationListeners.add(new ConversationItemListener.OnConversationItemListener() {
137151
@Override
138152
public void onItemBind(FMessageWpp fMessage, ViewGroup viewGroup) {
139-
if (fMessage.getKey().isFromMe) return;
153+
if (fMessage.getKey().isFromMe)
154+
return;
140155
var dateTextView = (TextView) viewGroup.findViewById(Utils.getID("date", "id"));
141156
bindRevokedMessageUI(fMessage, dateTextView, "antirevoke");
142157
}
@@ -149,10 +164,12 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable {
149164
var objFMessage = findObjectFMessage(param);
150165
var field = ReflectionUtils.getFieldByType(param.method.getDeclaringClass(), statusPlaybackClass);
151166

152-
if (obj == null || field == null || objFMessage == null) return;
167+
if (obj == null || field == null || objFMessage == null)
168+
return;
153169

154170
Object objView = field.get(obj);
155-
if (objView == null) return;
171+
if (objView == null)
172+
return;
156173

157174
var textViews = ReflectionUtils.getFieldsByType(statusPlaybackClass, TextView.class);
158175
if (textViews.isEmpty()) {
@@ -173,22 +190,29 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable {
173190
}
174191

175192
private void bindRevokedMessageUI(FMessageWpp fMessage, TextView dateTextView, String antirevokeType) {
176-
if (dateTextView == null) return;
193+
if (dateTextView == null)
194+
return;
177195

178196
var key = fMessage.getKey();
179197
var messageRevokedList = getRevokedMessagesForJid(fMessage);
180198
var id = fMessage.getRowId();
181199
String keyOrig = null;
182-
if (messageRevokedList.contains(key.messageID) || ((keyOrig = MessageStore.getInstance().getOriginalMessageKey(id)) != null && messageRevokedList.contains(keyOrig))) {
183-
var timestamp = DelMessageStore.getInstance(Utils.getApplication()).getTimestampByMessageId(keyOrig == null ? key.messageID : keyOrig);
200+
if (messageRevokedList.contains(key.messageID)
201+
|| ((keyOrig = MessageStore.getInstance().getOriginalMessageKey(id)) != null
202+
&& messageRevokedList.contains(keyOrig))) {
203+
var timestamp = DelMessageStore.getInstance(Utils.getApplication())
204+
.getTimestampByMessageId(keyOrig == null ? key.messageID : keyOrig);
184205
if (timestamp > 0) {
185206
var date = Objects.requireNonNull(DATE_FORMAT_THREAD_LOCAL.get()).format(new Date(timestamp));
186207
dateTextView.getPaint().setUnderlineText(true);
187-
dateTextView.setOnClickListener(v -> Utils.showToast(String.format(Utils.getApplication().getString(ResId.string.message_removed_on), date), Toast.LENGTH_LONG));
208+
dateTextView.setOnClickListener(v -> Utils.showToast(
209+
String.format(Utils.getApplication().getString(ResId.string.message_removed_on), date),
210+
Toast.LENGTH_LONG));
188211
}
189212
var antirevokeValue = Integer.parseInt(prefs.getString(antirevokeType, "0"));
190213
if (antirevokeValue == 1) {
191-
var newTextData = UnobfuscatorCache.getInstance().getString("messagedeleted") + " | " + dateTextView.getText();
214+
var newTextData = UnobfuscatorCache.getInstance().getString("messagedeleted") + " | "
215+
+ dateTextView.getText();
192216
dateTextView.setText(newTextData);
193217
} else if (antirevokeValue == 2) {
194218
var drawable = Utils.getApplication().getDrawable(ResId.drawable.deleted);
@@ -207,7 +231,6 @@ private void bindRevokedMessageUI(FMessageWpp fMessage, TextView dateTextView, S
207231
}
208232
}
209233

210-
211234
private int handleRevocationAttempt(FMessageWpp fMessage) {
212235
try {
213236
showRevocationToast(fMessage);
@@ -216,21 +239,25 @@ private int handleRevocationAttempt(FMessageWpp fMessage) {
216239
}
217240
String messageKey = (String) XposedHelpers.getObjectField(fMessage.getObject(), "A01");
218241
String stripJID = fMessage.getKey().remoteJid.getPhoneNumber();
219-
int revokeboolean = stripJID.equals("status") ? Integer.parseInt(prefs.getString("antirevokestatus", "0")) : Integer.parseInt(prefs.getString("antirevoke", "0"));
220-
if (revokeboolean == 0) return revokeboolean;
242+
int revokeboolean = stripJID.equals("status") ? Integer.parseInt(prefs.getString("antirevokestatus", "0"))
243+
: Integer.parseInt(prefs.getString("antirevoke", "0"));
244+
if (revokeboolean == 0)
245+
return revokeboolean;
221246
var messageRevokedList = getRevokedMessagesForJid(fMessage);
222247
if (!messageRevokedList.contains(messageKey)) {
223248
try {
224249
CompletableFuture.runAsync(() -> {
225250
persistRevokedMessage(fMessage);
226251
try {
227252
var mConversation = WppCore.getCurrentConversation();
228-
if (mConversation != null && Objects.equals(stripJID, WppCore.getCurrentUserJid().getPhoneNumber())) {
253+
if (mConversation != null
254+
&& Objects.equals(stripJID, WppCore.getCurrentUserJid().getPhoneNumber())) {
229255
mConversation.runOnUiThread(() -> {
230256
if (mConversation.hasWindowFocus()) {
231257
mConversation.startActivity(mConversation.getIntent());
232258
mConversation.overridePendingTransition(0, 0);
233-
mConversation.getWindow().getDecorView().findViewById(android.R.id.content).postInvalidate();
259+
mConversation.getWindow().getDecorView().findViewById(android.R.id.content)
260+
.postInvalidate();
234261
} else {
235262
mConversation.recreate();
236263
}
@@ -254,13 +281,20 @@ private void showRevocationToast(FMessageWpp fMessage) {
254281
messageSuffix = Utils.getApplication().getString(ResId.string.deleted_status);
255282
jidAuthor = fMessage.getUserJid();
256283
}
257-
if (jidAuthor.userJid == null) return;
284+
if (jidAuthor.userJid == null)
285+
return;
258286
String name = WppCore.getContactName(jidAuthor);
259287
if (TextUtils.isEmpty(name)) {
260288
name = jidAuthor.getPhoneNumber();
261289
}
262290
String message;
263-
if (jidAuthor.isGroup() && fMessage.getUserJid().isNull()) {
291+
// Show "Participant deleted a message in GroupName" for group messages where we
292+
// know
293+
// who the sender is (!isNull). The old condition was inverted — it showed this
294+
// only
295+
// when getUserJid().isNull() which is exactly when we do NOT know the
296+
// participant.
297+
if (jidAuthor.isGroup() && !fMessage.getUserJid().isNull()) {
264298
var participantJid = fMessage.getUserJid();
265299
String participantName = WppCore.getContactName(participantJid);
266300
if (TextUtils.isEmpty(participantName)) {
@@ -273,7 +307,8 @@ private void showRevocationToast(FMessageWpp fMessage) {
273307
if (prefs.getBoolean("toastdeleted", false)) {
274308
Utils.showToast(message, Toast.LENGTH_LONG);
275309
}
276-
Tasker.sendTaskerEvent(name, jidAuthor.getPhoneNumber(), jidAuthor.isStatus() ? "deleted_status" : "deleted_message");
310+
Tasker.sendTaskerEvent(name, jidAuthor.getPhoneNumber(),
311+
jidAuthor.isStatus() ? "deleted_status" : "deleted_message");
277312
}
278313

279314
@NonNull

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

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,22 @@ private void saveOne(Context context, Object msg) throws Exception {
118118
jidObj = getObj(key, "chatJid");
119119
if (jidObj == null)
120120
jidObj = getObj(key, "A00");
121-
if (jidObj != null)
122-
chatJid = jidObj.toString();
121+
if (jidObj != null) {
122+
// Prefer getRawString() — WhatsApp JID object toString() may return display
123+
// name,
124+
// not the raw 'number@domain' form needed for the group filter query.
125+
try {
126+
Object rawStr = jidObj.getClass().getMethod("getRawString").invoke(jidObj);
127+
if (rawStr instanceof String && !((String) rawStr).isEmpty()) {
128+
chatJid = (String) rawStr;
129+
}
130+
} catch (Throwable ignored) {
131+
}
132+
// Fallback to toString() if getRawString() unavailable
133+
if (chatJid == null) {
134+
chatJid = jidObj.toString();
135+
}
136+
}
123137
if (chatJid != null && (chatJid.equalsIgnoreCase("false") || chatJid.equalsIgnoreCase("true"))) {
124138
chatJid = null;
125139
}
@@ -197,18 +211,36 @@ private void saveOne(Context context, Object msg) throws Exception {
197211
}
198212

199213
// 7. Sender JID
200-
String senderJid = fromMe ? "Me" : chatJid;
214+
// For personal chats: senderJid = "Me" (if fromMe) or the contact's chatJid.
215+
// For group chats: senderJid = the individual participant JID (not the group
216+
// JID).
217+
String senderJid = fromMe ? "Me" : null;
201218
Object participant = getObj(msg, "participant");
202219
if (participant == null)
203220
participant = getObj(msg, "senderJid");
204221
if (participant == null)
205222
participant = getObj(msg, "A0b");
206223
if (participant != null) {
207-
String val = participant.toString();
208-
if (!val.equalsIgnoreCase("false") && !val.equalsIgnoreCase("true")) {
224+
String val = null;
225+
// Try getRawString() first to get 'number@s.whatsapp.net'
226+
try {
227+
Object rawStr = participant.getClass().getMethod("getRawString").invoke(participant);
228+
if (rawStr instanceof String && !((String) rawStr).isEmpty()) {
229+
val = (String) rawStr;
230+
}
231+
} catch (Throwable ignored) {
232+
}
233+
if (val == null)
234+
val = participant.toString();
235+
if (!val.equalsIgnoreCase("false") && !val.equalsIgnoreCase("true") && !val.isEmpty()) {
209236
senderJid = val;
210237
}
211238
}
239+
// Final fallback: use chatJid only for non-group personal chats
240+
if (senderJid == null) {
241+
boolean isGroupChat = chatJid != null && chatJid.endsWith("@g.us");
242+
senderJid = isGroupChat ? "Unknown" : chatJid;
243+
}
212244

213245
// 8. Media Details
214246
String mediaPath = null;

0 commit comments

Comments
 (0)