初始化项目
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,68 @@
|
||||
import org.gradle.kotlin.dsl.implementation
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.bbitcn.phone"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.bbitcn.phone"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
implementation( "org.java-websocket:Java-WebSocket:1.6.0")
|
||||
//lifecycle-viewmodel-compose
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
//MMKV
|
||||
implementation("com.tencent:mmkv:2.2.2")
|
||||
implementation("com.google.code.gson:gson:2.13.1")
|
||||
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.bbitcn.phone
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.bbitcn.phone", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/logo"
|
||||
android:label="@string/app_name"
|
||||
android:name=".MyApp"
|
||||
android:roundIcon="@mipmap/logo"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.BBITShow">
|
||||
<activity
|
||||
android:name=".ui.PhoneActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.BBITShow">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.bbitcn.phone
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bbitcn.phone.utls.MMKVUtil
|
||||
|
||||
|
||||
/**
|
||||
* @Description APPLICATION类
|
||||
* @Author DuanKaiji
|
||||
* @CreateTime 2024年03月27日 13:43
|
||||
*/
|
||||
val M = Modifier
|
||||
val MD = Modifier
|
||||
.animateContentSize(
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class MyApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// 初始化MMKV
|
||||
MMKVUtil.init(applicationContext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.bbitcn.sericulture.base
|
||||
|
||||
import com.bbitcn.phone.utls.MMKVUtil
|
||||
import com.google.gson.Gson
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
*
|
||||
* @Description 列表临时存储库
|
||||
* @Author DuanKaiji
|
||||
* @CreateTime 2024年08月06日 11:39:31
|
||||
*/
|
||||
abstract class BaseListTempDataBase<T> {
|
||||
protected val gson: Gson = Gson()
|
||||
|
||||
abstract fun getKey(): String
|
||||
|
||||
abstract fun getType(): Type
|
||||
|
||||
fun getAll(): MutableList<T> {
|
||||
val json = MMKVUtil.get(getKey(), "[]")
|
||||
return Gson().fromJson(json, getType())
|
||||
}
|
||||
|
||||
suspend fun init(data: List<T>) {
|
||||
MMKVUtil.put(getKey(), gson.toJson(data))
|
||||
}
|
||||
|
||||
suspend fun insert(t: T, afterInsert: (T) -> Unit = {}): Boolean {
|
||||
// val type: Type = ParameterizedTypeImpl(T::class.java)
|
||||
val temp: MutableList<T> = getAll()
|
||||
if (temp.contains(t)) {
|
||||
return false
|
||||
}
|
||||
temp.add(t)
|
||||
MMKVUtil.put(getKey(), gson.toJson(temp))
|
||||
afterInsert(t)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*/
|
||||
fun update(predicate: (T) -> Boolean, update: (T) -> Unit = {}, afterUpdate: () -> Unit = {}): Boolean {
|
||||
val temp: MutableList<T> = getAll()
|
||||
for (mode in temp) {
|
||||
if (predicate(mode)) {
|
||||
update(mode)
|
||||
MMKVUtil.put(getKey(), gson.toJson(temp))
|
||||
afterUpdate()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun delete(predicate: (T) -> Boolean,afterDel: (T) -> Unit = { }): Boolean {
|
||||
val temp: MutableList<T> = getAll()
|
||||
val iterator = temp.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val mode = iterator.next()
|
||||
if (predicate(mode)) {
|
||||
iterator.remove()
|
||||
MMKVUtil.put(getKey(), gson.toJson(temp))
|
||||
afterDel(mode)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
fun clear(): Boolean {
|
||||
MMKVUtil.put(getKey(), "[]")
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.bbitcn.phone.base
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bbitcn.phone.utls.PollingTask
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.SocketException
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
open class BaseViewModel : ViewModel() {
|
||||
|
||||
|
||||
protected var pollingTask: PollingTask =
|
||||
PollingTask.Companion.getInstance(javaClass.simpleName) // 使用类名作为ID
|
||||
|
||||
protected fun doInUIThread(task: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
task()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun <T> doInIoThreadThenUI(
|
||||
loadingTips: String = "正在加载中",
|
||||
showDialog: Boolean = true,
|
||||
onError: (Throwable) -> Unit = { },
|
||||
onIO: suspend () -> T,
|
||||
onFinish: () -> Unit = { },
|
||||
onUI: (T) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val result = runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (showDialog) {
|
||||
// Toasty.showLoadingDialog(loadingTips)
|
||||
}
|
||||
onIO()
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
// hideLoadingDialog()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
result.onSuccess { data ->
|
||||
onUI(data)
|
||||
}.onFailure { exception ->
|
||||
// ✅ 如果是协程取消,不处理,只记录日志
|
||||
if (exception is CancellationException || exception.cause is SocketException && exception.cause?.message?.contains(
|
||||
"Socket closed"
|
||||
) == true
|
||||
) {
|
||||
return@onFailure
|
||||
}
|
||||
// 其他异常继续处理
|
||||
exception.printStackTrace()
|
||||
onError(exception)
|
||||
exception.message?.let {
|
||||
// Toasty.error(it)
|
||||
}
|
||||
}.also {
|
||||
// ✅ 最终执行的操作
|
||||
onFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun <T> doInIoThread(
|
||||
loadingTips: String = "正在加载中",
|
||||
showDialog: Boolean = true,
|
||||
onError: (Throwable) -> Unit = { },
|
||||
onFinish: () -> Unit = { },
|
||||
doInIO: suspend () -> T,
|
||||
) {
|
||||
doInIoThreadThenUI(loadingTips, showDialog, onError, doInIO, onFinish) { }
|
||||
}
|
||||
|
||||
fun <T> doInIoThreadNoDialog(
|
||||
onError: (Throwable) -> Unit = { },
|
||||
task: suspend () -> T,
|
||||
) {
|
||||
doInIoThread(showDialog = false, doInIO = task, onError = onError)
|
||||
}
|
||||
|
||||
/**
|
||||
* 在IO线程中执行任务,可选择是否显示加载对话框
|
||||
*/
|
||||
fun doInIoThreadWith(showLoading: Boolean, loadingTips: String, function: suspend () -> Unit) {
|
||||
if (showLoading) {
|
||||
doInIoThread(loadingTips) { function() }
|
||||
} else {
|
||||
doInIoThreadNoDialog { function() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动一个无限轮询任务
|
||||
*
|
||||
* @param pollingInterval 轮询间隔时间(单位:秒)
|
||||
* @param pollingTask 轮询任务的挂起函数
|
||||
*/
|
||||
fun polling(intervalSeconds: Long, task: suspend () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
while (true) {
|
||||
task()
|
||||
delay(intervalSeconds * 1000L) // 转换秒为毫秒
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟开始轮询任务
|
||||
*/
|
||||
suspend fun delayPolling(delaySeconds: Long, intervalSeconds: Long, task: suspend () -> Unit) {
|
||||
delay(delaySeconds * 1000L)
|
||||
polling(intervalSeconds, task)
|
||||
}
|
||||
|
||||
private val taskMap = mutableMapOf<String, Job>()
|
||||
|
||||
/**
|
||||
* 放弃旧任务,执行新任务
|
||||
*
|
||||
* @param key 任务的唯一标识符
|
||||
* @param block 任务的挂起函数,必须使用协程,不能开启新的协程,否则无法取消任务
|
||||
*/
|
||||
fun launchTaskNewFirst(
|
||||
key: String,
|
||||
block: suspend () -> Unit
|
||||
) {
|
||||
taskMap[key]?.cancel() // 取消旧任务
|
||||
taskMap[key] = viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 有新任务时,取消。优先执行旧任务直到完成
|
||||
*
|
||||
* @param key 任务的唯一标识符
|
||||
* @param block 任务函数,可以自由新建协程,但一定要在任务完成时调用 onFinished 回调,否则会导致后续任务永远无法执行
|
||||
*/
|
||||
fun launchTaskOldFirst(
|
||||
key: String,
|
||||
block: (onFinished: () -> Unit) -> Unit
|
||||
) {
|
||||
val existing = taskMap[key]
|
||||
if (existing?.isActive == true) {
|
||||
return
|
||||
}
|
||||
val job = viewModelScope.launch {
|
||||
try {
|
||||
suspendCancellableCoroutine<Unit> { continuation ->
|
||||
block {
|
||||
if (continuation.isActive) {
|
||||
// continuation.resume(Unit) { cause, _, _ ->
|
||||
// println("resume 后任务被取消: $cause")
|
||||
// }
|
||||
}
|
||||
taskMap.remove(key)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
taskMap.remove(key)
|
||||
}
|
||||
}
|
||||
taskMap[key] = job
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
taskMap.values.forEach { it.cancel() }
|
||||
taskMap.clear()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.bbitcn.phone.base
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bbitcn.phone.M
|
||||
|
||||
@Composable
|
||||
fun MyAnimatedVisibility(
|
||||
visible: Boolean,
|
||||
modifier: Modifier = M,
|
||||
enter: EnterTransition = fadeIn(),
|
||||
exit: ExitTransition = fadeOut(),
|
||||
label: String = "AnimatedVisibility",
|
||||
content: @Composable AnimatedVisibilityScope.() -> Unit
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
modifier = modifier,
|
||||
enter = enter,
|
||||
exit = exit,
|
||||
label = label,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.bbitcn.phone.base
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bbitcn.phone.M
|
||||
import kotlinx.coroutines.delay
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
||||
@Composable
|
||||
fun MyBottomSheet(
|
||||
showBottomSheet: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sheetHeight: Dp = 500.dp,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
var showSheet by remember { mutableStateOf(false) }
|
||||
|
||||
// 拦截返回键
|
||||
if (showBottomSheet) {
|
||||
BackHandler {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(showBottomSheet) {
|
||||
if (showBottomSheet) {
|
||||
showSheet = true
|
||||
} else {
|
||||
showSheet = false
|
||||
}
|
||||
}
|
||||
|
||||
if (showBottomSheet || showSheet) {
|
||||
Box(modifier = Modifier.fillMaxSize().padding(bottom = 100.dp)) {
|
||||
AnimatedVisibility(
|
||||
visible = showBottomSheet,
|
||||
enter = fadeIn(animationSpec = tween(200)),
|
||||
exit = fadeOut(animationSpec = tween(200))
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0x99000000))
|
||||
.noVisualFeedbackClickable { onDismissRequest() }
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = showSheet,
|
||||
enter = slideInVertically(
|
||||
initialOffsetY = { it },
|
||||
animationSpec = tween(250, easing = FastOutSlowInEasing)
|
||||
),
|
||||
exit = slideOutVertically(
|
||||
targetOffsetY = { it },
|
||||
animationSpec = tween(200, easing = FastOutSlowInEasing)
|
||||
),
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(sheetHeight)
|
||||
.background(
|
||||
Color.White,
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) {}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.bbitcn.phone.base
|
||||
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class ParameterizedTypeImpl(private var clazz: Class<*>) : ParameterizedType {
|
||||
override fun getActualTypeArguments(): Array<Type> {
|
||||
return arrayOf(clazz)
|
||||
}
|
||||
|
||||
override fun getRawType(): Type {
|
||||
return MutableList::class.java
|
||||
}
|
||||
|
||||
override fun getOwnerType(): Type? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.bbitcn.phone.base
|
||||
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
|
||||
/**
|
||||
* 自定义点击事件
|
||||
* 不使用clickable的点击效果
|
||||
*/
|
||||
fun Modifier.noVisualFeedbackClickable(
|
||||
onClick: () -> Unit
|
||||
): Modifier {
|
||||
return this.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
onClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.bbitcn.phone.model
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class Page(
|
||||
var id: UUID,
|
||||
var sort: Int,
|
||||
var name: String,
|
||||
var url: String
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.bbitcn.phone.model
|
||||
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
|
||||
data class Plan(
|
||||
val id: UUID,
|
||||
var name: String,
|
||||
val createTime: Date,
|
||||
var cycleRefreshTime :Int,
|
||||
var pages: List<Page>
|
||||
|
||||
)
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.bbitcn.phone.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Phone
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarDefaults
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.bbitcn.phone.M
|
||||
import com.bbitcn.phone.base.MyBottomSheet
|
||||
import com.bbitcn.phone.ui.compose.ConnectScreen
|
||||
import com.bbitcn.phone.ui.compose.ControlScreen
|
||||
import com.bbitcn.phone.ui.compose.SetScreen
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class PhoneActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
val viewModel = viewModel<PhoneViewModel>()
|
||||
viewModel.startDiscovery(this)
|
||||
val pagerState = rememberPagerState { 10 }
|
||||
val scope = rememberCoroutineScope()
|
||||
val showBottomSheet by viewModel.isDrawerOpen.collectAsState()
|
||||
|
||||
// 连接、控制、设置
|
||||
val navList = listOf(
|
||||
"连接" to Icons.Default.Phone,
|
||||
"控制" to Icons.Default.PlayArrow,
|
||||
"设置" to Icons.Default.Settings
|
||||
)
|
||||
Scaffold(
|
||||
modifier = M
|
||||
.fillMaxSize(),
|
||||
floatingActionButton = {
|
||||
if( pagerState.currentPage == 2 && !showBottomSheet) {
|
||||
FloatingActionButton(onClick = {
|
||||
// 新增计划
|
||||
viewModel.addPlan()
|
||||
}) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
NavigationBar(windowInsets = NavigationBarDefaults.windowInsets) {
|
||||
navList.forEachIndexed { index, item ->
|
||||
NavigationBarItem(
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(index)
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
item.second,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = { Text(item.first) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { contentPadding ->
|
||||
HorizontalPager(state = pagerState, contentPadding = contentPadding) {
|
||||
Column(modifier = M.fillMaxSize()) {
|
||||
when (it) {
|
||||
0 -> ConnectScreen(viewModel)
|
||||
1 -> ControlScreen(viewModel)
|
||||
2 -> SetScreen(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
MyBottomSheet(
|
||||
showBottomSheet = showBottomSheet,
|
||||
onDismissRequest = {
|
||||
viewModel.closeDrawer()
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = M.fillMaxSize()
|
||||
) {
|
||||
// 底部抽屉
|
||||
viewModel.drawerContent.value()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FunctionModel(name: String, content: @Composable () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(5.dp)
|
||||
) {
|
||||
Text(
|
||||
name,
|
||||
fontSize = MaterialTheme.typography.headlineMedium.fontSize,
|
||||
modifier = Modifier
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(5.dp)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MyFuncButtonRow(content: @Composable () -> Unit) {
|
||||
LazyRow {
|
||||
item {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
package com.bbitcn.phone.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bbitcn.phone.M
|
||||
import com.bbitcn.phone.base.BaseViewModel
|
||||
import com.bbitcn.phone.model.Page
|
||||
import com.bbitcn.phone.model.Plan
|
||||
import com.bbitcn.phone.utls.PlanTempDatabase
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.java_websocket.client.WebSocketClient
|
||||
import org.java_websocket.handshake.ServerHandshake
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.net.URI
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
|
||||
class PhoneViewModel : BaseViewModel() {
|
||||
// 控制 Drawer 内容的 MutableStateFlow
|
||||
private val _drawerContent = MutableStateFlow<@Composable () -> Unit> { }
|
||||
val drawerContent: StateFlow<@Composable () -> Unit> = _drawerContent.asStateFlow()
|
||||
|
||||
/**
|
||||
* 控制 Drawer 状态的 Boolean Flow
|
||||
*/
|
||||
private val _isDrawerOpen = MutableStateFlow(false)
|
||||
val isDrawerOpen: StateFlow<Boolean> = _isDrawerOpen.asStateFlow()
|
||||
|
||||
/**
|
||||
* 扫描到的所有设备
|
||||
*/
|
||||
private val _devices = MutableStateFlow<List<NsdServiceInfo>>(emptyList())
|
||||
val devices = _devices.asStateFlow()
|
||||
|
||||
/**
|
||||
* 设备当前页面列表
|
||||
*/
|
||||
private val _devicePagesList = MutableStateFlow<Plan?>(null)
|
||||
val devicePagesList = _devicePagesList.asStateFlow()
|
||||
|
||||
/**
|
||||
* 自己的计划列表
|
||||
*/
|
||||
private val _plans = MutableStateFlow<List<Plan>>(emptyList())
|
||||
val plans = _plans.asStateFlow()
|
||||
|
||||
/**
|
||||
* 当前连接的设备信息
|
||||
*/
|
||||
private val _device = MutableStateFlow<NsdServiceInfo?>(null)
|
||||
val device = _device.asStateFlow()
|
||||
|
||||
/**
|
||||
* 自动刷新状态
|
||||
*/
|
||||
private val _autoSearching = MutableStateFlow<Boolean>(true)
|
||||
val autoSearching = _autoSearching.asStateFlow()
|
||||
|
||||
/**
|
||||
* 发现监听器
|
||||
*/
|
||||
private var discoveryListener: NsdManager.DiscoveryListener? = null
|
||||
private var client: WebSocketClient? = null
|
||||
lateinit var nsdManager: NsdManager
|
||||
val port = 8080
|
||||
|
||||
|
||||
init {
|
||||
// 刷新个人配置
|
||||
_plans.value = PlanTempDatabase.getAll()
|
||||
}
|
||||
|
||||
fun connectToTv(nsdServiceInfo: NsdServiceInfo) {
|
||||
connectToTvByIp(nsdServiceInfo.host.hostAddress, nsdServiceInfo)
|
||||
}
|
||||
|
||||
fun connectToTvByIp(tvIp: String, nsdServiceInfo: NsdServiceInfo? = null) {
|
||||
doInIoThread {
|
||||
val uri = URI("ws://$tvIp:$port")
|
||||
client = object : WebSocketClient(uri) {
|
||||
override fun onOpen(handshakedata: ServerHandshake?) {
|
||||
_device.value = nsdServiceInfo
|
||||
?: NsdServiceInfo().apply {
|
||||
serviceName = "手动连接设备"
|
||||
host =
|
||||
URI("http://$tvIp").host?.let { InetAddress.getByName(it) }
|
||||
port = port
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessage(message: String?) {
|
||||
message?.let {
|
||||
try {
|
||||
val json = JSONObject(it)
|
||||
if (json["type"] == "tv") {
|
||||
val listType = object : TypeToken<Plan>() {}.type
|
||||
val plan: Plan = Gson().fromJson(json["data"].toString(), listType)
|
||||
plan.pages.sortedBy { it.id }
|
||||
_devicePagesList.value = plan
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClose(code: Int, reason: String?, remote: Boolean) {
|
||||
initConnectedDeviceData()
|
||||
client?.close()
|
||||
client = null
|
||||
}
|
||||
|
||||
override fun onError(ex: Exception?) {
|
||||
ex?.printStackTrace()
|
||||
}
|
||||
}
|
||||
client?.connect()
|
||||
}
|
||||
}
|
||||
|
||||
// 开始发现服务
|
||||
fun startDiscovery(context: Context) {
|
||||
nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||
discoveryListener = object : NsdManager.DiscoveryListener {
|
||||
override fun onServiceFound(service: NsdServiceInfo) {
|
||||
// 异步解析服务
|
||||
nsdManager.resolveService(service, object : NsdManager.ResolveListener {
|
||||
override fun onServiceResolved(nsdServiceInfo: NsdServiceInfo) {
|
||||
val host = nsdServiceInfo.host.hostAddress
|
||||
val port = nsdServiceInfo.port
|
||||
if (isServiceAvailable(host, port)) {
|
||||
_devices.value =
|
||||
(_devices.value + nsdServiceInfo).distinctBy { it.serviceName }
|
||||
connectToTv(nsdServiceInfo)
|
||||
} else {
|
||||
Log.w("NSD", "服务不可达: $host:$port")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onServiceLost(service: NsdServiceInfo) {
|
||||
_devices.value = _devices.value.filter { it.serviceName != service.serviceName }
|
||||
}
|
||||
|
||||
override fun onDiscoveryStarted(serviceType: String) {}
|
||||
override fun onDiscoveryStopped(serviceType: String) {}
|
||||
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {}
|
||||
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {}
|
||||
}
|
||||
|
||||
nsdManager.discoverServices(
|
||||
"_bbit_show._tcp.",
|
||||
NsdManager.PROTOCOL_DNS_SD,
|
||||
discoveryListener
|
||||
)
|
||||
_autoSearching.value = true
|
||||
}
|
||||
|
||||
fun isServiceAvailable(ip: String, port: Int, timeout: Int = 1000): Boolean {
|
||||
return try {
|
||||
Socket().use { socket ->
|
||||
socket.connect(InetSocketAddress(ip, port), timeout)
|
||||
}
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动断开连接
|
||||
*/
|
||||
fun disconnect() {
|
||||
client?.close()
|
||||
client = null
|
||||
initConnectedDeviceData()
|
||||
}
|
||||
|
||||
// 停止发现
|
||||
fun stopDiscovery() {
|
||||
discoveryListener?.let {
|
||||
nsdManager.stopServiceDiscovery(it)
|
||||
discoveryListener = null
|
||||
}
|
||||
_autoSearching.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础控制
|
||||
*/
|
||||
fun next() = sendAction("next")
|
||||
fun prev() = sendAction("prev")
|
||||
fun reload() = sendAction("reload")
|
||||
fun stop() = sendAction("stop")
|
||||
|
||||
/**
|
||||
* 多页控制
|
||||
*/
|
||||
fun nextPage() = sendAction("nextPage")
|
||||
fun prevPage() = sendAction("prevPage")
|
||||
fun reloadAll() = sendAction("reload_all")
|
||||
fun switchPage(id: UUID) = sendAction("switchPage", id.toString())
|
||||
fun loadList(content: String) = sendAction("load_list", content)
|
||||
|
||||
private fun sendAction(action: String, data: String? = null) {
|
||||
val json = JSONObject()
|
||||
json.put("action", action)
|
||||
if (data != null) json.put("data", data)
|
||||
client?.send(json.toString())
|
||||
Log.d("WebSocket", "发送消息: $json")
|
||||
}
|
||||
|
||||
|
||||
fun closeDrawer() {
|
||||
_isDrawerOpen.value = false
|
||||
}
|
||||
|
||||
fun public() {
|
||||
doInIoThread {
|
||||
_drawerContent.value = {
|
||||
LazyColumn {
|
||||
item {
|
||||
Text(
|
||||
text = "选择方案",
|
||||
modifier = M
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
fontSize = MaterialTheme.typography.headlineLarge.fontSize,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
items(items = _plans.value) { plan ->
|
||||
Button(
|
||||
modifier = M.fillMaxWidth(),
|
||||
onClick = {
|
||||
// 发送方案数据
|
||||
val gson = GsonBuilder()
|
||||
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
||||
.create()
|
||||
|
||||
val jsonString = gson.toJson(plan)
|
||||
loadList(jsonString)
|
||||
_isDrawerOpen.value = false
|
||||
}
|
||||
) {
|
||||
Text(plan.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_isDrawerOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun addPlan() {
|
||||
doInIoThread {
|
||||
_drawerContent.value = {
|
||||
var name by remember { mutableStateOf("") }
|
||||
var autoRefreshTimes by remember { mutableStateOf("0") }
|
||||
Column {
|
||||
Text(
|
||||
text = "新建方案",
|
||||
modifier = M
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
fontSize = MaterialTheme.typography.headlineLarge.fontSize,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Column(modifier = M.padding(horizontal = 16.dp)) {
|
||||
TextField(
|
||||
modifier = M.fillMaxWidth(),
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("方案名称") },
|
||||
singleLine = true
|
||||
)
|
||||
TextField(
|
||||
modifier = M.fillMaxWidth(),
|
||||
value = autoRefreshTimes,
|
||||
onValueChange = { autoRefreshTimes = it },
|
||||
label = { Text("自动刷新时间(0为不刷新)") },
|
||||
singleLine = true,
|
||||
// 限制为数字输入
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
)
|
||||
Button(modifier = M.fillMaxWidth(), onClick = {
|
||||
doInIoThread {
|
||||
PlanTempDatabase.insert(
|
||||
Plan(
|
||||
id = UUID.randomUUID(),
|
||||
name = name,
|
||||
createTime = Date(),
|
||||
cycleRefreshTime = autoRefreshTimes.toIntOrNull() ?: 0,
|
||||
pages = emptyList()
|
||||
)
|
||||
)
|
||||
_plans.value = PlanTempDatabase.getAll()
|
||||
_isDrawerOpen.value = false
|
||||
}
|
||||
}) { Text("新建") }
|
||||
}
|
||||
}
|
||||
}
|
||||
_isDrawerOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun editPlan(plan: Plan) {
|
||||
doInIoThread {
|
||||
_drawerContent.value = {
|
||||
var name by remember { mutableStateOf(plan.name) }
|
||||
var autoRefreshTimes by remember { mutableStateOf(plan.cycleRefreshTime.toString()) }
|
||||
Column {
|
||||
Text(
|
||||
text = "编辑方案",
|
||||
modifier = M
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
fontSize = MaterialTheme.typography.headlineLarge.fontSize,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Column(modifier = M.padding(horizontal = 16.dp)) {
|
||||
TextField(
|
||||
modifier = M.fillMaxWidth(),
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("方案名称") },
|
||||
singleLine = true
|
||||
)
|
||||
TextField(
|
||||
modifier = M.fillMaxWidth(),
|
||||
value = autoRefreshTimes,
|
||||
onValueChange = { autoRefreshTimes = it },
|
||||
label = { Text("自动刷新时间(0为不刷新)") },
|
||||
singleLine = true,
|
||||
// 限制为数字输入
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
)
|
||||
Row(
|
||||
modifier = M.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Button(modifier = M.weight(1f), onClick = {
|
||||
doInIoThread {
|
||||
PlanTempDatabase.update({ it.id == plan.id }, update = {
|
||||
it.name = name
|
||||
it.cycleRefreshTime = autoRefreshTimes.toIntOrNull() ?: 0
|
||||
}) {
|
||||
_plans.value = PlanTempDatabase.getAll()
|
||||
_isDrawerOpen.value = false
|
||||
}
|
||||
}
|
||||
}) { Text("确定") }
|
||||
Button(modifier = M.weight(1f), onClick = {
|
||||
PlanTempDatabase.delete({
|
||||
it.id == plan.id
|
||||
}) {
|
||||
_plans.value = PlanTempDatabase.getAll()
|
||||
_isDrawerOpen.value = false
|
||||
}
|
||||
}) { Text("删除") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_isDrawerOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun editPage(plan: Plan) {
|
||||
doInIoThread {_drawerContent.value = {
|
||||
// 保存原始快照用于比较
|
||||
val originalList = remember { plan.pages.map { it.copy() } }
|
||||
var list by remember { mutableStateOf(plan.pages.map { it.copy() }) }
|
||||
|
||||
// 判断列表是否被修改
|
||||
val isModified by remember(list) {
|
||||
derivedStateOf {
|
||||
if (list.size != originalList.size) return@derivedStateOf true
|
||||
list.zip(originalList).any { (cur, orig) ->
|
||||
cur.id != orig.id || cur.name != orig.name || cur.url != orig.url || cur.sort != orig.sort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = M.fillMaxSize()) {
|
||||
Text(
|
||||
text = "方案细则 - ${plan.name}",
|
||||
modifier = M.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
fontSize = MaterialTheme.typography.headlineLarge.fontSize,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Button(modifier = M.fillMaxWidth(), onClick = {
|
||||
list = list + Page(
|
||||
id = UUID.randomUUID(),
|
||||
sort = list.size + 1,
|
||||
name = "新页面",
|
||||
url = ""
|
||||
)
|
||||
}) { Text("新增") }
|
||||
|
||||
LazyColumn(modifier = M.weight(1f)) {
|
||||
itemsIndexed(items = list) { index, page ->
|
||||
var pageSort by remember { mutableStateOf(page.sort.toString()) }
|
||||
var pageName by remember { mutableStateOf(page.name) }
|
||||
var pageUrl by remember { mutableStateOf(page.url) }
|
||||
|
||||
Row(modifier = M.fillMaxWidth().padding(vertical = 5.dp)) {
|
||||
TextField(
|
||||
modifier = M.weight(2f).padding(horizontal = 4.dp),
|
||||
value = pageSort,
|
||||
onValueChange = { newSort ->
|
||||
pageSort = newSort
|
||||
list = list.toMutableList().also { l ->
|
||||
l[index] = l[index].copy(sort = newSort.toIntOrNull() ?: 0)
|
||||
}
|
||||
},
|
||||
label = { Text("序号") },
|
||||
singleLine = true
|
||||
)
|
||||
TextField(
|
||||
modifier = M.weight(3f).padding(horizontal = 4.dp),
|
||||
value = pageName,
|
||||
onValueChange = { newName ->
|
||||
pageName = newName
|
||||
list = list.toMutableList().also { l ->
|
||||
l[index] = l[index].copy(name = newName)
|
||||
}
|
||||
},
|
||||
label = { Text("名称") },
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
|
||||
Row(modifier = M.fillMaxWidth().padding(vertical = 5.dp)) {
|
||||
TextField(
|
||||
modifier = M.weight(4f).padding(horizontal = 4.dp),
|
||||
value = pageUrl,
|
||||
onValueChange = { newUrl ->
|
||||
pageUrl = newUrl
|
||||
list = list.toMutableList().also { l ->
|
||||
l[index] = l[index].copy(url = newUrl)
|
||||
}
|
||||
},
|
||||
label = { Text("网址") },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri)
|
||||
)
|
||||
Button(
|
||||
modifier = M.padding(horizontal = 4.dp),
|
||||
onClick = {
|
||||
list = list.filter { it.id != page.id }
|
||||
}
|
||||
) { Text("删除") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isModified) {
|
||||
Button(modifier = M.fillMaxWidth(), onClick = {
|
||||
doInIoThread {
|
||||
PlanTempDatabase.update({ it.id == plan.id }, update = {
|
||||
it.pages = list.sortedBy { it.sort }
|
||||
}) {
|
||||
_plans.value = PlanTempDatabase.getAll()
|
||||
_isDrawerOpen.value = false
|
||||
}
|
||||
}
|
||||
}) { Text("保存") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_isDrawerOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun initConnectedDeviceData() {
|
||||
_devicePagesList.value = null
|
||||
_device.value = null
|
||||
_plans.value = PlanTempDatabase.getAll()
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.bbitcn.phone.ui.compose
|
||||
|
||||
import android.R.attr.port
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.bbitcn.phone.ui.FunctionModel
|
||||
import com.bbitcn.phone.ui.MyFuncButtonRow
|
||||
import com.bbitcn.phone.ui.PhoneViewModel
|
||||
|
||||
@Composable
|
||||
fun ConnectScreen(viewModel: PhoneViewModel) {
|
||||
var ip by remember { mutableStateOf("10.0.4.74") }
|
||||
val device by viewModel.device.collectAsState()
|
||||
val autoSearching by viewModel.autoSearching.collectAsState()
|
||||
|
||||
FunctionModel("状态信息") {
|
||||
Column {
|
||||
Text(
|
||||
"连接状态:" + if (device == null) "未连接" else "已连接到设备<${device?.serviceName}>",
|
||||
fontSize = MaterialTheme.typography.bodyLarge.fontSize
|
||||
)
|
||||
Text(
|
||||
"扫描状态:" + if (autoSearching) "正在自动搜索中" else "",
|
||||
fontSize = MaterialTheme.typography.bodyLarge.fontSize
|
||||
)
|
||||
}
|
||||
}
|
||||
FunctionModel("自动连接") {
|
||||
Column {
|
||||
val devicesList by viewModel.devices.collectAsState()
|
||||
LazyRow {
|
||||
items(items = devicesList) {
|
||||
Button(onClick = {
|
||||
viewModel.connectToTv(it)
|
||||
}) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
it.serviceName,
|
||||
fontSize = MaterialTheme.typography.bodyLarge.fontSize
|
||||
)
|
||||
Text(
|
||||
it.host.hostAddress + ":" + it.port,
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
MyFuncButtonRow {
|
||||
Button(enabled = !autoSearching, onClick = {
|
||||
viewModel.startDiscovery(context)
|
||||
}) {
|
||||
Text("开始搜索")
|
||||
}
|
||||
Button(enabled = autoSearching, onClick = {
|
||||
viewModel.stopDiscovery()
|
||||
}) {
|
||||
Text("停止搜索")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FunctionModel("手动连接") {
|
||||
Column {
|
||||
TextField(
|
||||
value = ip,
|
||||
onValueChange = { ip = it },
|
||||
label = { Text("设备IP地址(可通过点击电视菜单键查看)") })
|
||||
MyFuncButtonRow {
|
||||
Button(onClick = {
|
||||
viewModel.connectToTvByIp(ip)
|
||||
}) { Text("连接") }
|
||||
Button(onClick = { viewModel.disconnect() }) {
|
||||
Text("断开")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.bbitcn.phone.ui.compose
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bbitcn.phone.M
|
||||
import com.bbitcn.phone.ui.FunctionModel
|
||||
import com.bbitcn.phone.ui.MyFuncButtonRow
|
||||
import com.bbitcn.phone.ui.PhoneViewModel
|
||||
|
||||
@Composable
|
||||
fun ControlScreen(viewModel: PhoneViewModel) {
|
||||
FunctionModel("设备状态") {
|
||||
val device by viewModel.device.collectAsState()
|
||||
if (device == null) {
|
||||
Text("连接已断开")
|
||||
} else {
|
||||
Column {
|
||||
Text("设备名称:${device?.serviceName}")
|
||||
Text("设备地址:${device?.host?.hostAddress}")
|
||||
}
|
||||
}
|
||||
}
|
||||
FunctionModel("基础控制") {
|
||||
MyFuncButtonRow {
|
||||
Button(onClick = {
|
||||
viewModel.prev()
|
||||
}) { Text("后退") }
|
||||
Button(onClick = {
|
||||
viewModel.next()
|
||||
}) { Text("前进") }
|
||||
Button(onClick = {
|
||||
viewModel.reload()
|
||||
}) { Text("刷新") }
|
||||
Button(onClick = {
|
||||
viewModel.stop()
|
||||
}) { Text("停止") }
|
||||
}
|
||||
}
|
||||
FunctionModel("多页控制") {
|
||||
Column {
|
||||
MyFuncButtonRow {
|
||||
Button(onClick = {
|
||||
viewModel.prevPage()
|
||||
}) { Text("上一页") }
|
||||
Button(onClick = {
|
||||
viewModel.nextPage()
|
||||
}) { Text("下一页") }
|
||||
Button(onClick = {
|
||||
viewModel.reloadAll()
|
||||
}) { Text("刷新") }
|
||||
Button(onClick = {
|
||||
viewModel.public()
|
||||
}) { Text("发布") }
|
||||
}
|
||||
}
|
||||
}
|
||||
val devicePagesList by viewModel.devicePagesList.collectAsState()
|
||||
devicePagesList?.let {
|
||||
FunctionModel("当前设备页面") {
|
||||
LazyColumn {
|
||||
items(items = it.pages) {
|
||||
Button(modifier = M.fillMaxWidth(), onClick = {
|
||||
viewModel.switchPage(it.id)
|
||||
}) {
|
||||
Text(
|
||||
modifier = M
|
||||
.fillMaxWidth(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
text = "${it.sort}.${it.name} - ${it.url}",
|
||||
textAlign = TextAlign.Left
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.bbitcn.phone.ui.compose
|
||||
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import com.bbitcn.phone.M
|
||||
import com.bbitcn.phone.ui.FunctionModel
|
||||
import com.bbitcn.phone.ui.PhoneViewModel
|
||||
|
||||
@Composable
|
||||
fun SetScreen(viewModel: PhoneViewModel) {
|
||||
FunctionModel("页面配置") {
|
||||
val plans by viewModel.plans.collectAsState()
|
||||
LazyColumn {
|
||||
items(items = plans) { plan ->
|
||||
Row(modifier = M.fillMaxWidth()) {
|
||||
Button(
|
||||
modifier = M.weight(1f), onClick = {
|
||||
// 编辑计划
|
||||
viewModel.editPlan(plan)
|
||||
}) { Text(modifier = M.fillMaxWidth(), text = plan.name) }
|
||||
Button(onClick = {
|
||||
viewModel.editPage(plan)
|
||||
}) { Text("编辑细则") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.bbitcn.phone.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.bbitcn.phone.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun BBITShowTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.bbitcn.phone.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.bbitcn.phone.utls;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.tencent.mmkv.MMKV;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* MMKV工具类
|
||||
*/
|
||||
public class MMKVUtil {
|
||||
private static MMKV mmkvInstance;
|
||||
|
||||
private MMKVUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化MMKV,只能在Application中初始化一次
|
||||
*/
|
||||
public static void init(Context applicationContext) {
|
||||
MMKV.initialize(applicationContext);
|
||||
mmkvInstance = MMKV.defaultMMKV();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据的方法,我们需要拿到保存数据的具体类型,然后根据类型调用不同的保存方法
|
||||
*/
|
||||
public static <T> void put(String key, T object) {
|
||||
if (object instanceof String) {
|
||||
mmkvInstance.encode(key, (String) object);
|
||||
} else if (object instanceof Integer) {
|
||||
mmkvInstance.encode(key, (Integer) object);
|
||||
} else if (object instanceof Boolean) {
|
||||
mmkvInstance.encode(key, (Boolean) object);
|
||||
} else if (object instanceof Float) {
|
||||
mmkvInstance.encode(key, (Float) object);
|
||||
} else if (object instanceof Long) {
|
||||
mmkvInstance.encode(key, (Long) object);
|
||||
} else if (object instanceof Double) {
|
||||
mmkvInstance.encode(key, (Double) object);
|
||||
} else if (object instanceof Set) {
|
||||
mmkvInstance.encode(key, (Set<String>) object);
|
||||
} else if (object instanceof Parcelable) {
|
||||
mmkvInstance.encode(key, (Parcelable) object);
|
||||
} else {
|
||||
mmkvInstance.encode(key, object == null ? "" : object.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 得到保存数据的方法,我们根据默认值得到保存的数据的具体类型,然后调用相对于的方法获取值
|
||||
*/
|
||||
public static <T> T get(String key, T defaultObject) {
|
||||
if (defaultObject instanceof String || defaultObject == null) {
|
||||
return (T) mmkvInstance.decodeString(key, (String) defaultObject);
|
||||
} else if (defaultObject instanceof Integer) {
|
||||
return (T) Integer.valueOf(mmkvInstance.decodeInt(key, (Integer) defaultObject));
|
||||
} else if (defaultObject instanceof Boolean) {
|
||||
return (T) Boolean.valueOf(mmkvInstance.decodeBool(key, (Boolean) defaultObject));
|
||||
} else if (defaultObject instanceof Float) {
|
||||
return (T) Float.valueOf(mmkvInstance.decodeFloat(key, (Float) defaultObject));
|
||||
} else if (defaultObject instanceof Long) {
|
||||
return (T) Long.valueOf(mmkvInstance.decodeLong(key, (Long) defaultObject));
|
||||
} else if (defaultObject instanceof Double) {
|
||||
return (T) Double.valueOf(mmkvInstance.decodeDouble(key, (Double) defaultObject));
|
||||
} else if (defaultObject instanceof Parcelable) {
|
||||
Parcelable p = (Parcelable) defaultObject;
|
||||
return (T) mmkvInstance.decodeParcelable(key, p.getClass());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得字符串
|
||||
*/
|
||||
public static String get(String key) {
|
||||
return mmkvInstance.decodeString(key, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除某个key值已经对应的值
|
||||
*/
|
||||
public static void remove(String key) {
|
||||
mmkvInstance.remove(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询某个key是否已经存在
|
||||
*/
|
||||
public boolean contains(String key) {
|
||||
return mmkvInstance.contains(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有数据
|
||||
*/
|
||||
public static void clear() {
|
||||
mmkvInstance.clear();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.bbitcn.phone.utls
|
||||
|
||||
import com.bbitcn.phone.base.ParameterizedTypeImpl
|
||||
import com.bbitcn.phone.model.Plan
|
||||
import com.bbitcn.sericulture.base.BaseListTempDataBase
|
||||
import kotlin.jvm.java
|
||||
import java.lang.reflect.Type
|
||||
|
||||
object PlanTempDatabase : BaseListTempDataBase<Plan>() {
|
||||
|
||||
override fun getKey(): String {
|
||||
return "MENU_PERMISSION_LIST2"
|
||||
}
|
||||
|
||||
override fun getType(): Type {
|
||||
return ParameterizedTypeImpl(Plan::class.java)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.bbitcn.phone.utls
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class PollingTask private constructor() {
|
||||
private var scheduler: ScheduledExecutorService? = null
|
||||
private var taskMap: MutableMap<String, ScheduledFuture<*>>? = null
|
||||
|
||||
// 私有化构造函数,确保外部不能直接实例化
|
||||
init {
|
||||
createNewScheduler()
|
||||
}
|
||||
|
||||
// 创建新的 ScheduledExecutorService 实例
|
||||
private fun createNewScheduler() {
|
||||
if (scheduler != null && !scheduler!!.isShutdown) {
|
||||
scheduler!!.shutdown()
|
||||
}
|
||||
scheduler = Executors.newScheduledThreadPool(3)
|
||||
taskMap = HashMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动延迟任务
|
||||
*
|
||||
* @param taskId 任务ID,用于唯一标识该任务
|
||||
* @param delaySeconds 延迟时间(以秒为单位)
|
||||
* @param task 需要执行的任务
|
||||
*/
|
||||
fun startDelayedTask(taskId: String, delaySeconds: Long, task: Runnable) {
|
||||
stopTask(taskId) // 如果已有相同ID的任务,先停止
|
||||
|
||||
val future = scheduler!!.schedule({
|
||||
task.run() // 执行任务
|
||||
}, delaySeconds, TimeUnit.SECONDS)
|
||||
taskMap!![taskId] = future // 保存任务的ScheduledFuture
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动IO线程的轮询任务
|
||||
*
|
||||
* @param taskId 任务ID,用于唯一标识该任务
|
||||
* @param intervalSeconds 轮询间隔时间(以秒为单位)
|
||||
* @param task 需要在IO线程中执行的任务
|
||||
*/
|
||||
fun startPollingTaskOnIOThread(taskId: String, intervalSeconds: Long, task: Runnable) {
|
||||
startPollingTask(taskId, intervalSeconds) {
|
||||
task.run()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动UI线程的轮询任务
|
||||
*
|
||||
* @param taskId 任务ID,用于唯一标识该任务
|
||||
* @param intervalSeconds 轮询间隔时间(以秒为单位)
|
||||
* @param task 需要在UI线程中执行的任务
|
||||
*/
|
||||
fun startPollingTaskOnUIThread(taskId: String, intervalSeconds: Long, task: Runnable) {
|
||||
startPollingTask(taskId, intervalSeconds) {
|
||||
// 执行UI线程任务(需要在UI线程中执行)
|
||||
Handler(Looper.getMainLooper()).post(task)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动IO线程任务并在UI线程中处理结果的轮询任务
|
||||
*
|
||||
* @param taskId 任务ID,用于唯一标识该任务
|
||||
* @param intervalSeconds 轮询间隔时间(以秒为单位)
|
||||
* @param ioTask 需要在IO线程中执行的任务
|
||||
* @param uiTask 需要在UI线程中执行的任务
|
||||
* @param <T> IO任务返回结果的类型
|
||||
</T> */
|
||||
fun <T> startPollingTask(
|
||||
taskId: String,
|
||||
intervalSeconds: Long,
|
||||
ioTask: IRxIOTask<T>,
|
||||
uiTask: IRxUITask<T>
|
||||
) {
|
||||
startPollingTask(taskId, intervalSeconds) {
|
||||
// 执行IO线程任务
|
||||
val result = ioTask.doInIOThread()
|
||||
// 将结果传递给UI线程任务
|
||||
Handler(Looper.getMainLooper()).post { uiTask.doInUIThread(result) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动基础轮询任务,供内部使用
|
||||
*
|
||||
* @param taskId 任务ID,用于唯一标识该任务
|
||||
* @param intervalSeconds 轮询间隔时间(以秒为单位)
|
||||
* @param task 需要执行的任务
|
||||
*/
|
||||
private fun startPollingTask(taskId: String, intervalSeconds: Long, task: Runnable) {
|
||||
stopTask(taskId) // 如果已有相同ID的任务,先停止
|
||||
val future = scheduler!!.scheduleWithFixedDelay(task, 0, intervalSeconds, TimeUnit.SECONDS)
|
||||
|
||||
// val future = scheduler!!.scheduleAtFixedRate(task, 0, intervalSeconds, TimeUnit.SECONDS)
|
||||
taskMap!![taskId] = future // 保存任务的ScheduledFuture
|
||||
}
|
||||
|
||||
// 停止所有任务
|
||||
fun stopAllTasks() {
|
||||
for (taskId in taskMap!!.keys) {
|
||||
// test("关闭轮询任务$taskId")
|
||||
}
|
||||
if (scheduler != null && !scheduler!!.isShutdown) {
|
||||
scheduler!!.shutdownNow() // 超时后强制停止
|
||||
taskMap!!.clear() // 清空任务映射
|
||||
}
|
||||
// 从实例映射中移除当前实例
|
||||
instances.values.remove(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止轮询任务
|
||||
*
|
||||
* @param taskId 任务ID
|
||||
*/
|
||||
fun stopTask(taskId: String) {
|
||||
val future = taskMap!![taskId]
|
||||
if (future != null) {
|
||||
future.cancel(true) // 取消任务,中断正在执行的任务
|
||||
taskMap!!.remove(taskId) // 从任务列表中移除
|
||||
}
|
||||
}
|
||||
|
||||
// 定义IO任务接口
|
||||
interface IRxIOTask<T> {
|
||||
fun doInIOThread(): T
|
||||
}
|
||||
|
||||
// 定义UI任务接口
|
||||
interface IRxUITask<T> {
|
||||
fun doInUIThread(t: T)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val instances: MutableMap<String, PollingTask> = HashMap()
|
||||
|
||||
@Synchronized
|
||||
fun getInstance(): PollingTask = getInstance("MAIN")
|
||||
|
||||
// 获取基于ID的单例实例
|
||||
@Synchronized
|
||||
fun getInstance(id: String): PollingTask {
|
||||
if (!instances.containsKey(id)) {
|
||||
instances[id] = PollingTask()
|
||||
}
|
||||
return instances[id]!!
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">BBIT展示屏控制工具</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.BBITShow" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.bbitcn.phone
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user