-
Notifications
You must be signed in to change notification settings - Fork 335
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
suzhelan
committed
Dec 18, 2023
1 parent
84313de
commit d391acc
Showing
4 changed files
with
469 additions
and
32 deletions.
There are no files selected for viewing
259 changes: 259 additions & 0 deletions
259
app/src/main/java/top/linl/hook/FixCleanRecentChat.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 + "个"); | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.