【Android】Accessibility 利用無障礙 自動化控制APP 麥當勞簽到範例
Android Accessibility 是一項提高 Android 裝置的可訪問性的功能。它讓視力、聽力、行動或認知上有障礙的人們更容易地使用 Android 設備。
無障礙功能包括語音指導、螢幕放大、無障礙模式和無障礙鍵盤等,這些技術可以幫助使用者獲得更好的訪問性。使用這些功能,使用者可以更輕鬆地使用 Android 設備,使他們更獨立、更自信地參與日常活動。
文章目錄
- 創建 AccessibilityService
- Manifest 註冊 Service
- 判斷是否開啟無障礙
- 創建 BoradcastReceiver 監聽 無障礙的事件
- 創建 ViewModel 改變數據
- 使用 uiautomatorviewer 取得元件 ID
- 元件交互
- 撰寫自動化邏輯
- AccessibilityDemo Github
1.創建 AccessibilityService
MyAccessibilityService.kt
class MyAccessibilityService : AccessibilityService() {
//自動化流程
override fun onAccessibilityEvent(event: AccessibilityEvent) {
}
// 開啟無障礙
override fun onServiceConnected() {
super.onServiceConnected()
}
// 中斷無障礙
override fun onInterrupt() {
}
// 關閉無障礙
override fun onUnbind(intent: Intent?): Boolean {
return super.onUnbind(intent)
}
}
2.Manifest 註冊 Service
AndroidManifest.xml
<service
android:name=".service.MyAccessibilityService"
android:exported="true"
android:label="@string/app_name"
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/accessibilityservice" />
</service>
accessibilityservice.xml
<?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:accessibilityFlags="flagDefault"
android:canRetrieveWindowContent="true"
android:notificationTimeout="100" />
3.判斷是否開啟無障礙
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val accessibilityPage = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.fab.setOnClickListener {
if (!isAccessibilityEnabled()) {
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
accessibilityPage.launch(intent)
}
}
}
private fun isAccessibilityEnabled(): Boolean {
val am = getSystemService(ACCESSIBILITY_SERVICE) as AccessibilityManager
return am.isEnabled
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/accessibleTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:text="無障礙狀態:"
android:textColor="@color/black"
android:textSize="24sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/accessibleStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="關閉"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="@+id/accessibleTitle"
app:layout_constraintStart_toEndOf="@+id/accessibleTitle"
app:layout_constraintTop_toTopOf="@+id/accessibleTitle" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_margin="12dp"
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@android:drawable/ic_dialog_email"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
4.創建 BoradcastReceiver 監聽 無障礙的事件
MainActivity,kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val mainViewModel by viewModels<MainViewModel>()
private val accessibilityPage = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
// 監聽無障礙
private val accessibleReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action.equals(Common.ACCESSIBLE_RECEIVER_ACTION)) {
mainViewModel.accessibleStatus.value =
intent.getStringExtra(Common.ACCESSIBLE_STATUS)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
registerReceiver(accessibleReceiver, IntentFilter(Common.ACCESSIBLE_RECEIVER_ACTION))
mainViewModel.accessibleStatus.observe(this) {
binding.accessibleStatus.text = it
}
binding.fab.setOnClickListener {
if (!isAccessibilityEnabled()) {
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
accessibilityPage.launch(intent)
} else {
Intent(Common.AUTO_SERVICE_PACKAGE).apply {
putExtra(Common.APP_NAME, "麥當勞")
sendBroadcast(this)
}
}
}
}
private fun isAccessibilityEnabled(): Boolean {
val am = getSystemService(ACCESSIBILITY_SERVICE) as AccessibilityManager
return am.isEnabled
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(accessibleReceiver)
}
}
MyAccessibilityService.kt
class MyAccessibilityService : AccessibilityService() {
private val serviceReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action.equals(Common.AUTO_SERVICE_PACKAGE)) {
performGlobalAction(GLOBAL_ACTION_HOME)
Thread.sleep(1500)
rootInActiveWindow?.findAccessibilityNodeInfosByText(intent.getStringExtra(Common.APP_NAME))
?.apply {
if (size > 0) {
get(0).click()
}
}
}
}
}
override fun onAccessibilityEvent(event: AccessibilityEvent) {
}
override fun onServiceConnected() {
super.onServiceConnected()
registerReceiver(serviceReceiver, IntentFilter(Common.AUTO_SERVICE_PACKAGE))
Intent(Common.ACCESSIBLE_RECEIVER_ACTION).apply {
putExtra(Common.ACCESSIBLE_STATUS, "開啟")
sendBroadcast(this)
}
}
override fun onInterrupt() {
Intent(Common.ACCESSIBLE_RECEIVER_ACTION).apply {
putExtra(Common.ACCESSIBLE_STATUS, "中斷")
sendBroadcast(this)
}
}
override fun onUnbind(intent: Intent?): Boolean {
unregisterReceiver(serviceReceiver)
Intent(Common.ACCESSIBLE_RECEIVER_ACTION).apply {
putExtra(Common.ACCESSIBLE_STATUS, "關閉")
sendBroadcast(this)
}
return super.onUnbind(intent)
}
}
5.創建 ViewModel 改變數據
MainViewModel.kt
class MainViewModel : ViewModel() {
val accessibleStatus = MutableLiveData<String>()
}
build.gradle
dependencies {
def fragment_version = "1.5.7"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
}
6.使用 uiautomatorviewer 取得元件 ID
uiautomatorviewer (AppData\Local\Android\Sdk\tools\bin)
手機號碼 ID (tw.com.mcddaily:id/etPhone)
登入按鈕 ID (com.android.camera:id/btnNext)
密碼 ID (com.android.camera:id/etPwd)
首頁文字 (天天來簽到)
7.元件交互
取得介面元件的兩種方式
event.source?.findAccessibilityNodeInfosByViewId
rootInActiveWindow?.findAccessibilityNodeInfosByViewId
AccessibilityExtension.kt
// 點擊
fun AccessibilityNodeInfo.click() = performAction(AccessibilityNodeInfo.ACTION_CLICK)
// 長按
fun AccessibilityNodeInfo.longClick() =
performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)
// 向下滑動一下
fun AccessibilityNodeInfo.scrollForward() =
performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
// 向上滑動一下
fun AccessibilityNodeInfo.scrollBackward() =
performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD)
// 填充文字
fun AccessibilityNodeInfo.input(content: String) = performAction(
AccessibilityNodeInfo.ACTION_SET_TEXT, Bundle().apply {
putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, content)
}
)
全局
// 返回键
performGlobalAction(GLOBAL_ACTION_BACK)
// Home键
performGlobalAction(GLOBAL_ACTION_HOME)
// 截圖
performGlobalAction(GLOBAL_ACTION_TAKE_SCREENSHOT)
// 最近事件
performGlobalAction(GLOBAL_ACTION_RECENTS)
// 通知欄
performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS)
// 鎖螢幕
performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN)
// 長按電源鍵
performGlobalAction(GLOBAL_ACTION_POWER_DIALOG)
8.撰寫自動化邏輯
MyAccessibilityService.kt
class MyAccessibilityService : AccessibilityService() {
private var autoTransferStatus = true
private var login = false
private val serviceReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action.equals(Common.AUTO_SERVICE_PACKAGE)) {
performGlobalAction(GLOBAL_ACTION_HOME)
Thread.sleep(1500)
rootInActiveWindow?.findAccessibilityNodeInfosByText(intent.getStringExtra(Common.APP_NAME))
?.apply {
if (size > 0) {
get(0).click()
}
}
}
}
}
override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (event.packageName == Common.PACKAGE_NAME && event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED && autoTransferStatus) {
if(!login) {
Thread.sleep(250)
rootInActiveWindow?.findAccessibilityNodeInfosByViewId("{event.packageName}:id/etPhone")
?.apply {
if (size>0) {
get(0).input("0912345678")
}
}
Thread.sleep(250)
rootInActiveWindow?.findAccessibilityNodeInfosByViewId("{event.packageName}:id/btnNext")
?.apply {
if (size > 0) {
get(0).click()
}
}
Thread.sleep(250)
rootInActiveWindow?.findAccessibilityNodeInfosByViewId("{event.packageName}:id/etPwd")
?.apply {
if (size>0) {
get(0).input("a123456")
}
}
Thread.sleep(250)
rootInActiveWindow?.findAccessibilityNodeInfosByViewId("{event.packageName}:id/btnNext")
?.apply {
if (size > 0) {
get(0).click()
}
}
}
Thread.sleep(250)
rootInActiveWindow?.findAccessibilityNodeInfosByText("天天來簽到")?.apply {
if (size > 0) {
login = true
autoTransferStatus = false
}
}
}
}
override fun onServiceConnected() {
super.onServiceConnected()
registerReceiver(serviceReceiver, IntentFilter(Common.AUTO_SERVICE_PACKAGE))
Intent(Common.ACCESSIBLE_RECEIVER_ACTION).apply {
putExtra(Common.ACCESSIBLE_STATUS, "開啟")
sendBroadcast(this)
}
}
override fun onInterrupt() {
Intent(Common.ACCESSIBLE_RECEIVER_ACTION).apply {
putExtra(Common.ACCESSIBLE_STATUS, "中斷")
sendBroadcast(this)
}
}
override fun onUnbind(intent: Intent?): Boolean {
unregisterReceiver(serviceReceiver)
Intent(Common.ACCESSIBLE_RECEIVER_ACTION).apply {
putExtra(Common.ACCESSIBLE_STATUS, "關閉")
sendBroadcast(this)
}
return super.onUnbind(intent)
}
}
9.AccessibilityDemo Github
AccessibilityDemo Github