一养叛、需求背景
新項目開發(fā),需預置“天翼云電腦”app宰翅,云電腦app界面里其實就是盒子端接入的鼠標和鍵盤外設弃甥,來操作云端的windows系統(tǒng)桌面;
云電腦客戶端使用的android系統(tǒng)本地的鼠標光標汁讼,而遠端光標(云桌面windows系統(tǒng)里的鼠標光標)發(fā)生變更時會把新的光標圖標傳遞給客戶端淆攻,讓客戶端使用這個圖標更新本地鼠標光標;
但是android7.0以下應用層沒有可以變更鼠標光標的API嘿架,所以如果設備時android7.0以下的系統(tǒng)瓶珊,需要廠家另行添加變更本地鼠標光標的API供云電腦app調用;
例如如下圖這種情況:
當鼠標移動到窗口邊緣時耸彪,需要將鼠標光標替換為雙箭頭圖標
二伞芹、需求接口定義
系統(tǒng)與應用約定,通過AIDL實現
setPointerIcon(Bitmap bitmap, int hotSpotX, int hotSpotY)和clear()
兩個函數蝉娜,來實現替換和恢復系統(tǒng)光標
// 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);
//清除自定義鼠標圖片的引用,恢復使用系統(tǒng)默認的鼠標圖標光標
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)側需調測bitmap狀態(tài)為recycled以及bitmap狀態(tài)為null時的情形南缓,以免出現此類異常時系統(tǒng)穩(wěn)定性出問題。
三荧呐、需求實現思路
閱讀源碼發(fā)現汉形,系統(tǒng)鼠標光標是在開機時由
frameworks\base\services\input\InputReader.cpp
中調用obtainPointerController函數創(chuàng)建指針控制器
\frameworks\base\services\jni\com_android_server_input_InputManagerService.cpp
的obtainPointerController函數中創(chuàng)建指針控制器,并給指針控制器賦值光標bitmap圖標資源
從上圖可以看出PointerIcon實例是通過 JNI Native callback回調到InputManagerService.java中的getPointerIcon()函數來獲缺恫概疆;
而且通過讀上述閱讀源碼發(fā)現,com_android_server_input_InputManagerService.cpp中PointerController只實例化一次
那么我們就有了大致思路:
- 云電腦通過aidl將bitmap和偏移量傳遞AIDL服務
- AIDL服務通過廣播將bitmap和偏移量傳遞給InputManagerService.java
- InputManagerService中使用拿到的bitmap和偏移量峰搪,創(chuàng)建新的PointerIcon
- 然后InputManagerService.java通過JNI通知C層刷新鼠標光標
- C層中的單例PointerController去set新創(chuàng)建的PointerIcon即可完成光標替換
四届案、功能具體實現
步驟一:實現AIDL服務和接口函數
實現AIDL服務和clear()、setPointerIcon()兩個函數
clear()函數:發(fā)“com.pointer.clear”廣播
setPointerIcon()函數:發(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;
當接收到clear()函數發(fā)送的“com.pointer.clear”廣播時
將Icon_Type = 0
當接收到setPointerIcon()函數發(fā)送的“com.pointer.change”廣播時
將Icon_Type = 1
并調用setSystemUiVisibility()Native函數
(這里也可以新增Jni native函數來通知下面,不過我這邊測試發(fā)現調用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創(chuàng)建新的圖標
當Icon_Type=0時獲取系統(tǒng)默認光標
將getPointerIcon()函數按照上述思路修改咐蚯,如下:
步驟三:修改com_android_server_input_InputManagerService.cpp更新PointerIcon
有之前上面源碼分析可知 obtainPointerController函數中
pointerController只創(chuàng)建一次童漩,且由鎖持有
所以我們在函數中獲取需要獲取pointerController實例時,要加鎖
注意1:這也是我步驟二中沒有新建JNI接口的原因春锋,查看源碼發(fā)現setSystemUiVisibility()函數已經加鎖矫膨,并對pointerController進行操作,所以直接在setSystemUiVisibility做了修改
注意2:當然具體項目不同可能代碼有差異期奔,如果修改setSystemUiVisibility()函數會對系統(tǒng)有影響侧馅,就需要自己實現JNI接口了,總之新實現的函數別忘了持有鎖
setSystemUiVisibility()函數修改前
setSystemUiVisibility()函數修改后
獲取pointerController實例呐萌,對controller進行setPointerIcon操作來替換資源
自測試界面馁痴,調用兩個接口測試OK:
以上為Android 4.4.2 系統(tǒng),添加自定義系統(tǒng)鼠標光標接口的分析流程和實現步驟
不同android版本可能這部分代碼有些差異肺孤,但是總體思路不會變化
天翼云電腦的對接繞不開鼠標光標問題罗晕,應該后續(xù)很多電信的項目有對接云電腦的需求
如有其他友商需實現,可參考該實現方法