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> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <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"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\xihel\.android\avd\Pixel_6_API_31.avd" /> <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 package io.sixminutes.ridicule
import android.app.ActivityManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.WindowManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -16,7 +12,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import io.sixminutes.ridicule.databinding.ActivityMainBinding import io.sixminutes.ridicule.databinding.ActivityMainBinding
import io.sixminutes.ridicule.databinding.FloatingWindowBinding
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -24,22 +19,6 @@ class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels() 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
@ -66,17 +45,6 @@ class MainActivity : AppCompatActivity() {
handleDeepLink(intent) 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.progressBar.isVisible = isLoading
binding.gaidText.text = gaid binding.gaidText.text = gaid
binding.launchButton.isEnabled = isServiceReady 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 意图 * @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是否活跃 * 检查Activity是否活跃
* @return 是否活跃 * @return 是否活跃

View File

@ -22,12 +22,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
// region 状态管理 // region 状态管理
private val _state = MutableStateFlow(MainViewState()) private val _state = MutableStateFlow(MainViewState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
private data class InternalState(
var floatingViewRef: WeakReference<View>? = null
)
private val internalState = InternalState()
// endregion // endregion
// region 依赖项 // region 依赖项
@ -84,19 +78,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}.also { intent -> }.also { intent ->
context.startActivity(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 // endregion
// region 内部实现 // region 内部实现
@ -132,23 +116,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return !Settings.canDrawOverlays(context) 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) { private inline fun updateState(block: (MainViewState) -> MainViewState) {
_state.update(block) _state.update(block)
} }
@ -159,6 +126,5 @@ data class MainViewState(
val isLoading: Boolean = true, val isLoading: Boolean = true,
val gaid: String = "", val gaid: String = "",
val isServiceReady: Boolean = false, val isServiceReady: Boolean = false,
val floatingWindowVisible: Boolean? = null,
val errorMessage: String? = null val errorMessage: String? = null
) )

View File

@ -3,14 +3,21 @@ package io.sixminutes.ridicule
import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.GestureDescription import android.accessibilityservice.GestureDescription
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Path import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.Rect import android.graphics.Rect
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log 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.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import kotlin.random.Random import kotlin.random.Random
@ -22,6 +29,7 @@ class MyAccessibilityService : AccessibilityService() {
private const val DEFAULT_MAX_RETRIES = 5 private const val DEFAULT_MAX_RETRIES = 5
private const val DEFAULT_RETRY_INTERVAL = 4000L private const val DEFAULT_RETRY_INTERVAL = 4000L
@SuppressLint("StaticFieldLeak")
@Volatile @Volatile
private var instance: MyAccessibilityService? = null private var instance: MyAccessibilityService? = null
@ -36,6 +44,9 @@ class MyAccessibilityService : AccessibilityService() {
// endregion // endregion
// region 类成员与初始化 // region 类成员与初始化
private var windowManager: WindowManager? = null
private var floatingView: View? = null
private var targetAppName: String? = null private var targetAppName: String? = null
private var targetAppPackageName: String? = null private var targetAppPackageName: String? = null
private var currentAppPackageName: String? = null private var currentAppPackageName: String? = null
@ -64,6 +75,15 @@ class MyAccessibilityService : AccessibilityService() {
instance = this instance = this
Log.d(TAG, "Service connected") Log.d(TAG, "Service connected")
startMainActivitySafely() 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 // endregion
@ -72,6 +92,8 @@ class MyAccessibilityService : AccessibilityService() {
cleanUpResources() cleanUpResources()
super.onDestroy() super.onDestroy()
// 移除悬浮窗口,防止内存泄漏
removeFloatingWindow()
} }
override fun onUnbind(intent: Intent?): Boolean { override fun onUnbind(intent: Intent?): Boolean {
@ -90,7 +112,7 @@ class MyAccessibilityService : AccessibilityService() {
* @param x X轴坐标像素 * @param x X轴坐标像素
* @param y Y轴坐标像素 * @param y Y轴坐标像素
*/ */
fun simulateTap(x: Int, y: Int) { private fun simulateTap(x: Int, y: Int) {
Log.d(TAG, "Simulating tap at ($x, $y)") Log.d(TAG, "Simulating tap at ($x, $y)")
val command = "input tap $x $y" val command = "input tap $x $y"
@ -128,19 +150,21 @@ class MyAccessibilityService : AccessibilityService() {
launcherButtonText = buttonText launcherButtonText = buttonText
targetAppName = appName targetAppName = appName
success = true success = true
running = true
// 显示悬浮窗
createFloatingWindow()
} else { } else {
Log.w(TAG, "Failed to click app: $appName") 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 return success
} }
fun stop() {
running = false
}
private fun isTargetAppRunning(): Boolean { private fun isTargetAppRunning(): Boolean {
val currentRoot = rootInActiveWindow val currentRoot = rootInActiveWindow
return if (currentRoot != null) { return if (currentRoot != null) {
@ -243,11 +267,11 @@ class MyAccessibilityService : AccessibilityService() {
private fun createClickGesture(x: Int, y: Int): GestureDescription { private fun createClickGesture(x: Int, y: Int): GestureDescription {
val path = Path().apply { moveTo(x.toFloat(), y.toFloat()) } val path = Path().apply { moveTo(x.toFloat(), y.toFloat()) }
return GestureDescription.Builder().addStroke( return GestureDescription.Builder().addStroke(
GestureDescription.StrokeDescription( GestureDescription.StrokeDescription(
path, 0L, // 开始时间 path, 0L, // 开始时间
50L // 持续时间(毫秒) 50L // 持续时间(毫秒)
) )
).build() ).build()
} }
/** /**
@ -274,8 +298,7 @@ class MyAccessibilityService : AccessibilityService() {
var found = false var found = false
findNodesByCondition(root) { node -> findNodesByCondition(root) { node ->
node.textMatches( node.textMatches(
buttonText, buttonText, exact = true
exact = true
) || node.contentDescriptionMatches(buttonText, exact = true) ) || node.contentDescriptionMatches(buttonText, exact = true)
}.firstOrNull()?.apply { }.firstOrNull()?.apply {
Log.d( Log.d(
@ -315,6 +338,8 @@ class MyAccessibilityService : AccessibilityService() {
var retryCount = 0 var retryCount = 0
fun attempt() { fun attempt() {
if (!running)
return
when { when {
checkCondition() -> onSuccess() checkCondition() -> onSuccess()
retryCount < maxRetries -> { retryCount < maxRetries -> {
@ -473,6 +498,7 @@ class MyAccessibilityService : AccessibilityService() {
) )
} }
// 检查前端应用
private fun checkAppPackageName() { private fun checkAppPackageName() {
Log.w(TAG, "Detected switch to non-target app: $currentAppPackageName") Log.w(TAG, "Detected switch to non-target app: $currentAppPackageName")
try { try {
@ -492,6 +518,7 @@ class MyAccessibilityService : AccessibilityService() {
} }
} }
// 检查当前 Activity
private fun checkActivity() { private fun checkActivity() {
Log.w(TAG, "Current class name is : $currentClassName") Log.w(TAG, "Current class name is : $currentClassName")
try { try {
@ -506,8 +533,7 @@ class MyAccessibilityService : AccessibilityService() {
"io.sixminutes.breakingnews.ClickTrackerActivity" -> simulateTap( "io.sixminutes.breakingnews.ClickTrackerActivity" -> simulateTap(
Random.nextInt( Random.nextInt(
0, 0, 720
720
), Random.nextInt(0, 1080) ), Random.nextInt(0, 1080)
) )
@ -539,10 +565,91 @@ class MyAccessibilityService : AccessibilityService() {
private fun returnToHomeAndRestart(): Boolean { private fun returnToHomeAndRestart(): Boolean {
Log.d(TAG, "Used home+restart strategy") Log.d(TAG, "Used home+restart strategy")
return performGlobalAction(GLOBAL_ACTION_HOME) && targetAppName != null && targetAppPackageName != null && findAndLaunchApp( return performGlobalAction(GLOBAL_ACTION_HOME) && targetAppName != null && targetAppPackageName != null && findAndLaunchApp(
targetAppName!!, targetAppName!!, targetAppPackageName!!
targetAppPackageName!!
) )
} }
// endregion // 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 --> <!-- 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_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="#22000000" android:layout_gravity="bottom|end"
android:layout_gravity="bottom|end"> android:background="#22000000">
<TextView <TextView
android:layout_width="wrap_content" android:id="@+id/first_message"
android:layout_width="240ddp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="16dp" android:padding="2dp"
android:text="悬浮窗" android:text="2"
android:textSize="18sp" /> 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>