转载注明出处:简书-十个雨点
简介
在使用辅助服务实现全局复制中,我介绍了通过辅助服务实现全局复制的功能,极大的提高了复制功能的使用范围,补充了通过点击获取文字的不足。
在如何通过Xposed框架获取点击的文字中,介绍了如何基于Xposed框架实现点击取词功能的,以及相对于辅助服务实现的优势。既然要摆脱辅助服务的限制,当然要把全局复制也用Xposed的方式实现了才行吧!
先看看效果
也可以下载全能分词体验
如果你觉得跟基于辅助服务实现的基本上没有区别,那就对了,因为压根就是一张图。。。。
Xposed 是什么?如何使用
关于Xposed框架如何使用的问题就不再赘述了,感兴趣的同学可以自行百度,或者参考这篇——如何通过Xposed框架获取点击的文字。
如何实现全局复制
有两个关键点需要先考虑清楚:
- 使用辅助服务实现全局复制是通过遍历AccessibilityNodeInfo来获得当前界面的布局,并获取页面中的文字。所以很自然就可以想到,通过Xposed,可以直接遍历View树,从而拿到当前界面的布局和文字。
- 全局复制通过通知栏或者悬浮窗触发,在触发以后,需要在当前Activity进行遍历,而不能被其他后台的Activity影响。从描述中就可以联想到Activity的生命周期:只要在onStart里注册一个BroadcastReceiver,用于接受触发全局复制的命令,然后在onStop里注销。
明确这两点以后就可以写代码了:
首先是注入Activity的onStart和onStop方法
public class XposedBigBang implements IXposedHookLoadPackage {
private static final String TAG = "XposedBigBang";
private final XposedUniversalCopyHandler mUniversalCopyHandler = new XposedUniversalCopyHandler();
private XSharedPreferences appXSP;
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
mFilters.add(new Filter.TextViewValidFilter());
mUniversalCopyHandler.setFilters(mFilters);
// installer 不注入。 防止代码出错。进不去installer 中。
if (!"de.robv.android.xposed.installer".equals(loadPackageParam.packageName) && !"com.android.systemui".equals(loadPackageParam.packageName)) {
findAndHookMethod(Activity.class, "onStart", new UniversalCopyOnStartHook());
findAndHookMethod(Activity.class, "onStop", new UniversalCopyOnStopHook());
}
}
private class UniversalCopyOnStartHook extends XC_MethodHook {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Activity activity = (Activity) param.thisObject;
mUniversalCopyHandler.onStart(activity);
}
}
private class UniversalCopyOnStopHook extends XC_MethodHook {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Activity activity = (Activity) param.thisObject;
mUniversalCopyHandler.onStop(activity);
}
}
}
最终都调用到了UniversalCopyHandler中,onStart和onStop只要简单的注册和注销BroadcastReceiver就行了,这里要注意的是:用try-catch把这部分代码包起来,否则容易出现崩溃:
public class XposedUniversalCopyHandler {
public static final String TAG="UniversalCopyHandler";
List<Activity> mActivities=new ArrayList<>();
IntentFilter intentFilter=new IntentFilter(UNIVERSAL_COPY_BROADCAST_XP);
Handler handler;
List<Filter> mFilters;
public void setFilters(List<Filter> mFilters) {
this.mFilters = mFilters;
}
public void onStart(Activity activity){
mActivities.add(activity);
try {
activity.getApplication().registerReceiver(mUniversalCopyBR,intentFilter);
} catch (Throwable e) {
e.printStackTrace();
}
}
public void onStop(Activity activity){
mActivities.remove(activity);
if (mActivities.size()==0){
try {
activity.getApplication().unregisterReceiver(mUniversalCopyBR);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
private BroadcastReceiver mUniversalCopyBR = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (handler==null){
handler=new Handler(Looper.getMainLooper());
}
handler.post(new Runnable() {
@Override
public void run() {
startUniversalCopy();
}
});
}
};
}
收到广播以后,就会调用到startUniversalCopy()方法,这里做的是:拿到当前Activity,遍历其DecorView,然后把结果发送到显示全局复制结果页中显示。直接看代码
private void startUniversalCopy(){
Log.e(TAG,"startUniversalCopy");
Activity topActivity=null;
ActivityManager activityManager= (ActivityManager) mActivities.get(0).getApplication().getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningTaskInfo> taskInfos=activityManager.getRunningTasks(1);
if (taskInfos.size()>0){
ComponentName top=taskInfos.get(0).topActivity;
if (top!=null){
String name=top.getClassName();
for (Activity activity:mActivities){
if (activity.getClass().getName().equals(name)){
topActivity=activity;
break;
}
}
}
}
if (topActivity==null){
if (mActivities.size()>0) {
topActivity = mActivities.get(mActivities.size() - 1);
if (topActivity.isFinishing()){
topActivity=null;
}
}
}
UniversalCopy(topActivity);
}
private int retryTimes=0;
private void UniversalCopy(final Activity activity) {
if (activity==null){
return;
}
boolean isSuccess=false;
label37: {
View decirView =activity.getWindow().getDecorView();
if(this.retryTimes < 10) {
String packageName;
packageName = activity.getPackageName();
if(decirView == null || packageName != null && packageName.contains("com.android.systemui")) {
++this.retryTimes;
this.handler.postDelayed(new Runnable() {
@Override
public void run() {
UniversalCopy(activity);
}
}, 100);
return;
}
WindowManager var5 = (WindowManager)activity.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
var5.getDefaultDisplay().getMetrics(displayMetrics);
int var1 = displayMetrics.heightPixels;
int var2 = displayMetrics.widthPixels;
ArrayList<CopyNode> nodeList = traverseNode(decirView, var2, var1);
for (CopyNode node:nodeList) {
Log.e(TAG, "traverseNode result= " + node);
}
if(nodeList.size() > 0) {
// Intent intent = new Intent(activity, CopyActivity.class);
Intent intent = new Intent();
intent.setComponent(new ComponentName(XposedConstant.PACKAGE_NAME,"com.forfan.bigbang.copy.CopyActivity"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Bundle bundle=new Bundle();
bundle.setClassLoader(CopyNode.class.getClassLoader());
bundle.putString("source_package", packageName);
bundle.putParcelableArrayList("copy_nodes", nodeList);
intent.putExtras(bundle);
try {
activity.startActivity(intent);
} catch (Throwable e) {
e.printStackTrace();
}
isSuccess = true;
break label37;
}
// ae.a(this.getApplication(), "APP_DATA", "UC_MODE_FAILED", packageName);
}
isSuccess = false;
}
if(!isSuccess) {
try {
Toast.makeText(activity, "error" , Toast.LENGTH_SHORT).show();
} catch (Throwable e) {
e.printStackTrace();
}
}
this.retryTimes = 0;
}
private ArrayList<CopyNode> traverseNode(View nodeInfo, int screenWidth, int scerrnHeight) {
ArrayList nodeList = new ArrayList();
if(nodeInfo != null ) {
if (!nodeInfo.isShown()){
return nodeList;
}
if (nodeInfo instanceof ViewGroup){
ViewGroup viewGroup = (ViewGroup) nodeInfo;
for(int var4 = 0; var4 < viewGroup.getChildCount(); ++var4) {
nodeList.addAll(this.traverseNode(viewGroup.getChildAt(var4), screenWidth, scerrnHeight));
}
}
if(nodeInfo.getClass().getName() != null && nodeInfo.getClass().getName().equals("android.webkit.WebView")) {
return nodeList;
} else {
String content = null;
String description = content;
if(nodeInfo.getContentDescription() != null) {
description = content;
if(!"".equals(nodeInfo.getContentDescription())) {
description = nodeInfo.getContentDescription().toString();
}
}
content = description;
String text=getTextInFilters(nodeInfo,mFilters);
if(text != null) {
content = description;
if(!"".equals(text)) {
content = text.toString();
}
}
if(content != null) {
Rect var8 = new Rect();
nodeInfo.getGlobalVisibleRect(var8);
if(checkBound(var8, screenWidth, scerrnHeight)) {
nodeList.add(new CopyNode(var8, content));
}
}
return nodeList;
}
} else {
return nodeList;
}
}
private String getTextInFilters(View v,List<Filter> filters){
for (Filter filter:filters){
if (filter.filter(v)){
return filter.getContent(v);
}
}
return null;
}
private boolean checkBound(Rect var1, int var2, int var3) {
return var1.bottom >= 0 && var1.right >= 0 && var1.top <= var3 && var1.left <= var2;
}
至于如何展示和让用户选择要复制的文字,则跟使用辅助服务实现全局复制一毛一样,这里就不再赘述了。
源码
详细代码可以看Bigbang工程源码的XposedBigBang和XposedUniversalCopyHandler类,XposedBigBang还包含了监控点击的hook,阅读代码时不要被影响了,感兴趣的同学可以看这篇——如何通过Xposed框架获取点击的文字。
还需要注意的是,Bigbang工程的通过productFlavors来区分Xposed版本和普通版本的,运行代码的时候注意修改。
源码也可以看UniversalCopy_xposed工程,这个是单独的Xposed实现全局复制的工程,除了和Bigbang中一样的全局复制功能,还包含了一些其他功能。