Skip to content

Instantly share code, notes, and snippets.

@sajjadyousefnia
Created January 31, 2025 16:14
Show Gist options
  • Save sajjadyousefnia/3b5e4db585862d05ac00696b0e81ef14 to your computer and use it in GitHub Desktop.
Save sajjadyousefnia/3b5e4db585862d05ac00696b0e81ef14 to your computer and use it in GitHub Desktop.
package com.yourpackage
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.Toast
class MyAccessibilityService : AccessibilityService() {
private val TAG = "MyAccessibilityService"
private var isChromeOpen = false
private var urls: MutableList<String> = mutableListOf()
private var currentUrlIndex = 0
private val handler = Handler(Looper.getMainLooper())
private val urlReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
urls = intent?.getStringArrayListExtra("urls")?.toMutableList() ?: mutableListOf()
currentUrlIndex = 0
if (urls.isNotEmpty()) {
openUrlInChrome(urls[currentUrlIndex])
}
}
}
override fun onServiceConnected() {
super.onServiceConnected()
val info = AccessibilityServiceInfo()
info.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC
info.notificationTimeout = 100
info.packageNames = arrayOf("com.android.chrome") // Listen only for Chrome
this.serviceInfo = info
Log.d(TAG, "Accessibility service connected")
registerReceiver(urlReceiver, IntentFilter("com.yourpackage.ACTION_SEND_URLS"))
}
override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
val packageName = event.packageName.toString()
Log.d(TAG, "onAccessibilityEvent: $packageName")
if (packageName == "com.android.chrome") {
if (!isChromeOpen) {
isChromeOpen = true
} else {
findAndSavePage()
}
} else {
isChromeOpen = false
}
}
}
private fun openUrlInChrome(url: String) {
Log.d(TAG, "openUrlInChrome: $url")
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
intent.setPackage("com.android.chrome")
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
startActivity(intent)
} catch (e: Exception) {
Log.e(TAG, "Error opening URL in Chrome", e)
Toast.makeText(this, "Error opening URL in Chrome", Toast.LENGTH_SHORT).show()
moveToNextUrl()
}
}
private fun findAndSavePage() {
Log.d(TAG, "findAndSavePage")
val rootNodeInfo = rootInActiveWindow ?: return
// Find the menu button (three dots)
val menuButton = findNodeByContentDescription(rootNodeInfo, "More options")
if (menuButton != null) {
menuButton.performAction(AccessibilityNodeInfo.ACTION_CLICK)
// Wait for the menu to open
handler.postDelayed({
// Find the "Save" or "Bookmark" option
val saveOption = findNodeByText(rootInActiveWindow, "Save")
if (saveOption != null) {
saveOption.performAction(AccessibilityNodeInfo.ACTION_CLICK)
Log.d(TAG, "Page saved successfully")
moveToNextUrl()
} else {
Log.e(TAG, "Save option not found")
moveToNextUrl()
}
}, 1000)
} else {
Log.e(TAG, "Menu button not found")
moveToNextUrl()
}
}
private fun moveToNextUrl() {
currentUrlIndex++
if (currentUrlIndex < urls.size) {
openUrlInChrome(urls[currentUrlIndex])
} else {
// All URLs processed
Log.d(TAG, "All URLs processed")
sendBroadcast(Intent("com.yourpackage.ACTION_URLS_PROCESSED"))
}
}
private fun findNodeByText(nodeInfo: AccessibilityNodeInfo?, text: String): AccessibilityNodeInfo? {
if (nodeInfo == null) return null
val nodes = nodeInfo.findAccessibilityNodeInfosByText(text)
if (nodes.isNotEmpty()) {
return nodes[0]
}
return null
}
private fun findNodeByContentDescription(nodeInfo: AccessibilityNodeInfo?, contentDescription: String): AccessibilityNodeInfo? {
if (nodeInfo == null) return null
if (nodeInfo.contentDescription?.toString() == contentDescription) {
return nodeInfo
}
for (i in 0 until nodeInfo.childCount) {
val child = nodeInfo.getChild(i)
val result = findNodeByContentDescription(child, contentDescription)
if (result != null) return result
}
return null
}
override fun onInterrupt() {
Log.d(TAG, "Accessibility service interrupted")
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(urlReceiver)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment