使用Xposed框架实现全局复制

转载注明出处:简书-十个雨点

简介

使用辅助服务实现全局复制中,我介绍了通过辅助服务实现全局复制的功能,极大的提高了复制功能的使用范围,补充了通过点击获取文字的不足。
如何通过Xposed框架获取点击的文字中,介绍了如何基于Xposed框架实现点击取词功能的,以及相对于辅助服务实现的优势。既然要摆脱辅助服务的限制,当然要把全局复制也用Xposed的方式实现了才行吧!

先看看效果

全局复制触发
全局复制触发

也可以下载全能分词体验

如果你觉得跟基于辅助服务实现的基本上没有区别,那就对了,因为压根就是一张图。。。。

Xposed 是什么?如何使用

关于Xposed框架如何使用的问题就不再赘述了,感兴趣的同学可以自行百度,或者参考这篇——如何通过Xposed框架获取点击的文字

如何实现全局复制

有两个关键点需要先考虑清楚:

  1. 使用辅助服务实现全局复制是通过遍历AccessibilityNodeInfo来获得当前界面的布局,并获取页面中的文字。所以很自然就可以想到,通过Xposed,可以直接遍历View树,从而拿到当前界面的布局和文字。
  2. 全局复制通过通知栏或者悬浮窗触发,在触发以后,需要在当前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中一样的全局复制功能,还包含了一些其他功能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容