feat: float button tile

This commit is contained in:
lisonge 2024-10-02 02:16:41 +08:00
parent 71af6d340d
commit 77f008dd42
12 changed files with 177 additions and 38 deletions

View File

@ -121,6 +121,7 @@ android {
"capture_snapshot" to "捕获快照",
"import_data" to "导入数据",
"http_server" to "HTTP服务",
"float_button" to "悬浮按钮",
).forEach {
resValue("string", it.first, it.second + "-debug")
}

View File

@ -186,6 +186,16 @@
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".debug.FloatingTileService"
android:exported="true"
android:icon="@drawable/ic_radio_button"
android:label="@string/float_button"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".debug.SnapshotActionService"

View File

@ -1,6 +1,5 @@
package li.songe.gkd.debug
import android.content.Context
import android.content.Intent
import android.view.ViewConfiguration
import androidx.compose.foundation.layout.size
@ -19,6 +18,8 @@ import li.songe.gkd.appScope
import li.songe.gkd.data.Tuple3
import li.songe.gkd.notif.floatingNotif
import li.songe.gkd.notif.notifyService
import li.songe.gkd.permission.canDrawOverlaysState
import li.songe.gkd.permission.notificationState
import li.songe.gkd.util.launchTry
import kotlin.math.sqrt
@ -88,8 +89,14 @@ class FloatingService : ExpandableBubbleService() {
companion object {
val isRunning = MutableStateFlow(false)
fun stop(context: Context = app) {
context.stopService(Intent(context, FloatingService::class.java))
fun start() {
if (!notificationState.checkOrToast()) return
if (!canDrawOverlaysState.checkOrToast()) return
app.startForegroundService(Intent(app, FloatingService::class.java))
}
fun stop() {
app.stopService(Intent(app, FloatingService::class.java))
}
}
}

View File

@ -0,0 +1,63 @@
package li.songe.gkd.debug
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import li.songe.gkd.util.OnChangeListen
import li.songe.gkd.util.OnDestroy
import li.songe.gkd.util.OnTileClick
class FloatingTileService : TileService(), OnDestroy, OnChangeListen, OnTileClick {
override fun onStartListening() {
super.onStartListening()
onStartListened()
}
override fun onClick() {
super.onClick()
onTileClicked()
}
override fun onStopListening() {
super.onStopListening()
onStopListened()
}
override fun onDestroy() {
super.onDestroy()
onDestroyed()
}
val scope = MainScope().also { scope ->
onDestroyed { scope.cancel() }
}
private val listeningFlow = MutableStateFlow(false).also { listeningFlow ->
onStartListened { listeningFlow.value = true }
onStopListened { listeningFlow.value = false }
}
init {
scope.launch {
combine(
FloatingService.isRunning,
listeningFlow
) { v1, v2 -> v1 to v2 }.collect { (running, listening) ->
if (listening) {
qsTile.state = if (running) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
}
onTileClicked {
if (FloatingService.isRunning.value) {
FloatingService.stop()
} else {
FloatingService.start()
}
}
}
}

View File

@ -41,6 +41,7 @@ import li.songe.gkd.db.DbSet
import li.songe.gkd.debug.SnapshotExt.captureSnapshot
import li.songe.gkd.notif.httpNotif
import li.songe.gkd.notif.notifyService
import li.songe.gkd.permission.notificationState
import li.songe.gkd.service.A11yService
import li.songe.gkd.util.LOCAL_HTTP_SUBS_ID
import li.songe.gkd.util.SERVER_SCRIPT_URL
@ -106,6 +107,7 @@ class HttpService : Service() {
}
fun start() {
if (!notificationState.checkOrToast()) return
app.startForegroundService(Intent(app, HttpService::class.java))
}
}

View File

@ -7,46 +7,57 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import li.songe.gkd.util.OnChangeListen
import li.songe.gkd.util.OnDestroy
import li.songe.gkd.util.OnTileClick
class HttpTileService : TileService() {
val scope = MainScope()
private val listeningFlow = MutableStateFlow(false)
override fun onCreate() {
super.onCreate()
scope.launch {
combine(
HttpService.isRunning,
listeningFlow
) { v1, v2 -> v1 to v2 }.collect { (httpRunning, listening) ->
if (listening) {
qsTile.state = if (httpRunning) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
}
}
class HttpTileService : TileService(), OnDestroy, OnChangeListen, OnTileClick {
override fun onStartListening() {
super.onStartListening()
listeningFlow.value = true
}
override fun onStopListening() {
super.onStopListening()
listeningFlow.value = false
onStartListened()
}
override fun onClick() {
super.onClick()
if (HttpService.isRunning.value) {
HttpService.stop()
} else {
HttpService.start()
}
onTileClicked()
}
override fun onStopListening() {
super.onStopListening()
onStopListened()
}
override fun onDestroy() {
super.onDestroy()
scope.cancel()
onDestroyed()
}
val scope = MainScope().also { scope ->
onDestroyed { scope.cancel() }
}
private val listeningFlow = MutableStateFlow(false).also { listeningFlow ->
onStartListened { listeningFlow.value = true }
onStopListened { listeningFlow.value = false }
}
init {
scope.launch {
combine(
HttpService.isRunning,
listeningFlow
) { v1, v2 -> v1 to v2 }.collect { (running, listening) ->
if (listening) {
qsTile.state = if (running) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
}
onTileClicked {
if (HttpService.isRunning.value) {
HttpService.stop()
} else {
HttpService.start()
}
}
}
}

View File

@ -16,6 +16,7 @@ import li.songe.gkd.appScope
import li.songe.gkd.shizuku.shizukuCheckGranted
import li.songe.gkd.util.initOrResetAppInfoCache
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.toast
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@ -31,6 +32,14 @@ class PermissionState(
fun updateAndGet(): Boolean {
return stateFlow.updateAndGet { check() }
}
fun checkOrToast(): Boolean {
updateAndGet()
if (!stateFlow.value) {
reason?.text?.let { toast(it) }
}
return stateFlow.value
}
}
private fun checkSelfPermission(permission: String): Boolean {

View File

@ -59,6 +59,7 @@ class ManageService : Service() {
val isRunning = MutableStateFlow(false)
fun start() {
if (!notificationState.checkOrToast()) return
app.startForegroundService(Intent(app, ManageService::class.java))
}

View File

@ -2,7 +2,6 @@ package li.songe.gkd.ui
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.media.projection.MediaProjectionManager
import android.os.Build
import androidx.compose.foundation.clickable
@ -53,7 +52,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.blankj.utilcode.util.LogUtils
@ -449,10 +447,9 @@ fun AdvancedPage() {
if (it) {
requiredPermission(context, notificationState)
requiredPermission(context, canDrawOverlaysState)
val intent = Intent(context, FloatingService::class.java)
ContextCompat.startForegroundService(context, intent)
FloatingService.start()
} else {
FloatingService.stop(context)
FloatingService.stop()
}
}
)

View File

@ -64,3 +64,31 @@ interface OnA11yConnected : CanOnCallback {
getCallbacks<() -> Unit>(8).forEach { it() }
}
}
interface OnChangeListen : CanOnCallback {
fun onStartListened(f: () -> Unit) {
getCallbacks<() -> Unit>(10).add(f)
}
fun onStartListened() {
getCallbacks<() -> Unit>(10).forEach { it() }
}
fun onStopListened(f: () -> Unit) {
getCallbacks<() -> Unit>(12).add(f)
}
fun onStopListened() {
getCallbacks<() -> Unit>(12).forEach { it() }
}
}
interface OnTileClick {
fun onTileClicked(f: () -> Unit) {
getCallbacks<() -> Unit>(14).add(f)
}
fun onTileClicked() {
getCallbacks<() -> Unit>(14).forEach { it() }
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#fff"
android:pathData="M480,680q83,0 141.5,-58.5T680,480q0,-83 -58.5,-141.5T480,280q-83,0 -141.5,58.5T280,480q0,83 58.5,141.5T480,680ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93ZM480,480Z" />
</vector>

View File

@ -5,4 +5,5 @@
<string name="import_data">导入数据</string>
<string name="capture_snapshot">捕获快照</string>
<string name="http_server">HTTP服务</string>
<string name="float_button">悬浮按钮</string>
</resources>