Skip to content

Commit

Permalink
fix : Clean Recent Chat for QQNT
Browse files Browse the repository at this point in the history
  • Loading branch information
suzhelan committed Dec 18, 2023
1 parent 84313de commit d391acc
Show file tree
Hide file tree
Showing 4 changed files with 469 additions and 32 deletions.
259 changes: 259 additions & 0 deletions app/src/main/java/top/linl/hook/FixCleanRecentChat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
/*
* QAuxiliary - An Xposed module for QQ/TIM
* Copyright (C) 2019-2023 QAuxiliary developers
* https://github.com/cinit/QAuxiliary
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version and our eula as published
* by QAuxiliary contributors.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>.
*/

package top.linl.hook;

import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import cc.ioctl.util.HookUtils;
import io.github.qauxv.util.Toasts;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import top.linl.util.reflect.ClassUtils;
import top.linl.util.reflect.FieIdUtils;
import top.linl.util.reflect.MethodTool;
import xyz.nextalone.hook.CleanRecentChat;

/**
* @author suzhelan
* @CreateDate 2023.12.18
*/
public class FixCleanRecentChat {

private static final HashMap<Object, Integer> viewHolderList = new LinkedHashMap<>();
private static int deleteTextViewId;
private final CleanRecentChat cleanRecentChat;

public FixCleanRecentChat(CleanRecentChat cleanRecentChat) {
this.cleanRecentChat = cleanRecentChat;
}

private void hookGetDeleteViewId() {
Class<?> superClass = ClassUtils.getClass("com.tencent.qqnt.chats.biz.guild.GuildDiscoveryItemBuilder").getSuperclass();
Class<?> findClass = null;
for (Field field : superClass.getDeclaredFields()) {
field.setAccessible(true);
Class<?> type = field.getType();
if (type.getName().startsWith("com.tencent.qqnt.chats.core.adapter.")) {
findClass = type;
break;
}
}
Method method = MethodTool.find(findClass).params(
android.view.ViewGroup.class,
java.util.List.class
).returnType(List.class)
.get();
HookUtils.hookAfterIfEnabled(cleanRecentChat, method, param -> {
if (deleteTextViewId != 0) {
return;
}
List<View> viewList = (List<View>) param.getResult();
for (View view : viewList) {
if (view instanceof TextView) {
TextView textView = (TextView) view;
if (textView.getText().toString().equals("删除")) {
deleteTextViewId = textView.getId();
break;
}
}
}
});

}

public void loadHook() throws Exception {
hookGetDeleteViewId();
hookOnHolder();

//不hook onCreate方法了 那样需要重启才能生效 hook onResume可在界面重新渲染到屏幕时会调用生效
Method onCreateMethod = MethodTool.find("com.tencent.mobileqq.activity.home.Conversation").name("onResume").params(boolean.class).get();
HookUtils.hookAfterIfEnabled(cleanRecentChat, onCreateMethod, param -> {
ImageView imageView = FieIdUtils.getFirstField(param.thisObject, ImageView.class);
imageView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
new Thread(new DeleteAllItemTask()).start();
return true;
}
});
});

}

private void hookOnHolder() {
//find
Class<?> recentContactItemHolderClass = ClassUtils.getClass("com.tencent.qqnt.chats.core.adapter.holder.RecentContactItemHolder");
Method onHolderBindTimeingCallSetOnClickMethod = null;
for (Method method : recentContactItemHolderClass.getDeclaredMethods()) {
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length == 3) {
if (paramTypes[0].getName().startsWith("com.tencent.qqnt.chats.core.adapter.builder.")
&& paramTypes[1].getName().startsWith("com.tencent.qqnt.chats.core.adapter.")
&& paramTypes[2] == int.class) {
method.setAccessible(true);
onHolderBindTimeingCallSetOnClickMethod = method;
break;
}
}
}
HookUtils.hookBeforeIfEnabled(cleanRecentChat, onHolderBindTimeingCallSetOnClickMethod, param -> {
int adapterIndex = (int) param.args[2];
Object item = param.args[1];
//Holder在前 索引在后 因为Holder在复用池中所以引用地址不会变 但是索引在Adapter中是随时变化的
viewHolderList.put(param.thisObject, adapterIndex);
});

}

