初始化项目

This commit is contained in:
BBIT-Kai
2026-05-25 15:27:47 +08:00
commit ec8289d0f5
64 changed files with 3635 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+68
View File
@@ -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")
}
+21
View File
@@ -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)
}
}
+28
View File
@@ -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

+10
View File
@@ -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>
+3
View File
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">BBIT展示屏控制工具</string>
</resources>
+5
View File
@@ -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)
}
}