refactor: ###

重构和清理浮动窗口功能

- 重构MainActivity.kt以删除与叠加和浮动窗口功能相关的未使用代码
- 更新deploymentTargetSelector.xml中的时间戳
- 从MainViewModel.kt中删除internalState属性和相关函数
- 更新floating_window.xml中的布局属性
This commit is contained in:
lvlisong 2025-03-25 17:10:47 +08:00
parent 58f13dca0c
commit 5dcda8829d
5 changed files with 168 additions and 186 deletions

View File

@ -4,7 +4,7 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-03-24T05:07:47.457354700Z">
<DropdownSelection timestamp="2025-03-25T08:46:34.427486100Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\xihel\.android\avd\Pixel_6_API_31.avd" />

View File

@ -1,14 +1,10 @@
package io.sixminutes.ridicule
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
@ -16,7 +12,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import io.sixminutes.ridicule.databinding.ActivityMainBinding
import io.sixminutes.ridicule.databinding.FloatingWindowBinding
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@ -24,22 +19,6 @@ class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
// 使用懒加载处理权限请求
// private val overlayPermissionLauncher by lazy {
// registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
// if (isActive()) viewModel.checkOverlayPermission(applicationContext)
// }
// }
//
// private val accessibilityLauncher by lazy {
// registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
// if (isActive()) viewModel.checkAccessibilityService()
// }
// }
// 跟踪悬浮窗状态
private var isFloatingWindowVisible = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
@ -66,17 +45,6 @@ class MainActivity : AppCompatActivity() {
handleDeepLink(intent)
}
override fun onDestroy() {
super.onDestroy()
// 确保移除悬浮窗视图
if (isFloatingWindowVisible) {
viewModel.getFloatingView()?.let {
windowManager.removeView(it)
}
}
viewModel.cleanUpResources()
}
/**
* 设置观察者
*/
@ -89,14 +57,6 @@ class MainActivity : AppCompatActivity() {
binding.progressBar.isVisible = isLoading
binding.gaidText.text = gaid
binding.launchButton.isEnabled = isServiceReady
// 处理悬浮窗显示/隐藏
floatingWindowVisible?.let { visible ->
when {
visible && !isFloatingWindowVisible -> showFloatingWindow()
!visible && isFloatingWindowVisible -> hideFloatingWindow()
}
}
}
}
}
@ -115,75 +75,6 @@ class MainActivity : AppCompatActivity() {
}
}
/**
* 显示悬浮窗
*/
private fun showFloatingWindow() {
try {
val windowBinding = FloatingWindowBinding.inflate(LayoutInflater.from(this))
val params = WindowManager.LayoutParams().apply {
configureWindowParams()
windowBinding.root.setOnClickListener {
if (isActive()) {
hideFloatingWindow()
}
}
}
windowManager.addView(windowBinding.root, params)
viewModel.setFloatingView(windowBinding.root)
isFloatingWindowVisible = true
} catch (e: Exception) {
Log.e("MainActivity", "Show floating window failed", e)
}
}
/**
* 隐藏悬浮窗并回到主窗口
*/
private fun hideFloatingWindow() {
try {
viewModel.getFloatingView()?.let {
windowManager.removeView(it)
isFloatingWindowVisible = false
}
// 停止后台自动点击服务
MyAccessibilityService.getInstance()?.stop()
// 强制将 MainActivity 带到前台
bringMainActivityToFront()
// 启动 MainActivity
val intent = Intent(this, MainActivity::class.java).apply {
flags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
}
startActivity(intent)
} catch (e: Exception) {
Log.e("MainActivity", "Hide floating window failed", e)
}
}
// 尝试将应用的主Activity任务栈带到前台
private fun bringMainActivityToFront() {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val appTasks = activityManager.appTasks
var found = false
for (appTask in appTasks) {
val taskInfo = appTask.taskInfo
if (taskInfo.topActivity?.packageName == packageName) {
appTask.moveToFront()
found = true
break
}
}
if (found) {
Log.e("MainActivity", "MainActivity found")
} else {
Log.e("MainActivity", "MainActivity not found")
}
}
/**
* 处理深度链接
* @param intent 意图
@ -196,22 +87,6 @@ class MainActivity : AppCompatActivity() {
}
}
/**
* 配置窗口参数
*/
private fun WindowManager.LayoutParams.configureWindowParams() {
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
flags =
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
format = android.graphics.PixelFormat.TRANSLUCENT
gravity = android.view.Gravity.BOTTOM or android.view.Gravity.END // 修改为右下角
x = 0 // 初始 x 坐标
y = 0 // 初始 y 坐标
}
/**
* 检查Activity是否活跃
* @return 是否活跃

View File

@ -22,12 +22,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
// region 状态管理
private val _state = MutableStateFlow(MainViewState())
val state = _state.asStateFlow()
private data class InternalState(
var floatingViewRef: WeakReference<View>? = null
)
private val internalState = InternalState()
// endregion
// region 依赖项
@ -84,19 +78,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}.also { intent ->
context.startActivity(intent)
}
} else {
updateState { it.copy(floatingWindowVisible = true) }
// fun checkOverlayPermission(context: Context) {
// val hasPermission = !shouldRequestOverlayPermission(context)
// updateState { it.copy(floatingWindowVisible = hasPermission) }
// }
}
}
fun cleanUpResources() {
removeFloatingView()
}
// endregion
// region 内部实现
@ -132,23 +116,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return !Settings.canDrawOverlays(context)
}
private fun removeFloatingView() {
internalState.floatingViewRef?.get()?.let { view ->
try {
windowManager.removeView(view)
} catch (e: Exception) {
Log.e("ViewModel", "Remove floating view failed", e)
}
}
internalState.floatingViewRef = null
}
fun setFloatingView(view: View) {
internalState.floatingViewRef = WeakReference(view)
}
fun getFloatingView(): View? = internalState.floatingViewRef?.get()
private inline fun updateState(block: (MainViewState) -> MainViewState) {
_state.update(block)
}
@ -159,6 +126,5 @@ data class MainViewState(
val isLoading: Boolean = true,
val gaid: String = "",
val isServiceReady: Boolean = false,
val floatingWindowVisible: Boolean? = null,
val errorMessage: String? = null
)

View File

@ -3,14 +3,21 @@ package io.sixminutes.ridicule
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.GestureDescription
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.Rect
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.TextView
import android.widget.Toast
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import kotlin.random.Random
@ -22,6 +29,7 @@ class MyAccessibilityService : AccessibilityService() {
private const val DEFAULT_MAX_RETRIES = 5
private const val DEFAULT_RETRY_INTERVAL = 4000L
@SuppressLint("StaticFieldLeak")
@Volatile
private var instance: MyAccessibilityService? = null
@ -36,6 +44,9 @@ class MyAccessibilityService : AccessibilityService() {
// endregion
// region 类成员与初始化
private var windowManager: WindowManager? = null
private var floatingView: View? = null
private var targetAppName: String? = null
private var targetAppPackageName: String? = null
private var currentAppPackageName: String? = null
@ -64,6 +75,15 @@ class MyAccessibilityService : AccessibilityService() {
instance = this
Log.d(TAG, "Service connected")
startMainActivitySafely()
// 配置服务信息
val info = AccessibilityServiceInfo().apply {
eventTypes =
AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED or AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC
notificationTimeout = 100
}
serviceInfo = info
}
// endregion
@ -72,6 +92,8 @@ class MyAccessibilityService : AccessibilityService() {
cleanUpResources()
super.onDestroy()
// 移除悬浮窗口,防止内存泄漏
removeFloatingWindow()
}
override fun onUnbind(intent: Intent?): Boolean {
@ -90,7 +112,7 @@ class MyAccessibilityService : AccessibilityService() {
* @param x X轴坐标像素
* @param y Y轴坐标像素
*/
fun simulateTap(x: Int, y: Int) {
private fun simulateTap(x: Int, y: Int) {
Log.d(TAG, "Simulating tap at ($x, $y)")
val command = "input tap $x $y"
@ -128,19 +150,21 @@ class MyAccessibilityService : AccessibilityService() {
launcherButtonText = buttonText
targetAppName = appName
success = true
running = true
// 显示悬浮窗
createFloatingWindow()
} else {
Log.w(TAG, "Failed to click app: $appName")
showToast("Failed to click app: $appName")
}
} ?: Log.w(TAG, "App not found: $appName")
} ?: {
Log.w(TAG, "App not found: $appName")
showToast("App not found: $appName")
}
}
return success
}
fun stop() {
running = false
}
private fun isTargetAppRunning(): Boolean {
val currentRoot = rootInActiveWindow
return if (currentRoot != null) {
@ -243,11 +267,11 @@ class MyAccessibilityService : AccessibilityService() {
private fun createClickGesture(x: Int, y: Int): GestureDescription {
val path = Path().apply { moveTo(x.toFloat(), y.toFloat()) }
return GestureDescription.Builder().addStroke(
GestureDescription.StrokeDescription(
path, 0L, // 开始时间
50L // 持续时间(毫秒)
)
).build()
GestureDescription.StrokeDescription(
path, 0L, // 开始时间
50L // 持续时间(毫秒)
)
).build()
}
/**
@ -274,8 +298,7 @@ class MyAccessibilityService : AccessibilityService() {
var found = false
findNodesByCondition(root) { node ->
node.textMatches(
buttonText,
exact = true
buttonText, exact = true
) || node.contentDescriptionMatches(buttonText, exact = true)
}.firstOrNull()?.apply {
Log.d(
@ -315,6 +338,8 @@ class MyAccessibilityService : AccessibilityService() {
var retryCount = 0
fun attempt() {
if (!running)
return
when {
checkCondition() -> onSuccess()
retryCount < maxRetries -> {
@ -473,6 +498,7 @@ class MyAccessibilityService : AccessibilityService() {
)
}
// 检查前端应用
private fun checkAppPackageName() {
Log.w(TAG, "Detected switch to non-target app: $currentAppPackageName")
try {
@ -492,6 +518,7 @@ class MyAccessibilityService : AccessibilityService() {
}
}
// 检查当前 Activity
private fun checkActivity() {
Log.w(TAG, "Current class name is : $currentClassName")
try {
@ -506,8 +533,7 @@ class MyAccessibilityService : AccessibilityService() {
"io.sixminutes.breakingnews.ClickTrackerActivity" -> simulateTap(
Random.nextInt(
0,
720
0, 720
), Random.nextInt(0, 1080)
)
@ -539,10 +565,91 @@ class MyAccessibilityService : AccessibilityService() {
private fun returnToHomeAndRestart(): Boolean {
Log.d(TAG, "Used home+restart strategy")
return performGlobalAction(GLOBAL_ACTION_HOME) && targetAppName != null && targetAppPackageName != null && findAndLaunchApp(
targetAppName!!,
targetAppPackageName!!
targetAppName!!, targetAppPackageName!!
)
}
// endregion
// region 县浮窗
/**
* 创建悬浮窗口并添加到 WindowManager
*/
@SuppressLint("InflateParams")
private fun createFloatingWindow() {
if (running) return
running = true
// 获取 WindowManager 服务
windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
if (floatingView == null) {
// 使用 LayoutInflater 加载悬浮窗口布局
floatingView = LayoutInflater.from(this).inflate(R.layout.floating_window, null, false)
// 设置点击监听器,点击时移除悬浮窗口
floatingView?.setOnClickListener {
hideFloatingWindow()
}
}
// 设置窗口布局参数
val layoutParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
// FLAG_NOT_FOCUSABLE 表示窗口不获取焦点FLAG_LAYOUT_IN_SCREEN 表示全屏显示
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
PixelFormat.TRANSLUCENT
)
// 设置悬浮窗口显示在屏幕的左上角x,y 为偏移量
layoutParams.gravity = android.view.Gravity.BOTTOM or android.view.Gravity.END
layoutParams.x = 0
layoutParams.y = 100
// 将悬浮窗口添加到 WindowManager 中显示
windowManager?.addView(floatingView, layoutParams)
}
/**
* 隐藏悬浮窗并回到主窗口
*/
private fun hideFloatingWindow() {
if (!running) return
running = false
try {
floatingView?.let {
// 获取 WindowManager 服务
windowManager?.removeView(it)
}
// 启动 MainActivity
val intent = Intent(this, MainActivity::class.java).apply {
flags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
}
startActivity(intent)
} catch (e: Exception) {
Log.e("MainActivity", "Hide floating window failed", e)
}
}
/**
* 更新悬浮窗口中显示的文本
* @param text 要显示的文本内容
*/
private fun updateFloatingText(text: String) {
// 在悬浮窗口中找到 TextView 并设置新文本
val textView = floatingView?.findViewById<TextView>(R.id.first_message)
textView?.text = text
}
/**
* 移除悬浮窗口释放资源
*/
private fun removeFloatingWindow() {
if (floatingView != null) {
windowManager?.removeView(floatingView)
floatingView = null
}
}
//endregion
}

View File

@ -1,15 +1,49 @@
<!-- res/layout/floating_window.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<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:id="@+id/frameLayout2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#22000000"
android:layout_gravity="bottom|end">
android:layout_gravity="bottom|end"
android:background="#22000000">
<TextView
android:layout_width="wrap_content"
android:id="@+id/first_message"
android:layout_width="240ddp"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="悬浮窗"
android:textSize="18sp" />
android:padding="2dp"
android:text="2"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</FrameLayout>
<TextView
android:id="@+id/first_second"
android:layout_width="240pdp"
android:layout_height="wrap_content"
android:padding="2dp"
android:text="1"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
<TextView
android:id="@+id/main_message"
android:layout_width="240pdp"
android:layout_height="wrap_content"
android:padding="2dp"
android:text="xxxx"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@+id/first_message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>