一、需求背景
新項(xiàng)目開發(fā),需預(yù)置“天翼云電腦”app,云電腦app界面里其實(shí)就是盒子端接入的鼠標(biāo)和鍵盤外設(shè),來操作云端的windows系統(tǒng)桌面;
云電腦客戶端使用的android系統(tǒng)本地的鼠標(biāo)光標(biāo),而遠(yuǎn)端光標(biāo)(云桌面windows系統(tǒng)里的鼠標(biāo)光標(biāo))發(fā)生變更時會把新的光標(biāo)圖標(biāo)傳遞給客戶端,讓客戶端使用這個圖標(biāo)更新本地鼠標(biāo)光標(biāo);
但是android7.0以下應(yīng)用層沒有可以變更鼠標(biāo)光標(biāo)的API,所以如果設(shè)備時android7.0以下的系統(tǒng),需要廠家另行添加變更本地鼠標(biāo)光標(biāo)的API供云電腦app調(diào)用;
例如如下圖這種情況:
當(dāng)鼠標(biāo)移動到窗口邊緣時,需要將鼠標(biāo)光標(biāo)替換為雙箭頭圖標(biāo)


二、需求接口定義
系統(tǒng)與應(yīng)用約定,通過AIDL實(shí)現(xiàn)
setPointerIcon(Bitmap bitmap, int hotSpotX, int hotSpotY)和clear()
兩個函數(shù),來實(shí)現(xiàn)替換和恢復(fù)系統(tǒng)光標(biāo)
// IPointerIconService.aidl
package com.chinatelecom.clouddesk;
import android.graphics.Bitmap;
// Declare any non-default types here with import statements
interface IPointerIconService {
/*
** 設(shè)置自定義的鼠標(biāo)光標(biāo)圖片
** @params:
** bitmap: 自定義光標(biāo)圖片。目前云電腦使用的鼠標(biāo)標(biāo)準(zhǔn)鼠標(biāo)光標(biāo)圖標(biāo)大小為32*32左右,其大小不會超過100*100。
** hotSpotX: 圖標(biāo)在鼠標(biāo)光標(biāo)X軸絕對位置上的相對偏移量
** hotSpotY: 圖標(biāo)在鼠標(biāo)光標(biāo)Y軸絕對位置上的相對偏移量
*/
void setPointerIcon(in Bitmap bitmap, int hotSpotX, int hotSpotY);
//清除自定義鼠標(biāo)圖片的引用,恢復(fù)使用系統(tǒng)默認(rèn)的鼠標(biāo)圖標(biāo)光標(biāo)
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);
}
注:系統(tǒng)側(cè)需調(diào)測bitmap狀態(tài)為recycled以及bitmap狀態(tài)為null時的情形,以免出現(xiàn)此類異常時系統(tǒng)穩(wěn)定性出問題。
三、需求實(shí)現(xiàn)思路
閱讀源碼發(fā)現(xiàn),系統(tǒng)鼠標(biāo)光標(biāo)是在開機(jī)時由
frameworks\base\services\input\InputReader.cpp
中調(diào)用obtainPointerController函數(shù)創(chuàng)建指針控制器

\frameworks\base\services\jni\com_android_server_input_InputManagerService.cpp
的obtainPointerController函數(shù)中創(chuàng)建指針控制器,并給指針控制器賦值光標(biāo)bitmap圖標(biāo)資源

從上圖可以看出PointerIcon實(shí)例是通過 JNI Native callback回調(diào)到InputManagerService.java中的getPointerIcon()函數(shù)來獲?。?/p>

而且通過讀上述閱讀源碼發(fā)現(xiàn),com_android_server_input_InputManagerService.cpp中PointerController只實(shí)例化一次
那么我們就有了大致思路:
- 云電腦通過aidl將bitmap和偏移量傳遞AIDL服務(wù)
- AIDL服務(wù)通過廣播將bitmap和偏移量傳遞給InputManagerService.java
- InputManagerService中使用拿到的bitmap和偏移量,創(chuàng)建新的PointerIcon
- 然后InputManagerService.java通過JNI通知C層刷新鼠標(biāo)光標(biāo)
- C層中的單例PointerController去set新創(chuàng)建的PointerIcon即可完成光標(biāo)替換
四、功能具體實(shí)現(xiàn)
步驟一:實(shí)現(xiàn)AIDL服務(wù)和接口函數(shù)
實(shí)現(xiàn)AIDL服務(wù)和clear()、setPointerIcon()兩個函數(shù)
clear()函數(shù):發(fā)“com.pointer.clear”廣播
setPointerIcon()函數(shù):發(fā)“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
* 創(chuàng)建日期: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接收廣播
新增靜態(tài)變量:static int Icon_Type = 0;
當(dāng)接收到clear()函數(shù)發(fā)送的“com.pointer.clear”廣播時
將Icon_Type = 0
當(dāng)接收到setPointerIcon()函數(shù)發(fā)送的“com.pointer.change”廣播時
將Icon_Type = 1
并調(diào)用setSystemUiVisibility()Native函數(shù)
(這里也可以新增Jni native函數(shù)來通知下面,不過我這邊測試發(fā)現(xiàn)調(diào)用setSystemUiVisibility不影響其他功能就直接用這個函數(shù)了)
來通知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()函數(shù)
當(dāng)Icon_Type=1時獲取使用bundle傳遞的bitmap創(chuàng)建新的圖標(biāo)
當(dāng)Icon_Type=0時獲取系統(tǒng)默認(rèn)光標(biāo)
將getPointerIcon()函數(shù)按照上述思路修改,如下:

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

有之前上面源碼分析可知 obtainPointerController函數(shù)中
pointerController只創(chuàng)建一次,且由鎖持有
所以我們在函數(shù)中獲取需要獲取pointerController實(shí)例時,要加鎖
注意1:這也是我步驟二中沒有新建JNI接口的原因,查看源碼發(fā)現(xiàn)setSystemUiVisibility()函數(shù)已經(jīng)加鎖,并對pointerController進(jìn)行操作,所以直接在setSystemUiVisibility做了修改
注意2:當(dāng)然具體項(xiàng)目不同可能代碼有差異,如果修改setSystemUiVisibility()函數(shù)會對系統(tǒng)有影響,就需要自己實(shí)現(xiàn)JNI接口了,總之新實(shí)現(xiàn)的函數(shù)別忘了持有鎖
setSystemUiVisibility()函數(shù)修改前

setSystemUiVisibility()函數(shù)修改后
獲取pointerController實(shí)例,對controller進(jìn)行setPointerIcon操作來替換資源

自測試界面,調(diào)用兩個接口測試OK:

以上為Android 4.4.2 系統(tǒng),添加自定義系統(tǒng)鼠標(biāo)光標(biāo)接口的分析流程和實(shí)現(xiàn)步驟
不同android版本可能這部分代碼有些差異,但是總體思路不會變化
天翼云電腦的對接繞不開鼠標(biāo)光標(biāo)問題,應(yīng)該后續(xù)很多電信的項(xiàng)目有對接云電腦的需求
如有其他友商需實(shí)現(xiàn),可參考該實(shí)現(xiàn)方法