Android

【Android】Accessibility 利用無障礙 自動化控制APP 麥當勞簽到範例

【Android】Accessibility 利用無障礙 自動化控制APP 麥當勞簽到範例

Android Accessibility 是一項提高 Android 裝置的可訪問性的功能。它讓視力、聽力、行動或認知上有障礙的人們更容易地使用 Android 設備。

無障礙功能包括語音指導、螢幕放大、無障礙模式和無障礙鍵盤等,這些技術可以幫助使用者獲得更好的訪問性。使用這些功能,使用者可以更輕鬆地使用 Android 設備,使他們更獨立、更自信地參與日常活動。


文章目錄

  1. 創建 AccessibilityService
  2. Manifest 註冊 Service
  3. 判斷是否開啟無障礙
  4. 創建 BoradcastReceiver 監聽 無障礙的事件
  5. 創建 ViewModel 改變數據
  6. 使用 uiautomatorviewer 取得元件 ID
  7. 元件交互
  8. 撰寫自動化邏輯
  9. 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

發表迴響