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

一、需求背景

新项目开发,需预置“天翼云电脑”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将bitmap和偏移量传递AIDL服务
  • AIDL服务通过广播将bitmap和偏移量传递给InputManagerService.java
  • InputManagerService中使用拿到的bitmap和偏移量,创建新的PointerIcon
  • 然后InputManagerService.java通过JNI通知C层刷新鼠标光标
  • C层中的单例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版本可能这部分代码有些差异,但是总体思路不会变化
天翼云电脑的对接绕不开鼠标光标问题,应该后续很多电信的项目有对接云电脑的需求
如有其他友商需实现,可参考该实现方法

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

推荐阅读更多精彩内容