引入库:
[versions]
lifecycleProcess = "2.8.3"
[libraries]
androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycleProcess" }
App:
import android.app.Application
import androidx.lifecycle.ProcessLifecycleOwner
class App : Application() {
companion object {
private const val TAG = "App"
}
override fun onCreate() {
super.onCreate()
init()
}
private fun init() {
// 监听Activity数量变化
registerActivityLifecycleCallbacks(AppLifecycleMonitor())
// 注册生命周期监听
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
}
}
AppLifecycleMonitor:
import android.app.Activity
import android.app.Application
import android.os.Bundle
import android.util.Log
import com.small.ball.assistant.ScreenEventManager
import java.util.concurrent.atomic.AtomicInteger
class AppLifecycleMonitor : Application.ActivityLifecycleCallbacks {
/**
* 前台可见的 Activity 数量
*/
private val foregroundCount = AtomicInteger(0)
companion object {
private const val TAG = "AppLifecycleTracker"
}
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?
) {
Log.d(TAG, "onActivityCreated: ${foregroundCount.get()}")
}
override fun onActivityStarted(activity: Activity) {
Log.d(TAG, "onActivityStarted before: ${foregroundCount.get()}")
foregroundCount.incrementAndGet()
Log.d(TAG, "onActivityStarted after: ${foregroundCount.get()}")
if (foregroundCount.get() == 1) {
Log.d(TAG, "onActivityStarted: 应用从后台切到前台")
ScreenEventManager.onAppForeground()
}
}
override fun onActivityResumed(activity: Activity) {
Log.d(TAG, "onActivityResumed: ${foregroundCount.get()}")
}
override fun onActivityPaused(activity: Activity) {
Log.d(TAG, "onActivityPaused: ${foregroundCount.get()}")
}
override fun onActivityStopped(activity: Activity) {
Log.d(TAG, "onActivityStopped before: ${foregroundCount.get()}")
foregroundCount.decrementAndGet()
Log.d(TAG, "onActivityStopped after: ${foregroundCount.get()}")
if (foregroundCount.get() == 0) {
Log.d(TAG, "onActivityStopped: 应用从前台切到后台")
ScreenEventManager.onAppBackground()
}
}
override fun onActivitySaveInstanceState(
activity: Activity,
outState: Bundle
) {
Log.d(TAG, "onActivitySaveInstanceState: ${foregroundCount.get()}")
}
override fun onActivityDestroyed(activity: Activity) {
Log.d(TAG, "onActivityDestroyed: ${foregroundCount.get()}")
}
}
AppLifecycleObserver:
import android.util.Log
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.small.ball.assistant.ScreenEventManager
class AppLifecycleObserver : DefaultLifecycleObserver {
companion object {
private const val TAG = "AppLifecycleObserver"
}
override fun onStart(owner: LifecycleOwner) {
Log.d(TAG, "App entered foreground")
ScreenEventManager.onAppForeground()
}
override fun onStop(owner: LifecycleOwner) {
Log.d(TAG, "App entered background")
ScreenEventManager.onAppBackground()
}
}
BorrowId:
enum class BorrowId {
UNKNOWN,
CAMERA,
STATUS_BAR,
AIR_CONDITIONER,
}
ScreenState:
enum class ScreenState {
UI,
BORROW,
TAKE
}
ScreenStateManager:
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicReference
object ScreenStateManager {
private val currentState = AtomicReference(ScreenState.UI)
private val borrowPool = mutableListOf<BorrowId>()
private val borrowScope = CoroutineScope(Dispatchers.IO)
private const val TAG = "ScreenStateManager"
fun changeState(
newState: ScreenState,
borrowId: BorrowId = BorrowId.UNKNOWN
) = synchronized(this) {
Log.d(TAG, "changeState: old=${currentState.get()}, new=$newState, borrowId=$borrowId")
when (newState) {
ScreenState.UI -> {
if (currentState.get() == ScreenState.TAKE) {
currentState.set(newState)
Log.d(TAG, "changeState: TAKE->UI => requestUI()")
} else if (currentState.get() == ScreenState.BORROW) {
borrowScope.launch {
delay(200)
val remove = borrowPool.remove(borrowId)
if (remove) {
Log.d(TAG, "changeState: BORROW->UI => unBorrow($borrowId)")
}
}
}
}
ScreenState.BORROW -> {
if (currentState.get() == ScreenState.UI || currentState.get() == ScreenState.BORROW) {
if (!borrowPool.contains(borrowId)) {
currentState.set(newState)
borrowPool.add(borrowId)
Log.d(TAG, "changeState borrow($borrowId)")
}
}
}
ScreenState.TAKE -> {
if (currentState.get() == ScreenState.UI) {
currentState.set(newState)
Log.d(TAG, "changeState: UI->TAKE => take()")
} else if (currentState.get() == ScreenState.BORROW) {
borrowScope.cancel()
currentState.set(newState)
// 是否清空 borrowPool?
borrowPool.clear()
Log.d(TAG, "changeState: BORROW->TAKE => take()")
}
}
}
}
}
ScreenEventManager:
import android.util.Log
import java.util.concurrent.atomic.AtomicBoolean
object ScreenEventManager {
private val popupShow = AtomicBoolean(false)
private val statusBarShow = AtomicBoolean(false)
private const val TAG = "ScreenEventManager"
/**
* 弹窗出现
*/
fun onPopupShow() {
Log.d(TAG, "onPopupShow")
popupShow.set(true)
ScreenStateManager.changeState(ScreenState.BORROW, BorrowId.CAMERA)
}
/**
* 弹窗消失
*/
fun onPopupHide() {
Log.d(TAG, "onPopupHide")
if (popupShow.get()) {
ScreenStateManager.changeState(ScreenState.UI, BorrowId.CAMERA)
popupShow.set(false)
}
}
/**
* 状态栏出现
*/
fun onStatusBarShow() {
Log.d(TAG, "onStatusBarShow")
statusBarShow.set(true)
ScreenStateManager.changeState(ScreenState.BORROW, BorrowId.STATUS_BAR)
}
/**
* 状态栏消失
*/
fun onStatusBarHide() {
Log.d(TAG, "onStatusBarHide")
if (statusBarShow.get()) {
ScreenStateManager.changeState(ScreenState.UI, BorrowId.STATUS_BAR)
statusBarShow.set(false)
}
}
/**
* 应用进入前台
*/
fun onAppForeground() {
Log.d(TAG, "onAppForeground")
ScreenStateManager.changeState(ScreenState.UI)
}
/**
* 应用进入后台
*/
fun onAppBackground() {
Log.d(TAG, "onAppBackground")
ScreenStateManager.changeState(ScreenState.TAKE)
}
}
AccessibilityNodeInfo:
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityWindowInfo
fun AccessibilityNodeInfo.fmt() = StringBuilder().apply {
append("屏幕ID: ${window.displayId}\n")
append("是否激活: ${window.isActive}\n")
append("窗口标题: ${window.title}\n")
append("窗口类型: ${window.type}\n")
append("窗口类型Display: ${typeToString(window.type)}\n")
append("窗口层级: ${window.layer}\n")
append("是否可用: $isEnabled\n")
append("是否可点击: $isClickable\n")
append("是否有焦点: $isFocused\n")
append("是否可关闭: $isDismissable\n")
}
private fun typeToString(type: Int): String {
when (type) {
AccessibilityWindowInfo.TYPE_APPLICATION -> {
return "TYPE_APPLICATION"
}
AccessibilityWindowInfo.TYPE_INPUT_METHOD -> {
return "TYPE_INPUT_METHOD"
}
AccessibilityWindowInfo.TYPE_SYSTEM -> {
return "TYPE_SYSTEM"
}
AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY -> {
return "TYPE_ACCESSIBILITY_OVERLAY"
}
AccessibilityWindowInfo.TYPE_SPLIT_SCREEN_DIVIDER -> {
return "TYPE_SPLIT_SCREEN_DIVIDER"
}
AccessibilityWindowInfo.TYPE_MAGNIFICATION_OVERLAY -> {
return "TYPE_MAGNIFICATION_OVERLAY"
}
/*AccessibilityWindowInfo.FLAG_ADD_TYPE_WINDOW_CONTROL -> {
return "FLAG_ADD_TYPE_WINDOW_CONTROL" }*/
else -> {
return "UNKNOWN"
}
}
}
AccessibilityEvent:
import android.os.Build
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED
import android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_ACTIVE
import android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_ADDED
import android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_BOUNDS
import android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_CHILDREN
import android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_FOCUSED
import android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_LAYER
import android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_PARENT
import android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_PIP
import android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_REMOVED
import android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_TITLE
fun AccessibilityEvent.fmt() = StringBuilder().apply {
append("事件时间: $eventTime\n")
append("应用的包名: $packageName\n")
append("应用的类名: $className\n")
append("事件类型: $eventType\n")
append("窗口标识符: $windowId\n")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
append("窗口屏幕标识符: $displayId\n")
}
append("窗口是否全屏: $isFullScreen\n")
append("窗口源: ${source}\n")
if (source != null) {
append("${source?.fmt()}")
}
append("窗口 WindowChanges: $windowChanges\n")
append("窗口 WindowChangesDisplay: ${windowChangesDisplay(windowChanges)}\n")
// append("事件遍历的颗粒度: $movementGranularity\n")
append("窗口 Action: $action\n")
/*append("密封数量: $recordCount\n")
if (recordCount > 0) { append("密封对象: ${getRecord(0)}\n")
}*/ // append("内容变化类型的位掩码: $contentChangeTypes\n")
// append("语音变化类型的位掩码: $speechStateChangeTypes\n")
// TYPE_WINDOWS_CHANGED}
private fun windowChangesDisplay(type: Int): String {
when (type) {
WINDOWS_CHANGE_ADDED -> {
return "WINDOWS_CHANGE_ADDED"
}
WINDOWS_CHANGE_REMOVED -> {
return "WINDOWS_CHANGE_REMOVED"
}
WINDOWS_CHANGE_TITLE -> {
return "WINDOWS_CHANGE_TITLE"
}
WINDOWS_CHANGE_BOUNDS -> {
return "WINDOWS_CHANGE_BOUNDS"
}
WINDOWS_CHANGE_LAYER -> {
return "WINDOWS_CHANGE_LAYER"
}
WINDOWS_CHANGE_ACTIVE -> {
return "WINDOWS_CHANGE_ACTIVE"
}
WINDOWS_CHANGE_FOCUSED -> {
return "WINDOWS_CHANGE_FOCUSED"
}
WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED -> {
return "WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED"
}
WINDOWS_CHANGE_PARENT -> {
return "WINDOWS_CHANGE_PARENT"
}
WINDOWS_CHANGE_CHILDREN -> {
return "WINDOWS_CHANGE_CHILDREN"
}
WINDOWS_CHANGE_PIP -> {
return "WINDOWS_CHANGE_PIP"
}
else -> {
return "UNKNOWN"
}
}
}
OverlayService:
import android.accessibilityservice.AccessibilityService
import android.content.Intent
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityWindowInfo
class OverlayService : AccessibilityService() {
companion object {
private const val TAG = "OverlayService"
}
override fun onServiceConnected() {
super.onServiceConnected()
Log.d(TAG, "onServiceConnected: run")
}
/**
* * 状态栏展开:
* 应用的包名: com.android.systemui
* 应用的类名: android.widget.FrameLayout
* 窗口类型: 3
* 窗口类型Display: TYPE_SYSTEM
* 窗口层级: 0
*/ override fun onAccessibilityEvent(event: AccessibilityEvent) {
// Log.d(TAG, "onAccessibilityEvent: run")
when (event.eventType) {
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
handlerWindowStateChanged(event)
}
AccessibilityEvent.TYPE_WINDOWS_CHANGED -> {
handlerWindowsChanged(event)
}
else -> {}
}
}
override fun onInterrupt() {
Log.d(TAG, "onInterrupt: run")
}
override fun onUnbind(intent: Intent?): Boolean {
return super.onUnbind(intent)
Log.d(TAG, "onUnbind: run")
}
private fun handlerWindowStateChanged(event: AccessibilityEvent) {
Log.d(TAG, "****** handlerWindowStateChanged ******")
Log.d(TAG, event.fmt().toString())
Log.d(TAG, "windows size: ${windows.size}")
if (windows.isNotEmpty()) {
Log.d(TAG, "last window: ${windows[windows.size - 1].id}")
}
if (event.source == null) {
Log.d(TAG, "source is null")
return
}
val pkgName = event.packageName
val clsName = event.className
// 弹窗
if (clsName!!.contains("Dialog")) {
ScreenEventManager.onPopupShow()
return
}
// 状态栏
if (pkgName == "com.android.systemui" && clsName == "android.widget.FrameLayout") {
ScreenEventManager.onStatusBarShow()
return
}
// 判断本应用 Activity 切换回前台:若包名为本应用,且窗口为 Activity,则认为弹窗消失或遮挡消失
if (packageName == packageName && clsName.contains("Activity")) {
// 如果此前有弹窗/其他窗口遮挡状态,均视为消失
ScreenEventManager.onPopupHide()
ScreenEventManager.onStatusBarHide()
return
}
// 判断其他应用切换到前台(新窗口出现)
if (packageName != packageName && packageName != "com.android.systemui") {
// 其他应用窗口成为前台 Activity // ScreenEventManager.onOtherWindowShown() return
}
}
private fun handlerWindowsChanged(event: AccessibilityEvent) {
Log.d(TAG, "====== handlerWindowsChanged ======")
Log.d(TAG, event.fmt().toString())
Log.d(TAG, "windows size: ${windows.size}")
if (windows.isNotEmpty()) {
Log.d(TAG, "last window: ${windows[windows.size - 1].id}")
}
// 以包名与 windowType 区分状态栏、悬浮窗、应用窗口等
var statusBarFound = false
var otherWindowFound = false
windows.forEach {
when (it.type) {
AccessibilityWindowInfo.TYPE_SYSTEM -> {}
AccessibilityWindowInfo.TYPE_APPLICATION -> {}
}
}
}
}