????本文仅用于做学习总结,转换成自己的理解,方便需要时快速查阅,深入研究可以去官网了解更多:官网链接点这里
????之前对接AI语音功能时,发现有些按钮(或文本)在我没有主动注册唤醒词场景下,还是响应了点击,使用profiler跟踪调用堆栈才发现是使用了无障碍服务实现的。因为开发的是系统应用,也没必要主动去打开无障碍服务开关,于是觉得无障碍服务有很大的可发挥空间,于是借助无障碍服务,实现了一个显示当前展示的Window/Activity/Dialog的悬浮窗,用于演示无障碍服务的用法及其强大之处。
class AccessibilityTest : AccessibilityService() {
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
Log.d(TAG, "onAccessibilityEvent: event = $event")
}
override fun onServiceConnected() {
super.onServiceConnected()
Log.d(TAG, "onServiceConnected: ")
}
override fun onUnbind(intent: Intent?): Boolean {
Log.d(TAG, "onUnbind: intent = $intent")
return super.onUnbind(intent)
}
override fun onInterrupt() {
Log.d(TAG, "onInterrupt: ")
}
}
????这个步骤和普通的Service注册有些不同,需要配置permission、intent-filter和无障碍服务的xml配置文件,基本都是固定的格式,只是按需改一些配置项。
<service
android:name=".accessibility.AccessibilityTest"
android:exported="true"
android:label="@string/accessibility_tip"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:process=":BackgroundService">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_config" />
</service>
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:canRetrieveWindowContent="true"
android:description="@string/accessibilty_desc" />
??:如果这里指定了包名(android:packageNames的值,多个包名用英文逗号分隔。)则只会收到对应包名应用的事件。
override fun onServiceConnected() {
super.onServiceConnected()
Log.d(TAG, "onServiceConnected: ")
val accessibilityServiceInfo = AccessibilityServiceInfo()
accessibilityServiceInfo.eventTypes = (AccessibilityEvent.TYPE_WINDOWS_CHANGED
or AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
or AccessibilityEvent.TYPE_VIEW_CLICKED
or AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
or AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED)
accessibilityServiceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK
accessibilityServiceInfo.notificationTimeout = 0
accessibilityServiceInfo.flags = AccessibilityServiceInfo.DEFAULT
// 如果这里指定了包名则只会收到对应包名应用的事件
accessibilityServiceInfo.packageNames = arrayOf("com.yanggui.animatortest")
serviceInfo = accessibilityServiceInfo
}
????完成以上的步骤,然后编译运行安装到手机上,然后从无障碍服务中开启,就能够在AccessibilityTest的onAccessibilityEvent中收到各种交互事件了。
无障碍服务跑起来后,会打印如下log:
// 从无障碍服务中打开accessibility_tip服务回调onServiceConnected方法:
14:25:34.170 D onServiceConnected:
// 打开当前应用会执行onAccessibilityEvent方法,回调一系列的事件信息,封装在// AccessibilityEvent中
14:26:44.792 D onAccessibilityEvent: event = EventType: TYPE_VIEW_CLICKED; EventTime: 7897173; PackageName: com.google.android.apps.nexuslauncher; MovementGranularity: 0; Action: 0; ContentChangeTypes: []; WindowChangeTypes: [] [ ClassName: android.widget.TextView; Text: [AnimatorTest]; ContentDescription: AnimatorTest; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: false; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: 0; ScrollY: 0; MaxScrollX: 0; MaxScrollY: 0; ScrollDeltaX: -1; ScrollDeltaY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
14:26:44.847 D onAccessibilityEvent: event = EventType: TYPE_WINDOW_CONTENT_CHANGED; EventTime: 7897231; PackageName: com.google.android.apps.nexuslauncher; MovementGranularity: 0; Action: 0; ContentChangeTypes: [CONTENT_CHANGE_TYPE_SUBTREE]; WindowChangeTypes: [] [ ClassName: android.widget.FrameLayout; Text: []; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: false; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: 0; ScrollY: 0; MaxScrollX: 0; MaxScrollY: 0; ScrollDeltaX: -1; ScrollDeltaY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
14:26:44.891 D onAccessibilityEvent: event = EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 7897277; PackageName: com.yanggui.animatortest; MovementGranularity: 0; Action: 0; ContentChangeTypes: []; WindowChangeTypes: [] [ ClassName: com.yanggui.animatortest.leanback.RowsSupportFragmentActivity; Text: [AnimatorTest]; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: true; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: 0; ScrollY: 0; MaxScrollX: 0; MaxScrollY: 0; ScrollDeltaX: -1; ScrollDeltaY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
14:26:44.952 D onAccessibilityEvent: event = EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 7897337; PackageName: com.yanggui.animatortest; MovementGranularity: 0; Action: 0; ContentChangeTypes: []; WindowChangeTypes: [] [ ClassName: com.yanggui.animatortest.leanback.RowsSupportFragmentActivity; Text: [AnimatorTest]; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: true; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: 0; ScrollY: 0; MaxScrollX: 0; MaxScrollY: 0; ScrollDeltaX: -1; ScrollDeltaY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
// 从无障碍服务中关闭accessibility_tip服务回调onUnbind方法:
14:24:38.264 D onUnbind: intent = Intent { cmp=com.yanggui.animatortest/.accessibility.AccessibilityTest }
@IntDef(flag = true, prefix = { "FEEDBACK_" }, value = {
FEEDBACK_AUDIBLE,
FEEDBACK_GENERIC,
FEEDBACK_HAPTIC,
FEEDBACK_SPOKEN,
FEEDBACK_VISUAL,
FEEDBACK_BRAILLE
})
@Retention(RetentionPolicy.SOURCE)
public @interface FeedbackType {}
@IntDef(
flag = true,
prefix = {"TYPE_"},
value = {
TYPE_VIEW_CLICKED, // 点击事件
TYPE_VIEW_LONG_CLICKED, // 长按事件
TYPE_VIEW_SELECTED, // view选中
TYPE_VIEW_FOCUSED, // view上焦,使用遥控操作的需要关注该事件
TYPE_VIEW_TEXT_CHANGED, // 表示更改 android.widget.EditText的文本的事件。
TYPE_WINDOW_STATE_CHANGED,
TYPE_NOTIFICATION_STATE_CHANGED,
TYPE_VIEW_HOVER_ENTER,
TYPE_VIEW_HOVER_EXIT,
TYPE_TOUCH_EXPLORATION_GESTURE_START,
TYPE_TOUCH_EXPLORATION_GESTURE_END,
TYPE_WINDOW_CONTENT_CHANGED,
TYPE_VIEW_SCROLLED,
TYPE_VIEW_TEXT_SELECTION_CHANGED,
TYPE_ANNOUNCEMENT,
TYPE_VIEW_ACCESSIBILITY_FOCUSED,
TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED,
TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
TYPE_GESTURE_DETECTION_START,
TYPE_GESTURE_DETECTION_END,
TYPE_TOUCH_INTERACTION_START,
TYPE_TOUCH_INTERACTION_END,
TYPE_WINDOWS_CHANGED,
TYPE_VIEW_CONTEXT_CLICKED,
TYPE_ASSIST_READING_CONTEXT,
TYPE_SPEECH_STATE_CHANGE
})
@Retention(RetentionPolicy.SOURCE)
public @interface EventType {}
????如上介绍的,无障碍服务是能够获取到各种交互事件,从onAccessibilityEvent回调中可轻松拿到交互控件的packageName和className,所以基于无障碍服务能力的支持,也就很容易实现悬浮展示当前Activity的功能了。
private const val TAG = "TopActivityEvent"
class TopActivityEventWindow {
companion object {
@SuppressLint("StaticFieldLeak")
private var rootView: View? = null
@SuppressLint("StaticFieldLeak")
private var tvContent: TextView? = null
@SuppressLint("StaticFieldLeak")
private var window: TopActivityEventWindow? = null
@SuppressLint("StaticFieldLeak")
fun showEvent(ctx: Context, pkgName: String?, activityClassName: String?) {
if (window == null) {
initEventWindow(ctx)
}
if (!pkgName.isNullOrBlank() && !activityClassName.isNullOrBlank()) {
tvContent?.text = "$pkgName\n$activityClassName"
} else {
Log.e(TAG, "showEvent: pkgName = $pkgName, activityClassName = $activityClassName")
}
}
private fun initEventWindow(ctx: Context) {
rootView =
LayoutInflater.from(ctx).inflate(R.layout.layout_top_activity_window, null, false)
val windowManager = ctx.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
windowManager?.apply {
val lp = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT
)
lp.type = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
WindowManager.LayoutParams.TYPE_TOAST
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// android31及以上 需要使用TYPE_APPLICATION_OVERLAY才能展示,使用ALERT_WINDOW会报没有权限
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
}
lp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
lp.gravity = Gravity.TOP or Gravity.LEFT
lp.format = PixelFormat.TRANSLUCENT
addView(rootView, lp)
}
tvContent = rootView?.findViewById(R.id.top_activity_window_text)
window = TopActivityEventWindow()
}
fun dismiss(ctx: Context) {
if (window != null) {
val windowManager = ctx.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
windowManager?.removeView(rootView)
tvContent = null
rootView = null
window = null
}
}
}
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
Log.d(TAG, "onAccessibilityEvent: event = $event")
if (event?.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
TopActivityEventWindow.showEvent(
this.applicationContext, "${event.packageName}", "${event.className}"
)
}
}