1.無障礙服務在后臺運行,并在系統(tǒng)觸發(fā)回調時接收這些回調事件,如焦點改變、按鈕被點擊等。
服務可以對特定包程序進行監(jiān)控,也可以進行所有應用包的監(jiān)控,需要設置對應的權限
服務類需要繼承 AccessibilityService 類
class ForegroundDetectorService : AccessibilityService() {}
2.生命周期
無障礙服務的生命周期由系統(tǒng)進行管理,并遵循已經建立的服務的生命周期.服務的啟動需要用戶在設置中手動打開來實現.
當打開無障礙服務后會調用 override fun onServiceConnected() {} 方法,可以進行一些操作,如設置無障礙反饋類型的詳細配置,如:
override fun onServiceConnected() {
? ? serviceInfo.apply {
? ? ? ? // 設置組合反饋類型(語音+震動)
? ? ? ? feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN or
? ? ? ? ? ? ? ? ? ? ? AccessibilityServiceInfo.FEEDBACK_HAPTIC
? ? ? ? // 配置語音反饋的詳細參數
? ? ? ? speechRate = 1.2f? // 語速
? ? ? ? isSpeechShutDown = false // 保持語音開啟
? ? }
? ? this.serviceInfo = serviceInfo
}
當關閉無障礙服務后會調用 override fun disableSelf() {} 方法
3.聲明權限
無障礙服務需要在 AndroidManifest.xml 中進行權限聲明
需要進行權限指定 android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" 和無障礙服務指定 android:name="android.accessibilityservice.AccessibilityService" 如下:
<service
? ? ? ? ? ? android:name=".ForegroundDetectorService"
? ? ? ? ? ? android:exported="false"
? ? ? ? ? ? android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
? ? ? ? ? ? <intent-filter>
? ? ? ? ? ? ? ? <action android:name="android.accessibilityservice.AccessibilityService"/>
? ? ? ? ? ? </intent-filter>
? ? ? ? ? ? <meta-data
? ? ? ? ? ? ? ? android:name="android.accessibilityservice"
? ? ? ? ? ? ? ? android:resource="@xml/empty_config"/>
? ? ? ? </service>
ForegroundDetectorService 是我們的無障礙服務, empty_config 是服務的配置設置,可以設置接收特定類型的無障礙事件,只監(jiān)聽特定應用包,指定時間內只獲取一次類型的事件,檢查窗口內容等
如下:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
? ? android:accessibilityEventTypes="typeWindowStateChanged"
? ? android:canRetrieveWindowContent="true"
? ? android:accessibilityFlags="flagDefault"
? ? android:description="@string/accessibility_service_desc"/>
可配置的項如下:
android:description 相關數據的描述文本
android:summary 摘要
android:settingsActivity 允許用戶修改此服務設置的活動的完全限定類名。
android:accessibilityEventTypes 此服務希望接收的事件類型
android:packageNames 希望接收的包,逗號分隔,如果不設置則接收所有包
android:accessibilityFeedbackType 服務提供的反饋類型
android:notificationTimeout 兩個相同事件類型的最短發(fā)送周期,單位是毫秒
android:accessibilityFlags 附加標志(反饋類型)
android:canRetrieveWindowContent 是否需要獲取活動窗口內容
android:canRequestTouchExplorationMode 是否支持觸摸模式,在此模式下,觸摸的項目會被朗讀出來,用戶界面可以通過手勢進行探索
android:canRequestEnhancedWebAccessibility 是否支持網頁無障礙增強功能
android:canRequestFilterKeyEvents 是否需要請求過濾關鍵事件
android:canControlMagnification 是否可以控制顯示放大
android:canPerformGestures 是否可以執(zhí)行手勢
android:canRequestFingerprintGestures 是否獲取從之吻傳感器獲取手勢
android:nonInteractiveUiTimeout 推薦的超時毫秒
android:interactiveUiTimeout
android:animatedImageDrawable 是否展示無障礙服務的使用動畫,用于幫助用戶如何使用它
android:htmlDescription Html 描述,展示無障礙服務的快捷方式使用,可用性和限制
android:canTakeScreenshot 是否需要能夠截取屏幕圖片
android:isAccessibilityTool 是否使用輔助功能來幫助殘障用戶
android:tileService 完全限定類名,與無障礙快捷方式 android.service.quicksettings.TileService 一對一映射
android:intro 無障礙快捷方式目標的詳細介紹,包括目的或行為
4. 無障礙服務的事件類型
視圖被點擊(如按鈕、復選框)
AccessibilityEvent.TYPE_VIEW_CLICKED
視圖被長按
AccessibilityEvent.TYPE_VIEW_LONG_CLICKED
視圖獲得焦點(如輸入框被選中)
AccessibilityEvent.TYPE_VIEW_FOCUSED
列表項或下拉選項被選中
AccessibilityEvent.TYPE_VIEW_SELECTED
視圖文本內容發(fā)生變化(如輸入框文字修改)
AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
窗口狀態(tài)變化(如彈出對話框或菜單)
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
通知欄狀態(tài)變化
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED
觸摸瀏覽手勢開始/結束(視障用戶手勢操作)
AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START
觸摸瀏覽手勢開始/結束(視障用戶手勢操作)
AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END
鼠標懸停進入/離開視圖(支持鼠標設備)
AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
鼠標懸停進入/離開視圖(支持鼠標設備)
AccessibilityEvent.TYPE_VIEW_HOVER_EXIT
視圖發(fā)生滾動
AccessibilityEvent.TYPE_VIEW_SCROLLED
文本選中范圍變化(如編輯框選中文字)
AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
窗口內容或布局結構變化
AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
應用主動觸發(fā)的無障礙通知(如語音提示)
AccessibilityEvent.TYPE_ANNOUNCEMENT
系統(tǒng)手勢監(jiān)測開始/結束
AccessibilityEvent.TYPE_GESTURE_DETECTION_START
系統(tǒng)手勢監(jiān)測開始/結束
AccessibilityEvent.TYPE_GESTURE_DETECTION_END
觸摸交互開始/結束(全局觸摸事件)
AccessibilityEvent.TYPE_TOUCH_INTERACTION_START
觸摸交互開始/結束(全局觸摸事件)
AccessibilityEvent.TYPE_TOUCH_INTERACTION_END
視圖獲得/失去無障礙焦點(區(qū)別于普通焦點)
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
屏幕窗口層級變化(如多任務切換)
AccessibilityEvent.TYPE_WINDOWS_CHANGED
視圖獲得/失去無障礙焦點(區(qū)別于普通焦點)
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
5.無障礙的反饋類型
聲音反饋 通過非語音的提示音(如蜂鳴聲、音效)提供反饋
AccessibilityServiceInfo.FEEDBACK_AUDIBLE
震動反饋 通過設備振動提供觸覺反饋
AccessibilityServiceInfo.FEEDBACK_HAPTIC
語音反饋 通過語音合成(TTS)朗讀文字內容(最常用的無障礙反饋方式)
AccessibilityServiceInfo.FEEDBACK_SPOKEN
視覺反饋 通過界面元素變化(如高亮、彈窗)提供視覺提示
AccessibilityServiceInfo.FEEDBACK_VISUAL
通用反饋 未指定具體類型的默認反饋機制
AccessibilityServiceInfo.FEEDBACK_GENERIC
盲文反饋 通過連接盲文點字顯示器輸出內容(針對視障用戶)
AccessibilityServiceInfo.FEEDBACK_BRAILLE
例子:
package com.example.footprint
//通知
//協程基礎包
//協程掛起函數支持
import android.accessibilityservice.AccessibilityService
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.graphics.PixelFormat
import android.os.Build
import android.util.Log
import android.view.Gravity
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
import android.widget.TextView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
import android.graphics.Color
//無障礙服務
class ForegroundDetectorService : AccessibilityService() {
? ? // 定義協程作用域
? ? //private val scope = CoroutineScope(Dispatchers.IO)
? ? private val job = SupervisorJob()
? ? private val scope = CoroutineScope(Dispatchers.IO + job)
? ? //懸浮窗相關
? ? private lateinit var windowManager: WindowManager
? ? private var overlayText: TextView? = null
? ? //無障礙服務中的事件
? ? override fun onAccessibilityEvent(event: AccessibilityEvent) {
? ? ? ? Log.d("ForegroundDetector", "${event.eventType}")
? ? ? ? if (event.eventType == TYPE_WINDOW_STATE_CHANGED) {
? ? ? ? }
? ? ? ? //窗口狀態(tài)變化(如彈出對話框或菜單)
? ? ? ? if (event.eventType == TYPE_WINDOW_STATE_CHANGED) {
? ? ? ? ? ? val currentApp = event.packageName?.toString()
? ? ? ? ? ? val appName = currentApp?.let { packageName ->
? ? ? ? ? ? ? ? try {
? ? ? ? ? ? ? ? ? ? // 通過PackageManager獲取應用名
? ? ? ? ? ? ? ? ? ? val pm = this@ForegroundDetectorService.packageManager
? ? ? ? ? ? ? ? ? ? pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString()
? ? ? ? ? ? ? ? } catch (e: Exception) {
? ? ? ? ? ? ? ? ? ? packageName // 失敗時返回包名
? ? ? ? ? ? ? ? }
? ? ? ? ? ? } ?: "null"
? ? ? ? ? ? val deviceModel = Build.MANUFACTURER + " " + Build.MODEL
? ? ? ? ? ? // 發(fā)送通知
? ? ? ? ? ? showNotification("前臺應用已更改: $currentApp", this)
? ? ? ? ? ? //網絡請求
? ? ? ? ? ? val prefs = getSharedPreferences("AppPrefs", MODE_PRIVATE)
? ? ? ? ? ? val serverUrl = prefs.getString("SERVER_URL", "https://footprint.codevtool.com/updata.php")
? ? ? ? ? ? //使用協程進行異步網絡請求,防止 ANR(應用無響應)錯誤
? ? ? ? ? ? scope.launch {
? ? ? ? ? ? // 執(zhí)行網絡請求
? ? ? ? ? ? ? ? // 執(zhí)行網絡請求
? ? ? ? ? ? ? ? try {
? ? ? ? ? ? ? ? ? ? val url = URL(serverUrl)
? ? ? ? ? ? ? ? ? ? (url.openConnection() as HttpURLConnection).apply {
? ? ? ? ? ? ? ? ? ? ? ? requestMethod = "POST"
? ? ? ? ? ? ? ? ? ? ? ? doOutput = true
? ? ? ? ? ? ? ? ? ? ? ? OutputStreamWriter(outputStream).use {
? ? ? ? ? ? ? ? ? ? ? ? ? ? it.write("手機,${currentApp},${appName},${deviceModel}")
? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? ? ? inputStream.bufferedReader().use {
? ? ? ? ? ? ? ? ? ? ? ? ? ? Log.d("Network", "響應: ${it.readText()}")
? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? } catch (e: Exception) {
? ? ? ? ? ? ? ? ? ? Log.e("Network", "請求失敗", e)
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? override fun onInterrupt() {}
? ? override fun onServiceConnected() {
? ? ? ? // 可選:配置無障礙服務參數
? ? }
? ? override fun onCreate() {
? ? ? ? super.onCreate()
? ? ? ? // 初始化窗口管理器
? ? ? ? windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
? ? }
? ? override fun onDestroy() {
? ? ? ? super.onDestroy()
? ? ? ? // 取消所有協程以避免內存泄漏
? ? ? ? // 正確地取消所有子協程
? ? ? ? job.cancel()
? ? ? ? //銷毀彈窗視圖
? ? ? ? overlayText?.takeIf { it.parent != null }?.let {
? ? ? ? ? ? windowManager.removeView(it)
? ? ? ? }
? ? }
? ? //展示無障礙格式的懸浮視圖
? ? private fun showOverlay(text: String) {
? ? ? ? // 移除舊視圖
? ? ? ? overlayText?.takeIf { it.parent != null }?.let {
? ? ? ? ? ? windowManager.removeView(it)
? ? ? ? }
? ? ? ? // 創(chuàng)建新視圖
? ? ? ? TextView(this).apply {
? ? ? ? ? ? setText(text)
? ? ? ? ? ? setBackgroundColor(0x66000000)
? ? ? ? ? ? setTextColor(Color.WHITE)
? ? ? ? ? ? setPadding(20, 10, 20, 10)
? ? ? ? ? ? WindowManager.LayoutParams(
? ? ? ? ? ? ? ? WindowManager.LayoutParams.WRAP_CONTENT,
? ? ? ? ? ? ? ? WindowManager.LayoutParams.WRAP_CONTENT,
? ? ? ? ? ? ? ? WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
? ? ? ? ? ? ? ? WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
? ? ? ? ? ? ? ? PixelFormat.TRANSLUCENT
? ? ? ? ? ? ).also { params ->
? ? ? ? ? ? ? ? params.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
? ? ? ? ? ? ? ? params.y = 100
? ? ? ? ? ? ? ? windowManager.addView(this, params)
? ? ? ? ? ? ? ? overlayText = this
? ? ? ? ? ? }
? ? ? ? ? ? // 3秒后自動消失
? ? ? ? ? ? postDelayed({
? ? ? ? ? ? ? ? if (parent != null) windowManager.removeView(this)
? ? ? ? ? ? }, 3000)
? ? ? ? }
? ? }
? ? // 顯示通知的輔助方法
? ? private fun showNotification(message: String, context: Context) {
? ? ? ? val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
? ? ? ? val channelId = "foreground_detector_channel"
? ? ? ? //因為 模塊級build.gradle 中配置的 minSdkVersion 已設置為26(Android 8.0/Oreo)或更高版本 因此條件判斷Build.VERSION.SDK_INT >= Build.VERSION_CODES.O始終為真,屬于冗余代碼
? ? ? ? //VERSION_CODES.O對應API級別26
? ? ? ? if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
? ? ? ? ? ? val channel = NotificationChannel(channelId, "Foreground Detector Channel", NotificationManager.IMPORTANCE_DEFAULT).apply {
? ? ? ? ? ? ? ? setSound(null, null)? // 禁用聲音
? ? ? ? ? ? ? ? enableVibration(false)? // 禁用震動
? ? ? ? ? ? ? ? lockscreenVisibility = Notification.VISIBILITY_SECRET? // 鎖屏不顯示
? ? ? ? ? ? }
? ? ? ? ? ? notificationManager.createNotificationChannel(channel)
? ? ? ? }
? ? ? ? val notification = Notification.Builder(context, channelId)
? ? ? ? ? ? .setContentTitle("前臺應用更改通知")
? ? ? ? ? ? .setContentText(message)
? ? ? ? ? ? .setSmallIcon(android.R.drawable.ic_dialog_info) // 替換為你的圖標資源
? ? ? ? ? ? .build()
? ? ? ? notificationManager.notify(1, notification)
? ? }
}