private static class DeleteAllItemTask implements Runnable {

private static final AtomicReference<Method> deleteMethod = new AtomicReference<>();
private static Class<?> utilType;
private static Field itemField;

private Object findItemField(Object viewHolder) throws IllegalAccessException {
if (itemField != null) {
return itemField.get(viewHolder);
}
for (Field field : viewHolder.getClass().getDeclaredFields()) {
try {
field.setAccessible(true);
Object fieldObj = field.get(viewHolder);
if (fieldObj == null) {
continue;
}
String toStr = fieldObj.toString();
if (toStr.contains("RecentContactChatItem")) {
field.setAccessible(true);
itemField = field;
break;
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
return itemField.get(viewHolder);
}

private Class<?> findUtilClassType(Object viewHolder) {
if (utilType != null) {
return utilType;
}
for (Field field : viewHolder.getClass().getDeclaredFields()) {
try {
field.setAccessible(true);
Object fieldObj = field.get(viewHolder);
if (fieldObj == null) {
continue;
}
if (fieldObj.getClass().getName().startsWith("com.tencent.qqnt.chats.core.ui.ChatsListVB$")) {
utilType = field.getType();
break;
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
return utilType;
}

private Method getDeleteMethod(Object viewHolder) {
if (deleteMethod.get() != null) {
return deleteMethod.get();
}
Class<?> findClass = findUtilClassType(viewHolder);
if (findClass == null) {
throw new RuntimeException("findClass is null");
}
Method finalDeleteMethod = MethodTool.find(findClass).params(int.class,//index ?
Object.class,//item
ClassUtils.getClass("com.tencent.qqnt.chats.core.adapter.holder.RecentContactItemBinding"),//view binder
int.class//click view id
).returnType(void.class)
.get();
deleteMethod.set(finalDeleteMethod);
return deleteMethod.get();
}

@Override
public void run() {
final AtomicBoolean isStop = new AtomicBoolean(false);
TimerTask task = new TimerTask() {
@Override
public void run() {
isStop.set(true);
}
};
//在2秒内尽量删除
Timer timer = new Timer();
timer.schedule(task, 2000);

Toasts.show("开始清理");
int deleteQuantity = 0;
while (!isStop.get()) {
int size = viewHolderList.size();
if (size == 0) {
try {
//停一下等待ItemHolder重新bind到屏幕上 然后继续删除
TimeUnit.MILLISECONDS.sleep(100);
continue;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
Iterator<Map.Entry<Object, Integer>> iterator = viewHolderList.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Object, Integer> viewHolderEntry = iterator.next();
try {
Object recentContactItemHolder = viewHolderEntry.getKey();
//delete util
Object util = FieIdUtils.getFirstField(recentContactItemHolder, findUtilClassType(recentContactItemHolder));//util run time obj
int adapterIndex = viewHolderEntry.getValue();//call param 1
Object itemInfo = findItemField(recentContactItemHolder);//call param 2
Object itemBinder = FieIdUtils.getFirstField(recentContactItemHolder,
ClassUtils.getClass("com.tencent.qqnt.chats.core.adapter.holder.RecentContactItemBinding"));//call param 3
int viewId = deleteTextViewId;//call param 4
getDeleteMethod(recentContactItemHolder).invoke(util, adapterIndex, itemInfo, itemBinder, viewId);
deleteQuantity++;
} catch (Exception e) {
throw new RuntimeException(e);
}
iterator.remove();
}
}
Toasts.show("已清理结束 数量" + deleteQuantity + "个");
}
}

}
Loading

0 comments on commit d391acc

Please sign in to comment.