02月09, 2017

使用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中一样的全局复制功能,还包含了一些其他功能。

转载注明出处:十个雨点

本文链接:http://www.siki.space/post/universal_copy_through_xposed.html

-- EOF --

Comments