美文网首页
Android 4.4.2 自定义系统鼠标光标接口实现

Android 4.4.2 自定义系统鼠标光标接口实现

作者: 跟我去北方吧 | 来源:发表于2020-12-08 10:10 被阅读0次

    一、需求背景

    新项目开发,需预置“天翼云电脑”app,云电脑app界面里其实就是盒子端接入的鼠标和键盘外设,来操作云端的windows系统桌面;
    云电脑客户端使用的android系统本地的鼠标光标,而远端光标(云桌面windows系统里的鼠标光标)发生变更时会把新的光标图标传递给客户端,让客户端使用这个图标更新本地鼠标光标;
    但是android7.0以下应用层没有可以变更鼠标光标的API,所以如果设备时android7.0以下的系统,需要厂家另行添加变更本地鼠标光标的API供云电脑app调用;

    例如如下图这种情况:
    当鼠标移动到窗口边缘时,需要将鼠标光标替换为双箭头图标


    image.png
    image.png

    二、需求接口定义

    系统与应用约定,通过AIDL实现
    setPointerIcon(Bitmap bitmap, int hotSpotX, int hotSpotY)和clear()
    两个函数,来实现替换和恢复系统光标

    // IPointerIconService.aidl
    package com.chinatelecom.clouddesk;
    
    import android.graphics.Bitmap;
    // Declare any non-default types here with import statements
    
    interface IPointerIconService {
        /*
         ** 设置自定义的鼠标光标图片
         ** @params:
         ** bitmap: 自定义光标图片。目前云电脑使用的鼠标标准鼠标光标图标大小为32*32左右,其大小不会超过100*100。
         ** hotSpotX: 图标在鼠标光标X轴绝对位置上的相对偏移量
         ** hotSpotY: 图标在鼠标光标Y轴绝对位置上的相对偏移量
         */
         void setPointerIcon(in Bitmap bitmap, int hotSpotX, int hotSpotY);
    
         //清除自定义鼠标图片的引用,恢复使用系统默认的鼠标图标光标
         void clear();
    }
    
    期望的绑定方式:
    Intent intent = new Intent("com.chinatelecom.clouddesk.IPointerIconService");
    intent.setPackage("com.chinatelecom.clouddesk");
    if (!bindService(intent, mConnection, Context.BIND_AUTO_CREATE)) {
        Log.e("tanz", "Could not bind to IPointerIconService with "+intent);
    }
    
    注:系统侧需调测bitmap状态为recycled以及bitmap状态为null时的情形,以免出现此类异常时系统稳定性出问题。
    
    

    三、需求实现思路

    阅读源码发现,系统鼠标光标是在开机时由
    frameworks\base\services\input\InputReader.cpp
    中调用obtainPointerController函数创建指针控制器


    image.png

    \frameworks\base\services\jni\com_android_server_input_InputManagerService.cpp
    的obtainPointerController函数中创建指针控制器,并给指针控制器赋值光标bitmap图标资源


    9505430-63bfb23999f169c6.png

    从上图可以看出PointerIcon实例是通过 JNI Native callback回调到InputManagerService.java中的
    getPointerIcon()函数来获取;


    image.png

    而且通过读上述阅读源码发现,com_android_server_input_InputManagerService.cpp中PointerController只实例化一次

    那么我们就有了大致思路:
    只需要实现的AIDL服务与InputManagerService.java通信,将bitmap和偏移量传递过来,InputManagerService.java中创建新的PointerIcon;
    然后InputManagerService.java通过JNI通知com_android_server_input_InputManagerService.cpp中的单例PointerController去set新的PointerIcon即可实现光标图标的替换

    四、功能具体实现

    步骤一:实现AIDL服务和接口函数
    实现AIDL服务和clear()、setPointerIcon()两个函数

    clear()函数:发“com.pointer.clear”广播

    setPointerIcon()函数:发“com.pointer.change”广播
    并将bitmap、hotSpotX、hotSpotY通过Bundle传递

    package com.chinatelecom.clouddesk;
    
    import android.app.Service;
    import android.content.Context;
    import android.content.Intent;
    import android.graphics.Bitmap;
    import android.os.Bundle;
    import android.os.IBinder;
    import android.os.RemoteException;
    import android.util.Log;
    
    /**
     * 作者:libeibei
     * 创建日期:20201109
     * 类说明:
     * API:setPointerIcon for telecom clouddesk
     * logcat -v time |grep -e InputManager -e InputReader -e PointerController
     **/
    public class PointerIconService extends Service {
    
        private static final String TAG = "CH_InputManager";
        Context mContext = null;
        Intent intent = null;
        Bundle bundle = null;
    
        @Override
        public IBinder onBind(Intent intent) {
            Log.i(TAG, "-----> onBind() ……");
            mContext = PointerIconService.this.getApplicationContext();
            return new PointerIconStub();
        }
    
        @Override
        public boolean onUnbind(Intent intent) {
            return super.onUnbind(intent);
        }
    
        private class PointerIconStub extends IPointerIconService.Stub {
            @Override
            public void clear() throws RemoteException {
                Log.i(TAG, "-----> clear()");
                intent = new Intent("com.pointer.clear");
                sendBroadcast(intent);
            }
    
            @Override
            public void setPointerIcon(Bitmap bitmap, int hotSpotX, int hotSpotY) throws RemoteException {
                Log.i(TAG, "-----> setPointerIcon()");
                if (bitmap == null) {
                    Log.i(TAG, "-----> bitmap is null , cancel");
                } else if (bitmap.isRecycled()) {
                    Log.i(TAG, "-----> bitmap is isRecycled , cancel");
                } else {
                    intent = new Intent("com.pointer.change");
                    bundle = new Bundle();
                    bundle.putParcelable("bitmap", bitmap);
                    bundle.putFloat("hotSpotX", (float) hotSpotX);
                    bundle.putFloat("hotSpotY", (float) hotSpotY);
                    intent.putExtras(bundle);
                    sendBroadcast(intent);
                }
            }
        }
    
    }
    
    

    步骤二:InputManagerService接收广播
    新增静态变量:static int Icon_Type = 0;
    当接收到clear()函数发送的“com.pointer.clear”广播时
    将Icon_Type = 0
    当接收到setPointerIcon()函数发送的“com.pointer.change”广播时
    将Icon_Type = 1

    并调用setSystemUiVisibility()Native函数
    (这里也可以新增Jni native函数来通知下面,不过我这边测试发现调用setSystemUiVisibility不影响其他功能就直接用这个函数了)
    来通知com_android_server_input_InputManagerService.cpp中的
    PointerIconController来更新Icon

    frameworks\base\services\java\com\android\server\input\InputManagerService.java
    
        // libeibei add for change pointer begin
        private void registerPointerReceiver() {
            IntentFilter pointerFilter = new IntentFilter();
            pointerFilter.addAction("com.pointer.clear");
            pointerFilter.addAction("com.pointer.change");
            mContext.registerReceiver(pointerReceiver, pointerFilter);
        }
    
        static int Icon_Type = 0;
        static Bitmap bitmap = null;
        static float hotSpotX = 0;
        static float hotSpotY = 0;
        static Bundle mBundle = null;
    
        BroadcastReceiver pointerReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (intent.getAction().equals("com.pointer.clear")) {
                    Log.i(TAG, "-----> onReceive clear");
                    Icon_Type = 0;
                    bitmap = null;
                    hotSpotX = 0;
                    hotSpotY = 0;
                    setSystemUiVisibility(0);
                }
    
                if (intent.getAction().equals("com.pointer.change")) {
                    Log.i(TAG, "-----> onReceive change");
                    mBundle = intent.getExtras();
                    if (mBundle != null) {
                        Icon_Type = 1;
                        bitmap = (Bitmap) mBundle.getParcelable("bitmap");
                        hotSpotX = mBundle.getFloat("hotSpotX");
                        hotSpotY = mBundle.getFloat("hotSpotY");
                        Log.i(TAG, "-----> hotSpotX = " + hotSpotX + ",hotSpotY = " + hotSpotY);
                        setSystemUiVisibility(1);
                    } else {
                        Log.i(TAG, "-----> mBundle = null");
                        Icon_Type = 0;
                        bitmap = null;
                        hotSpotX = 0;
                        hotSpotY = 0;
                        setSystemUiVisibility(0);
                    }
                }
    
            }
        };
        // libeibei add for change pointer end
    

    并修改getPointerIcon()函数
    当Icon_Type=1时获取使用bundle传递的bitmap创建新的图标
    当Icon_Type=0时获取系统默认光标
    将getPointerIcon()函数按照上述思路修改,如下:

    image.png

    步骤三:修改com_android_server_input_InputManagerService.cpp更新PointerIcon

    image.png

    有之前上面源码分析可知 obtainPointerController函数中
    pointerController只创建一次,且由锁持有
    所以我们在函数中获取需要获取pointerController实例时,要加锁

    注意1:这也是我步骤二中没有新建JNI接口的原因,查看源码发现setSystemUiVisibility()函数已经加锁,并对pointerController进行操作,所以直接在setSystemUiVisibility做了修改
    注意2:当然具体项目不同可能代码有差异,如果修改setSystemUiVisibility()函数会对系统有影响,就需要自己实现JNI接口了,总之新实现的函数别忘了持有锁

    setSystemUiVisibility()函数修改前


    image.png

    setSystemUiVisibility()函数修改后
    获取pointerController实例,对controller进行setPointerIcon操作来替换资源


    image.png

    自测试界面,调用两个接口测试OK:


    ezgif-7-386343925533.gif

    以上为Android 4.4.2 系统,添加自定义系统鼠标光标接口的分析流程和实现步骤
    不同android版本可能这部分代码有些差异,但是总体思路不会变化
    天翼云电脑的对接绕不开鼠标光标问题,应该后续很多电信的项目有对接云电脑的需求
    如有其他友商需实现,可参考该实现方法

    相关文章

      网友评论

          本文标题:Android 4.4.2 自定义系统鼠标光标接口实现

          本文链接:https://www.haomeiwen.com/subject/uerfgktx.html