Compare commits

...

20 Commits
master ... dev

Author SHA1 Message Date
yangxisong e1ddc14553 feat: 连接状态 2025-12-04 09:34:45 +08:00
yangxisong 9c29fee24e feat: 服务消息UI交互 2025-12-02 15:21:29 +08:00
yangxisong 84961a367c feat: 优化flow订阅策略 2025-11-27 17:40:43 +08:00
yangxisong 0c122b9352 feat: 组件解耦 2025-11-27 16:52:43 +08:00
yangxisong 4712855f6c feat: app 签名 2025-11-21 14:18:22 +08:00
yangxisong ae8209e025 feat: app 签名 2025-11-20 18:21:42 +08:00
yangxisong 0b0c863765 feat: App 应用内更新 2025-11-20 18:21:29 +08:00
yangxisong c9a2b78a74 feat: 基础功能 2025-11-19 15:58:32 +08:00
yangxisong 699bc11c25 feat: home 2025-11-18 15:24:16 +08:00
yangxisong e5d17bc970 feat: statusBar ui 2025-11-14 14:05:27 +08:00
yangxisong 6b234f86e7 feat: statusBar ui 2025-11-14 11:44:49 +08:00
yangxisong 189ffa71bf feat: statusBar ui 2025-11-13 18:21:37 +08:00
yangxisong 4193326b92 feat: statusBar ui 2025-11-13 17:55:24 +08:00
yangxisong ca99eaa3e4 feat: toast 2025-11-13 16:04:13 +08:00
yangxisong 1b0e5ec8ab feat: 网络 2025-11-13 15:15:09 +08:00
yangxisong f8b5f3a395 feat: 网络 2025-11-13 10:30:42 +08:00
yangxisong 4327a8638c feat: 网络 2025-11-13 10:25:54 +08:00
yangxisong 570de3c4e7 feat: 网络 2025-11-12 18:20:57 +08:00
yangxisong 22ff4384ca feat: 初始化 SDK 2025-11-10 17:20:15 +08:00
yangxisong 41726d5006 Initial commit 2025-11-07 18:20:15 +08:00
101 changed files with 3803 additions and 4 deletions

5
.gitignore vendored
View File

@ -23,13 +23,10 @@ misc.xml
deploymentTargetDropDown.xml deploymentTargetDropDown.xml
render.experimental.xml render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase) # Google Services (e.g. APIs or Firebase)
google-services.json google-services.json
# Android Profiling # Android Profiling
*.hprof *.hprof
app/release

252
README.md
View File

@ -1,2 +1,254 @@
# Rokid # Rokid
## 准备工作
### 配置文件
配置文件 `init.json``JSON` 格式存储,通过 `Key-Value` 配置系统功能。
> 示例:
```json
{
"launchPackageName":"com.rokid.sample",
"wifiSsid":"<wifi名称>",
"wifiPwd":"<wifi密码>"
}
```
通过 `adb push` 至目录 `/sdcard/init.json`
### 调试连接
```shell
adb shell setprop persist.vendor.adb 1
```
## Glasses行业版SDK
### 0.背景
* 该插件仅适用于Glasses行业版OS。
### 1.开发接入
#### 1.1配置
1. 导入 sprite_dcg_sdk.aar
### 1.系统服务
行业版SDK提供服务列表
* 语音指令服务 `IInstructService`,应用可监听系统固定语音指令
* 离线TTS服务 `ITTSService`
* 系统功能服务 `ISystemFuncService` 提供系统设置功能
* 媒体服务 `ISpriteMediaService`,提供拍照、录像等功能
#### 1.1 异步获取服务
* 异步获取语音指令服务ServiceManager.getInstructService(Context context, IGetServiceCallback<IInstructService> callback)
* 异步获取TTS服务ServiceManager.getTTSService(Context context, IGetServiceCallback<ITTSService> callback)
* 异步获取系统功能服务ServiceManager.getSystemFuncService(Context context, IGetServiceCallback<ISystemFuncService> callback)
* 异步获取媒体服务ServiceManager.getSpriteMediaService(Context context, IGetServiceCallback<ISpriteMediaService> callback)
> 调用异步接口时如果服务没有bind会自动bind
```java
ServiceManager.getTTSService(this) {
it.playTtsMsg("让每个人享受科技Leave Nobody Behind!")
}
```
#### 1.1 同步获取服务
* bind 服务ServiceManager.
* 同步获取服务ServiceManager.getService(Class<T> serviceClass)
> 调用同步接口时必须提前bind如果服务没有bind或者bind还没完成时将返回 `null`
```java
ServiceManager.getService(ITTSService::class.java)?.playTtsMsg("让每个人享受科技Leave Nobody Behind!")
```
### 2. 语音指令服务 `IInstructService`
#### 2.1 可用语音指令
```java
const int INSTRUCT_WAKEUP = 1; // 若琪、乐琪
const int INSTRUCT_VOLUME_UP = 2; // 声音大一点
const int INSTRUCT_VOLUME_DOWN = 3; // 声音小一点
const int INSTRUCT_LIGHT_UP = 4; // 亮一点
const int INSTRUCT_LIGHT_DOWN = 5; // 暗一点
const int INSTRUCT_TAKE_PHOTO = 6; // 拍照
const int INSTRUCT_START_AUDIO_RECORD = 7; // 录音
const int INSTRUCT_STOP_AUDIO_RECORD = 8; // 结束录音
const int INSTRUCT_START_VIDEO_RECORD = 9; // 录像
const int INSTRUCT_STOP_VIDEO_RECORD = 10; // 结束录像
const int INSTRUCT_QUIT = 11; // 退出
```
#### 2.2 注册/反注册语音指令监听
```java
interface IInstructListener {
void onInstructReceived(int instruct);
}
void registerListener(IInstructListener listener);
void unregisterListener(IInstructListener listener);
```
例如:
```kotlin
private val mInstructListener = object : IInstructListener.Stub() { // 因为通过binder调用服务这里必须是Stub对象
override fun onInstructReceived(instruct: Int) {
Log.d("Instruct", "on receive instruct: $instruct")
// 注意: 服务的回调都不在主线程如果要操作UI请自己切换到主线程
// textView.text = "收到语音指令: $instruct"
}
}
override fun onResume() {
super.onResume()
ServiceManager.getInstructService(this) { it.registerListener(mInstructListener) }
}
override fun onPause() {
super.onPause()
ServiceManager.getInstructService(this) { it.unregisterListener(mInstructListener) }
}
```
### 3. TTS服务 `ITTSService`
#### 3.1 播放TTS
```java
void playTtsMsg(String ttsMsg);
```
### 4. 系统基础功能服务 `ISystemFuncService`
#### 4.1 获取/设置系统音量,音量值范围 `[0, 15]`
```java
int getVolumeSpecified();
void setVolumeSpecified(int value);
```
#### 4.2 获取/设置屏幕亮度,亮度值范围 `[0, 15]`
```java
int getBrightnessSpecified();
void setBrightnessSpecified(int value);
```
#### 4.3 获取设备序列号
```java
String getSn();
```
#### 4.4 获取/设置自动休眠时间(单位:毫秒)
```java
long getScreenAutoSleepTime();
void setScreenAutoSleepTime(long ms);
```
#### 4.5 唤醒设备
```java
void wakeUp();
```
#### 4.6 设备休眠
```java
void goToSleep();
```
#### 4.7 设备重启
```java
void reboot();
```
#### 4.8 设备关机
```java
void shutdown();
```
### 5. 多媒体服务 `ISpriteMediaService`
#### 5.1 多媒体Listener
```java
interface ISpriteMediaListener {
void onTakePicture(int errorCode, String path); // 拍照回调
void onVideoRecord(int errorCode, String path); // 录像回调
void onAudioRecord(int errorCode, String path); // 录音回调
}
// 错误码
interface ISpriteMediaService {
const int ERROR_NONE = 0;
const int ERROR_CAMERA_EXCEPTION = 1;
const int ERROR_P_SENSOR_NEAR = 2;
}
```
#### 5.2 设置监听
```java
void setMediaListener(ISpriteMediaListener listener);
```
#### 5.3 设置拍照分辨率
```java
void setPictureSize(int width, int height);
/* 可用分辨率
[4032x3024, 4000x3000, 4032x2268, 3264x2448, 3200x2400, 2268x3024, 2876x2156, 2688x2016, 2582x1936, 2400x1800, 1800x2400, 2560x1440, 2400x1350, 2048x1536, 2016x1512, 1920x1080, 1600x1200, 1440x1080, 1280x720, 720x1280, 1024x768, 800x600, 648x648, 854x480, 800x480, 640x480, 480x640, 352x288, 320x240, 320x180, 176x144]
*/
```
#### 5.4 设置录像分辨率
```java
void setVideoSize(int width, int height);
```
#### 5.5 拍照结果通过Lisener获取
```java
void takePicture();
```
#### 5.6 录像结果通过Lisener获取
```java
void startVideoRecord(); // 开始录像
void stopVideoRecord(); // 停止录像
boolean isVideoRecording(); // 是否正在录像
```
#### 5.7 录音结果通过Lisener获取
```java
void startAudioRecord();
void stopAudioRecord();
boolean isAudioRecording();
```
### 6. 自定义按键
#### 6.1 触控板标准按键
标准按键可通过onKeyDown、onKeyUp监听。可用键值如下
* 点击KeyEvent.KEYCODE_ENTER
* 前滑KeyEvent.KEYCODE_DPAD_DOWN、KeyEvent.KEYCODE_DPAD_RIGHT
* 后滑KeyEvent.KEYCODE_DPAD_UP、KeyEvent.KEYCODE_DPAD_LEFT
* 双击KeyEvent.KEYCODE_BACK
#### 6.2 特殊按键
特殊按键需通过广播注册监听,可监听事件:
* 长按触摸板SysKeyAction.TOUCH_BAR_LONG_PRESS
* 点击拍照键SysKeyAction.SPRITE_BUTTON_CLICK
* 按下/弹起拍照键SysKeyAction.SPRITE_BUTTON_DOWN、SysKeyAction.SPRITE_BUTTON_UP
* 长按拍照键SysKeyAction.SPRITE_BUTTON_LONG_PRESS
例如监听拍照键和触摸板长按事件:
```java
private final BroadcastReceiver mSysKeyReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (SysKeyAction.SPRITE_BUTTON_CLICK.equals(action)) {
} else if (SysKeyAction.TOUCH_BAR_LONG_PRESS.equals(action)) {
}
}
};
private void registerSysKeyReceiver() {
try {
IntentFilter filter = new IntentFilter();
filter.addAction(SysKeyAction.SPRITE_BUTTON_CLICK);
filter.addAction(SysKeyAction.TOUCH_BAR_LONG_PRESS);
registerReceiver(mSysKeyReceiver, filter)
} catch (Exception e) {
e.printStackTrace();
}
}
```

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

95
app/build.gradle.kts Normal file
View File

@ -0,0 +1,95 @@
import java.io.FileInputStream
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
val keystores: Properties? = loadKeystoreProperties("keystore.properties")
android {
namespace = "com.yuanxuan.rokid"
compileSdk {
version = release(36)
}
defaultConfig {
applicationId = "com.yuanxuan.rokid"
minSdk = 29
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
keystores?.let {
signingConfigs {
create("release") {
storeFile = rootProject.file(it["storeFile"] as String)
storePassword = it["storePassword"] as String
keyAlias = it["keyAlias"] as String
keyPassword = it["keyPassword"] as String
}
}
}
buildTypes {
debug {
signingConfig = signingConfigs.getByName("release")
}
release {
signingConfig = signingConfigs.getByName("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 {
viewBinding = true
buildConfig = true
}
}
dependencies {
implementation(fileTree("libs") { include("*.aar", "*.jar") })
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.leanback)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.logger.timber)
implementation(libs.okhttp.logging.interceptor)
implementation(libs.androidx.work.ktx)
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
implementation(libs.lottie)
implementation(libs.androidx.navigation.fragment)
implementation(libs.androidx.navigation.ui)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
fun loadKeystoreProperties(filename: String): Properties? {
val keystorePropertiesFile = file("${project.rootDir}/$filename")
return if (keystorePropertiesFile.exists()) {
val keystoreProperties = Properties()
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
keystoreProperties
} else {
null
}
}

Binary file not shown.

21
app/proguard-rules.pro vendored Normal file
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

View File

@ -0,0 +1,24 @@
package com.yuanxuan.rokid
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.yuanxuan.rokid", appContext.packageName)
}
}

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name=".RokidApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Rokid"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name="com.yuanxuan.rokid.keeplive.KeepLiveService" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/parovider_path" />
</provider>
</application>
</manifest>

View File

@ -0,0 +1,132 @@
package com.yuanxuan.rokid
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
import androidx.activity.addCallback
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavOptions
import androidx.navigation.fragment.NavHostFragment
import com.yuanxuan.rokid.databinding.ActivityMainBinding
import com.yuanxuan.rokid.extension.fadeIn
import com.yuanxuan.rokid.extension.fadeOut
import com.yuanxuan.rokid.keeplive.KeepLiveService
import com.yuanxuan.rokid.network.websocket.WebSocketConnectionState
import com.yuanxuan.rokid.network.websocket.WebSocketEvent
import com.yuanxuan.rokid.ui.NoticeFragment
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel by viewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
observer()
/**
* 拦截返回键事件防止返回到桌面
*/
onBackPressedDispatcher.addCallback {
}
/**
* 这个服务保证心跳包准时发
*/
val serviceIntent = Intent(this, KeepLiveService::class.java)
startForegroundService(serviceIntent)
/**
* 出路服务端事件
*/
lifecycleScope.launch {
viewModel.socketEvent.collect { event ->
when (event) {
is WebSocketEvent.Notice -> {
viewModel.wakeUp()
delay(500)
navigationToast(event.msg)
}
WebSocketEvent.Unknow -> {}
}
}
}
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return if (viewModel.uiState.value.isVoiceInputVisible) {
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
viewModel.quitInstructReceived()
}
true
} else {
super.dispatchKeyEvent(event)
}
}
private fun navigationToast(toast: String) {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val options = NavOptions.Builder()
.setEnterAnim(android.R.anim.fade_in)
.setExitAnim(android.R.anim.fade_out)
.setPopEnterAnim(android.R.anim.fade_in)
.setPopExitAnim(android.R.anim.fade_out)
.build()
val bundle = Bundle().apply {
putString(NoticeFragment.TOAST_MESSAGE, toast)
}
val navController = navHostFragment.navController
navController.navigate(R.id.notice_fragment, bundle, options)
}
private fun observer() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
binding.batteryLevel.text =
resources.getString(R.string.status_bar_battery, uiState.batteryLevel)
binding.batteryLevelIv.setImageLevel(uiState.batteryLevel)
binding.wifiIv.setImageLevel(uiState.wifiLevel)
if (uiState.isVoiceInputVisible) {
binding.lottieVoiceInput.playAnimation()
binding.lottieVoiceInput.fadeIn(300)
} else {
binding.lottieVoiceInput.cancelAnimation()
binding.lottieVoiceInput.fadeOut(300)
}
if (uiState.connectStatus == WebSocketConnectionState.CONNECTED) {
binding.socketStatus.text =
resources.getString(R.string.socket_status_connected)
} else {
binding.socketStatus.text =
resources.getString(R.string.socket_status_connecting)
}
}
}
}
}
}

View File

@ -0,0 +1,78 @@
package com.yuanxuan.rokid
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.yuanxuan.rokid.dependencies.AppDependencies.webSocketManager
import com.yuanxuan.rokid.dependencies.AppDependencies.deviceServiceManager
import com.yuanxuan.rokid.device.DeviceServiceManager
import com.yuanxuan.rokid.network.websocket.WebSocketConnectionState
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
class MainViewModel : ViewModel() {
private val _keyEventFlow = MutableSharedFlow<KeyEvent>()
val keyEventFlow = _keyEventFlow.asSharedFlow()
val uiState = combine(
deviceServiceManager.batteryPercentage,
deviceServiceManager.wifiState,
deviceServiceManager.instructState,
webSocketManager.connectFlow
) { battery, wifi, instruct, connect ->
MainUiState(
batteryLevel = battery,
wifiLevel = if (wifi is DeviceServiceManager.WifiState.Connected) wifi.level else 0,
isVoiceInputVisible = instruct == DeviceServiceManager.InstructState.WaitingInstructState,
connectStatus = connect
)
}.stateIn(
scope = viewModelScope,
initialValue = MainUiState(
batteryLevel = 0,
wifiLevel = 0,
isVoiceInputVisible = false,
connectStatus = WebSocketConnectionState.DISCONNECTED
),
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds),
)
val socketEvent = webSocketManager.socketEventFlow
fun quitInstructReceived() {
deviceServiceManager.quitInstructReceived()
}
fun onKeyEventDispatched(event: KeyEvent) {
viewModelScope.launch {
_keyEventFlow.emit(event)
}
}
fun wakeUp() {
deviceServiceManager.wakeup()
}
fun playTTS(tts: String) {
deviceServiceManager.playTTS(tts)
}
sealed interface KeyEvent {
data object DpadRight : KeyEvent //前滑
data object DpadLeft : KeyEvent //后滑
data object Enter : KeyEvent //前滑
}
data class MainUiState(
val batteryLevel: Int,
val wifiLevel: Int,
val isVoiceInputVisible: Boolean,
val connectStatus: WebSocketConnectionState,
)
}

View File

@ -0,0 +1,36 @@
package com.yuanxuan.rokid
import android.app.Application
import com.yuanxuan.rokid.dependencies.AppDependencies
import com.yuanxuan.rokid.dependencies.ApplicationDependencyProvider
import com.yuanxuan.rokid.network.websocket.WebSocketReconnectWorker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import timber.log.Timber
class RokidApplication : Application() {
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG)
Timber.plant(tree = Timber.DebugTree())
AppDependencies.init(this, ApplicationDependencyProvider(this, applicationScope))
/**
* 启动APP必须先获取到SN 后面网络依赖
*/
applicationScope.launch {
runCatching {
AppDependencies.deviceServiceManager.getSn()
}.onSuccess {
WebSocketReconnectWorker.schedule(this@RokidApplication)
}.onFailure {
Timber.e(it)
}
}
}
}

View File

@ -0,0 +1,74 @@
package com.yuanxuan.rokid.dependencies
import android.app.Application
import com.yuanxuan.rokid.device.DeviceServiceManager
import com.yuanxuan.rokid.network.http.ApiRepository
import com.yuanxuan.rokid.network.http.ApiService
import com.yuanxuan.rokid.network.http.DownloadApiRepository
import com.yuanxuan.rokid.network.http.DownloadRetrofitClient
import com.yuanxuan.rokid.network.http.RetrofitClient
import com.yuanxuan.rokid.network.websocket.WebSocketManager
import com.yuanxuan.rokid.toast.ToastManager
/**
* 项目的服务管理器所有依赖集中管理
*/
object AppDependencies {
private lateinit var _application: Application
private lateinit var provider: Provider
val application: Application
get() = _application
fun init(application: Application, provider: Provider) {
if (this::_application.isInitialized || this::provider.isInitialized) {
return
}
_application = application
AppDependencies.provider = provider
}
val deviceServiceManager by lazy {
provider.provideDeviceServiceManager()
}
val webSocketManager by lazy {
provider.provideWebSocketManager()
}
private val retrofitClient by lazy {
provider.provideRetrofitClient()
}
private val downloadRetrofitClient by lazy {
provider.provideDownloadRetrofitClient()
}
val requestApi by lazy {
provider.provideApiRepository(
apiService = retrofitClient.apiService
)
}
val downloadApi by lazy {
provider.provideDownloadApiRepository(
apiService = downloadRetrofitClient.apiService
)
}
val toastManager by lazy {
provider.provideToastManager()
}
interface Provider {
fun provideDeviceServiceManager(): DeviceServiceManager
fun provideWebSocketManager(): WebSocketManager
fun provideRetrofitClient(): RetrofitClient
fun provideDownloadRetrofitClient(): DownloadRetrofitClient
fun provideApiRepository(apiService: ApiService): ApiRepository
fun provideDownloadApiRepository(apiService: ApiService): DownloadApiRepository
fun provideToastManager(): ToastManager
}
}

View File

@ -0,0 +1,52 @@
package com.yuanxuan.rokid.dependencies
import android.app.Application
import com.yuanxuan.rokid.device.DeviceServiceManager
import com.yuanxuan.rokid.network.http.ApiRepository
import com.yuanxuan.rokid.network.http.ApiService
import com.yuanxuan.rokid.network.http.DownloadApiRepository
import com.yuanxuan.rokid.network.http.DownloadRetrofitClient
import com.yuanxuan.rokid.network.http.RetrofitClient
import com.yuanxuan.rokid.network.websocket.WebSocketManager
import com.yuanxuan.rokid.toast.ToastManager
import kotlinx.coroutines.CoroutineScope
class ApplicationDependencyProvider(val context: Application, val scope: CoroutineScope) :
AppDependencies.Provider {
override fun provideDeviceServiceManager(): DeviceServiceManager {
return DeviceServiceManager(context)
}
override fun provideWebSocketManager(): WebSocketManager {
return WebSocketManager(
context = context,
scope = scope
)
}
override fun provideRetrofitClient(): RetrofitClient {
return RetrofitClient()
}
override fun provideDownloadRetrofitClient(): DownloadRetrofitClient {
return DownloadRetrofitClient()
}
override fun provideApiRepository(apiService: ApiService): ApiRepository {
return ApiRepository(
apiService = apiService
)
}
override fun provideDownloadApiRepository(apiService: ApiService): DownloadApiRepository {
return DownloadApiRepository(apiService = apiService)
}
override fun provideToastManager(): ToastManager {
return ToastManager(
context = context,
scope = scope
)
}
}

View File

@ -0,0 +1,380 @@
package com.yuanxuan.rokid.device
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.os.BatteryManager
import com.rokid.dcg.sprite.service.IInstructListener
import com.rokid.dcg.sprite.service.IInstructService
import com.rokid.dcg.sprite.service.ISpriteMediaService
import com.rokid.dcg.sprite.service.ISystemFuncService
import com.rokid.dcg.sprite.service.ITTSService
import com.rokid.dcg.sprite.service.ServiceManager
import com.rokid.dcg.sprite.syskey.SysKeyAction
import com.yuanxuan.rokid.toast.ToastUtils
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class DeviceServiceManager(val context: Application) : ConnectivityManager.NetworkCallback() {
companion object {
/**
* 语音指令等待超时
*/
private const val INSTRUCT_WAITE_TIME = 15000L
const val MAX_VOLUME = 15
const val MAX_BRIGHTNESS = 15
}
private var instructService: IInstructService? = null
private var ttsService: ITTSService? = null
private var systemFuncService: ISystemFuncService? = null
private var spriteMediaService: ISpriteMediaService? = null
private val _instructState = MutableStateFlow<InstructState>(InstructState.None)
val instructState = _instructState.asStateFlow()
/**
* 系统电量
*/
private val _batteryPercentage = MutableStateFlow(getBatteryLevel(context))
val batteryPercentage = _batteryPercentage.asStateFlow()
/**
* wifi
*/
private val _wifiState: MutableStateFlow<WifiState> = MutableStateFlow(WifiState.Unconnected)
val wifiState = _wifiState.asStateFlow()
private val wifiManager by lazy {
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
}
/**
* 音量
*/
private val _volume: MutableStateFlow<Int> = MutableStateFlow(0)
val volume = _volume.asStateFlow()
/**
* 亮度
*/
private val _brightness: MutableStateFlow<Int> = MutableStateFlow(0)
val brightness = _brightness.asStateFlow()
lateinit var sn: String
private set
val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception, "全局捕获协程异常:${exception.message}")
}
private val scope =
CoroutineScope(SupervisorJob() + Dispatchers.Main + coroutineExceptionHandler)
private var wakeTimeoutJob: Job? = null
init {
val filter = IntentFilter().apply {
addAction(SysKeyAction.SPRITE_BUTTON_DOWN)
addAction(SysKeyAction.SPRITE_BUTTON_UP)
addAction(SysKeyAction.SPRITE_BUTTON_CLICK)
addAction(SysKeyAction.SPRITE_BUTTON_LONG_PRESS)
addAction(SysKeyAction.SPRITE_BUTTON_VERY_VERY_LONG_PRESS)
addAction(SysKeyAction.TOUCH_BAR_LONG_PRESS)
addAction(Intent.ACTION_BATTERY_CHANGED) // 监听电量
}
context.registerReceiver(SysKeyReceiver(), filter)
scope.launch {
instructState.collect {
Timber.d("语音指令状态 $it")
when (it) {
InstructState.None -> instructService?.unregisterListener(instructListener)
InstructState.WaitingInstructState -> {
instructService?.registerListener(instructListener)
startWakeTimeoutCountdown()
}
}
}
}
/**
* 监听wifi状态
*/
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build()
connectivityManager.registerNetworkCallback(request, this)
}
val instructListener = object : IInstructListener.Stub() {
override fun onInstructReceived(p0: Int) {
Timber.d("接受到语音指令$p0")
if (p0 != IInstructService.INSTRUCT_QUIT) {
startWakeTimeoutCountdown()
}
when (p0) {
IInstructService.INSTRUCT_WAKEUP -> {
}
IInstructService.INSTRUCT_VOLUME_UP -> {
volumeAdd()
}
IInstructService.INSTRUCT_VOLUME_DOWN -> {
volumeSubtract()
}
IInstructService.INSTRUCT_LIGHT_UP -> {
brightnessAdd()
}
IInstructService.INSTRUCT_LIGHT_DOWN -> {
brightnessSubtract()
}
IInstructService.INSTRUCT_TAKE_PHOTO -> {
}
IInstructService.INSTRUCT_START_AUDIO_RECORD -> {
}
IInstructService.INSTRUCT_STOP_AUDIO_RECORD -> {
}
IInstructService.INSTRUCT_START_VIDEO_RECORD -> {
}
IInstructService.INSTRUCT_STOP_VIDEO_RECORD -> {
}
IInstructService.INSTRUCT_QUIT -> {
_instructState.update { InstructState.None }
}
}
}
}
/**
* [INSTRUCT_WAITE_TIME] 一段时间无新指令 退出等待状态
*/
private fun startWakeTimeoutCountdown() {
wakeTimeoutJob?.cancel()
wakeTimeoutJob = scope.launch {
delay(INSTRUCT_WAITE_TIME)
_instructState.update { InstructState.None }
}
}
fun quitInstructReceived() {
_instructState.update { InstructState.None }
}
fun wakeup() {
systemFuncServiceTodo { service ->
service.wakeUp()
}
}
fun playTTS(msg: String) {
ttsServiceTodo {
ttsService?.playTtsMsg(msg)
}
}
fun volumeAdd() {
systemFuncServiceTodo { service ->
val volume = (service.volumeSpecified + 1).coerceIn(0, 15)
service.volumeSpecified = volume
_volume.update { volume }
}
}
fun volumeSubtract() {
systemFuncServiceTodo { service ->
val volume = (service.volumeSpecified - 1).coerceIn(0, 15)
service.volumeSpecified = volume
_volume.update { volume }
}
}
fun brightnessAdd() {
systemFuncServiceTodo { service ->
val brightness = (service.brightnessSpecified + 1).coerceIn(0, 15)
service.brightnessSpecified = brightness
_brightness.update { brightness }
}
}
fun brightnessSubtract() {
systemFuncServiceTodo { service ->
val brightness = (service.brightnessSpecified - 1).coerceIn(0, 15)
service.brightnessSpecified = brightness
_brightness.update { brightness }
}
}
suspend fun getSn() = suspendCancellableCoroutine { continuation ->
ServiceManager.getSystemFuncService(context) { service ->
if (service == null) {
continuation.resumeWithException(Throwable("没有拿到SN 要重启APP"))
return@getSystemFuncService
}
systemFuncService = service
sn = service.sn
_volume.update { service.volumeSpecified }
_brightness.update { service.brightnessSpecified }
continuation.resume(sn)
}
}
private inline fun ttsServiceTodo(crossinline todo: (ITTSService) -> Unit) {
if (ttsService == null) {
ServiceManager.getTTSService(context) { service ->
if (service == null) {
Timber.d("设备初始化TTS服务失败")
return@getTTSService
}
ttsService = service
todo.invoke(service)
}
} else {
todo.invoke(ttsService!!)
}
}
private inline fun systemFuncServiceTodo(crossinline todo: (ISystemFuncService) -> Unit) {
if (systemFuncService == null) {
ServiceManager.getSystemFuncService(context) { service ->
if (service == null) {
Timber.d("设备初始化系统功能服务失败")
return@getSystemFuncService
}
systemFuncService = service
todo.invoke(service)
}
} else {
todo.invoke(systemFuncService!!)
}
}
private inline fun instructServiceTodo(crossinline todo: (IInstructService) -> Unit) {
if (instructService == null) {
ServiceManager.getInstructService(context) { service ->
if (service == null) {
Timber.d("设备初始化语音指令服务失败")
return@getInstructService
}
instructService = service
todo.invoke(service)
}
} else {
todo.invoke(instructService!!)
}
}
private fun getBatteryLevel(context: Context): Int {
val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
return bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
}
inner class SysKeyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val action = intent?.action ?: return
when (action) {
Intent.ACTION_BATTERY_CHANGED -> {
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
if (level == -1 || scale == -1)
return
val batteryPercentage = (level / scale.toFloat() * 100).toInt()
_batteryPercentage.update { batteryPercentage }
}
SysKeyAction.SPRITE_BUTTON_DOWN -> {
Timber.d("按下拍照键 SPRITE_BUTTON_DOWN")
}
SysKeyAction.SPRITE_BUTTON_UP -> {
Timber.d("抬起拍照键 SPRITE_BUTTON_UP")
}
SysKeyAction.SPRITE_BUTTON_CLICK -> {
ToastUtils.showLong("SysKeyAction.SPRITE_BUTTON_CLICK")
Timber.d("点击拍照键 SPRITE_BUTTON_CLICK")
}
SysKeyAction.SPRITE_BUTTON_LONG_PRESS -> {
Timber.d("长按拍照键 SPRITE_BUTTON_LONG_PRESS")
}
SysKeyAction.SPRITE_BUTTON_VERY_VERY_LONG_PRESS -> {
Timber.d("超级长的长按拍照键 SPRITE_BUTTON_VERY_VERY_LONG_PRESS")
}
SysKeyAction.TOUCH_BAR_LONG_PRESS -> {
Timber.d("长按触摸板 TOUCH_BAR_LONG_PRESS")
instructServiceTodo {
_instructState.update { InstructState.WaitingInstructState }
}
}
}
}
}
override fun onLost(network: Network) {
_wifiState.update { WifiState.Unconnected }
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
_wifiState.update {
val level = wifiManager.calculateSignalLevel(networkCapabilities.signalStrength)
WifiState.Connected(
/**
* maxSignalLevel 4 level区间应该是 0-3 这里却返回了4 限制一下
* 参考 [com.yuanxuan.rokid.R.drawable.wifi_level_list] 0 为wifi断开
*/
level = level.coerceAtMost(wifiManager.maxSignalLevel - 1) + 1
)
}
}
sealed interface InstructState {
data object None : InstructState
data object WaitingInstructState : InstructState
}
sealed interface WifiState {
data object Unconnected : WifiState
data class Connected(val level: Int) : WifiState
}
}

View File

@ -0,0 +1,12 @@
package com.yuanxuan.rokid.extension
import android.content.res.Resources
import android.util.TypedValue
fun Number.dpToPx(): Float {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this.toFloat(),
Resources.getSystem().displayMetrics
)
}

View File

@ -0,0 +1,37 @@
package com.yuanxuan.rokid.extension
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.view.View
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
fun View.animateTranslationY(translationY: Float, duration: Int) {
animate()
.translationY(translationY)
.setDuration(250)
.start()
}
fun View.fadeIn(duration: Long) {
if (this.isVisible) return
this.alpha = 0f
this.isVisible = true
this.animate()
.alpha(1f)
.setDuration(duration)
.setListener(null)
}
fun View.fadeOut(duration: Long) {
if (this.isVisible.not()) return
this.animate()
.alpha(0f)
.setDuration(duration)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
this@fadeOut.isInvisible = true
}
})
}

View File

@ -0,0 +1,82 @@
package com.yuanxuan.rokid.keeplive
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import com.yuanxuan.rokid.R
/**
* 提高APP进程优先级 保持长链接状态
*/
class KeepLiveService : Service() {
private lateinit var wakeLock: PowerManager.WakeLock
companion object {
const val NOTIFICATION_CHANNEL_ID = "KeepLiveServiceChannel"
const val NOTIFICATION_CHANNEL_NAME = NOTIFICATION_CHANNEL_ID
const val NOTIFICATION_ID = 101
const val WAKE_LOCK_TAG = "KeepLiveService:WakeLock"
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
createNotificationChannel()
initWakeLock()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(NOTIFICATION_ID, createNotification())
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
if (::wakeLock.isInitialized && wakeLock.isHeld) {
wakeLock.release()
}
}
private fun initWakeLock() {
wakeLock =
(getSystemService(POWER_SERVICE) as PowerManager)
.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
WAKE_LOCK_TAG
)
.apply {
if (isHeld.not())
acquire()
}
}
private fun createNotificationChannel() {
// 最低支持Android 9 就不判断了
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
NOTIFICATION_CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
private fun createNotification(): Notification {
return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setContentTitle(NOTIFICATION_CHANNEL_NAME)
.setContentText(NOTIFICATION_CHANNEL_NAME)
.setSmallIcon(R.mipmap.ic_launcher)
.setOngoing(true)
.build()
}
}

View File

@ -0,0 +1,92 @@
package com.yuanxuan.rokid.network
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.content.FileProvider
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.yuanxuan.rokid.dependencies.AppDependencies
import timber.log.Timber
import java.io.File
import java.util.concurrent.TimeUnit
class ApkDownloadWorker(appContext: Context, params: WorkerParameters) :
CoroutineWorker(appContext, params) {
companion object {
const val WORE_NAME = "ApkDownloadWorker"
fun schedule(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val request = OneTimeWorkRequestBuilder<ApkDownloadWorker>()
.setConstraints(constraints)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
//重试策略 一共5次 线性增加间隔时长
.setBackoffCriteria(
backoffPolicy = BackoffPolicy.LINEAR,
backoffDelay = 30,
timeUnit = TimeUnit.SECONDS
)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
uniqueWorkName = WORE_NAME,
existingWorkPolicy = ExistingWorkPolicy.REPLACE,
request = request
)
}
}
override suspend fun doWork(): Result {
Timber.d("重试次数 $runAttemptCount")
// 重试限制5次
if (runAttemptCount >= 5) {
return Result.failure()
}
AppDependencies.downloadApi.apkDownload("http://192.168.2.83:8000/download/app-release.apk")
.fold(
onSuccess = {
installApkSilent(it)
return Result.success()
},
onFailure = {
return Result.retry()
}
)
}
fun installApkSilent(apkFile: File) {
if (!apkFile.exists()) {
Timber.e("APK 文件不存在,无法安装: ${apkFile.absolutePath}")
return
}
val intent = Intent(Intent.ACTION_VIEW)
val uri: Uri = FileProvider.getUriForFile(
applicationContext,
"${applicationContext.packageName}.fileProvider",
apkFile
)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setDataAndType(uri, "application/vnd.android.package-archive")
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
applicationContext.startActivity(intent)
} catch (e: Exception) {
Timber.e(e, "启动安装程序失败")
}
}
}

View File

@ -0,0 +1,39 @@
package com.yuanxuan.rokid.network
import timber.log.Timber
import java.net.Inet4Address
import java.net.NetworkInterface
object NetUtils {
// fun getBaseUrl() = "http://${getLocalIPV4address()}.70:7070"
fun getBaseUrl() = "http://${getLocalIPV4address()}.13:8765"
/**
* 获取局域网IP
*/
fun getLocalIPV4address(): String? {
try {
val networkInterfaces = NetworkInterface.getNetworkInterfaces()
while (networkInterfaces.hasMoreElements()) {
val networkInterface = networkInterfaces.nextElement()
val inetAddresses = networkInterface.inetAddresses
while (inetAddresses.hasMoreElements()) {
val inetAddress = inetAddresses.nextElement()
if (!inetAddress.isLoopbackAddress && inetAddress is Inet4Address) {
val fullIp = inetAddress.hostAddress
if (fullIp.isNullOrEmpty())
return null
val lastDotIndex = fullIp.lastIndexOf(".")
// 返回第一个找到的IPv4地址
return fullIp.take(lastDotIndex)
}
}
}
} catch (e: Exception) {
Timber.e(e, "获取局域网IP地址时发生异常")
}
return null
}
}

View File

@ -0,0 +1,14 @@
package com.yuanxuan.rokid.network.bean
data class ApiResponse<T>(
val code: String,
val message: String,
val data: T
) {
fun isSuccess() = code == "200"
}
data class Test(
val userId: String,
val name: String
)

View File

@ -0,0 +1,22 @@
package com.yuanxuan.rokid.network.bean
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class WebSocketResponse(
@SerializedName("MsgType")
val msgType: Int,
@SerializedName("Msg")
val msg: JsonElement
) {
enum class MsgType(val value: Int) {
Notice(0);
companion object {
fun fromValue(value: Int) = entries.firstOrNull { it.value == value }
}
}
}

View File

@ -0,0 +1,12 @@
package com.yuanxuan.rokid.network.http
import com.yuanxuan.rokid.network.bean.Test
class ApiRepository(private val apiService: ApiService) {
suspend fun test(): Test? {
apiService.test()
return null
}
}

View File

@ -0,0 +1,21 @@
package com.yuanxuan.rokid.network.http
import com.yuanxuan.rokid.network.NetUtils
import com.yuanxuan.rokid.network.bean.ApiResponse
import com.yuanxuan.rokid.network.bean.Test
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Streaming
import retrofit2.http.Url
interface ApiService {
@GET
suspend fun test(@Url url: String = "${NetUtils.getBaseUrl()}/test"): ApiResponse<Test>
@Streaming
@GET
suspend fun downloadFiles(@Url fileUrl: String): Response<ResponseBody>
}

View File

@ -0,0 +1,68 @@
package com.yuanxuan.rokid.network.http
import com.yuanxuan.rokid.dependencies.AppDependencies
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.ResponseBody
import retrofit2.HttpException
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
class DownloadApiRepository(private val apiService: ApiService) {
suspend fun apkDownload(url: String): Result<File> = withContext(Dispatchers.IO) {
runCatching {
val response = apiService.downloadFiles(url)
if (!response.isSuccessful) throw HttpException(response)
val body = response.body() ?: throw IOException("下载失败:响应成功但响应体为空。")
val file = File(AppDependencies.application.externalCacheDir, "rokid.apk")
val isSuccess = writeResponseBodyToDisk(body, file)
if (!isSuccess) {
throw IOException("将文件写入磁盘时失败。")
}
file
}
}
private fun writeResponseBodyToDisk(body: ResponseBody, file: File): Boolean {
var inputStream: InputStream? = null
var outputStream: OutputStream? = null
try {
val fileReader = ByteArray(102400)
val fileSize = body.contentLength()
var fileSizeDownloaded: Long = 0
inputStream = body.byteStream()
outputStream = FileOutputStream(file)
while (true) {
val read = inputStream.read(fileReader)
if (read == -1) {
break
}
outputStream.write(fileReader, 0, read)
fileSizeDownloaded += read.toLong()
val progress = (fileSizeDownloaded * 100 / fileSize).toInt()
Timber.d("文件下载中... $progress%")
}
outputStream.flush()
Timber.d("文件下载成功: ${file.path}")
return true
} catch (e: IOException) {
Timber.e(e, "写入文件时发生 IO 异常")
return false
} finally {
inputStream?.close()
outputStream?.close()
}
}
}

View File

@ -0,0 +1,38 @@
package com.yuanxuan.rokid.network.http
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
class DownloadRetrofitClient {
private val okHttpClient: OkHttpClient by lazy {
val httpLoggingInterceptor by lazy {
HttpLoggingInterceptor { message ->
Timber.tag("OkHttp").d(message)
}.apply {
level = HttpLoggingInterceptor.Level.BASIC
}
}
OkHttpClient.Builder()
.connectTimeout(15.seconds)
.readTimeout(30.seconds)
.writeTimeout(30.seconds)
.addInterceptor(httpLoggingInterceptor)
.build()
}
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
.client(okHttpClient)
.baseUrl("http://localhost/")
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
.build()
}
val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java)
}
}

View File

@ -0,0 +1,5 @@
package com.yuanxuan.rokid.network.http
import java.io.IOException
class NetworkException(message: String, cause: Throwable? = null) : IOException(message, cause)

View File

@ -0,0 +1,38 @@
package com.yuanxuan.rokid.network.http
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
class RetrofitClient {
private val okHttpClient: OkHttpClient by lazy {
val httpLoggingInterceptor by lazy {
HttpLoggingInterceptor { message ->
Timber.tag("OkHttp").d(message)
}.apply {
level = HttpLoggingInterceptor.Level.BODY
}
}
OkHttpClient.Builder()
.connectTimeout(15.seconds)
.readTimeout(30.seconds)
.writeTimeout(30.seconds)
.addInterceptor(httpLoggingInterceptor)
.build()
}
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
.client(okHttpClient)
.baseUrl("http://localhost/")
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
.build()
}
val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java)
}
}

View File

@ -0,0 +1,77 @@
package com.yuanxuan.rokid.network.websocket
import com.google.gson.Gson
import com.yuanxuan.rokid.network.NetUtils
import com.yuanxuan.rokid.network.bean.WebSocketResponse
import com.yuanxuan.rokid.network.websocket.WebSocketConnection.Companion.DEFAULT_SEND_TIMEOUT
import com.yuanxuan.rokid.network.websocket.WebSocketConnection.Companion.PING_INTERVAL_TIME
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import timber.log.Timber
class OkHttpWebSocketConnection(val scope: CoroutineScope) : WebSocketListener(),
WebSocketConnection {
private val _webSocketConnectionStateFlow: MutableStateFlow<WebSocketConnectionState> =
MutableStateFlow(WebSocketConnectionState.DISCONNECTED)
val webSocketConnectionStateFlow = _webSocketConnectionStateFlow.asStateFlow()
private val _eventFlow = MutableSharedFlow<WebSocketEvent>()
val eventFlow = _eventFlow.asSharedFlow()
private var client: WebSocket? = null
@Synchronized
override fun connect(sn: String) {
val okHttpClient = OkHttpClient.Builder()
.readTimeout(DEFAULT_SEND_TIMEOUT)
.pingInterval(PING_INTERVAL_TIME)
.build()
val request = Request.Builder().url(NetUtils.getBaseUrl())
.addHeader("sn", sn).build()
_webSocketConnectionStateFlow.update {
WebSocketConnectionState.CONNECTING
}
client = okHttpClient.newWebSocket(request, this)
}
override fun onMessage(webSocket: WebSocket, text: String) {
Timber.d(text)
val response = Gson().fromJson(text, WebSocketResponse::class.java)
val msgType = WebSocketResponse.MsgType.fromValue(response.msgType)
val event = when (msgType) {
WebSocketResponse.MsgType.Notice -> {
WebSocketEvent.Notice(response.msg.asString)
}
null -> {
WebSocketEvent.Unknow
}
}
scope.launch {
_eventFlow.emit(event)
}
}
override fun onOpen(webSocket: WebSocket, response: Response) {
_webSocketConnectionStateFlow.update { WebSocketConnectionState.CONNECTED }
Timber.d("websocket onOpen")
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Timber.e("websocket断开连接 $t")
_webSocketConnectionStateFlow.update { WebSocketConnectionState.FAILED }
}
}

View File

@ -0,0 +1,27 @@
package com.yuanxuan.rokid.network.websocket
import kotlin.time.Duration.Companion.seconds
interface WebSocketConnection {
companion object {
val DEFAULT_SEND_TIMEOUT = 10.seconds
val PING_INTERVAL_TIME = 30.seconds
}
fun connect(sn: String)
// fun isDead(): Boolean
// fun disconnect()
// fun sendRequest(request: WebSocketRequestMessage) {
// return sendRequest(request, DEFAULT_SEND_TIMEOUT.inWholeSeconds)
// }
// fun sendRequest(
// request: WebSocketRequestMessage,
// timeoutSeconds: Long
// )
}

View File

@ -0,0 +1,15 @@
package com.yuanxuan.rokid.network.websocket;
public enum WebSocketConnectionState {
DISCONNECTED,
CONNECTING,
CONNECTED,
DISCONNECTING,
AUTHENTICATION_FAILED,
REMOTE_DEPRECATED,
FAILED;
public boolean isFailure() {
return this == AUTHENTICATION_FAILED || this == REMOTE_DEPRECATED || this == FAILED;
}
}

View File

@ -0,0 +1,7 @@
package com.yuanxuan.rokid.network.websocket
sealed interface WebSocketEvent {
data class Notice(val msg: String) : WebSocketEvent
data object Unknow : WebSocketEvent
}

View File

@ -0,0 +1,41 @@
package com.yuanxuan.rokid.network.websocket
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class WebSocketManager(context: Context, scope: CoroutineScope) {
private val webSocketConnection = OkHttpWebSocketConnection(scope = scope)
val socketEventFlow = webSocketConnection.eventFlow
val connectFlow = webSocketConnection.webSocketConnectionStateFlow
init {
scope.launch {
webSocketConnection.webSocketConnectionStateFlow.collectLatest { state ->
when (state) {
WebSocketConnectionState.DISCONNECTED,
WebSocketConnectionState.CONNECTING,
WebSocketConnectionState.CONNECTED,
WebSocketConnectionState.DISCONNECTING,
WebSocketConnectionState.AUTHENTICATION_FAILED,
WebSocketConnectionState.REMOTE_DEPRECATED -> {
}
WebSocketConnectionState.FAILED -> {
WebSocketReconnectWorker.schedule(context)
}
}
}
}
}
fun connect(sn: String) {
webSocketConnection.connect(sn)
}
}

View File

@ -0,0 +1,48 @@
package com.yuanxuan.rokid.network.websocket
import android.content.Context
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.yuanxuan.rokid.dependencies.AppDependencies
import kotlinx.coroutines.delay
import timber.log.Timber
class WebSocketReconnectWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(
context,
workerParams
) {
override suspend fun doWork(): Result {
/**
* 第一次启动给点时间连wifi重连等5s
*/
delay(5000)
Timber.d("尝试开始重连")
AppDependencies.webSocketManager.connect(AppDependencies.deviceServiceManager.sn)
return Result.success()
}
companion object {
const val WORE_NAME = "WebSocketReconnectWorker"
fun schedule(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val request = OneTimeWorkRequestBuilder<WebSocketReconnectWorker>()
.setConstraints(constraints)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
uniqueWorkName = WORE_NAME,
existingWorkPolicy = ExistingWorkPolicy.REPLACE,
request = request
)
}
}
}

View File

@ -0,0 +1,37 @@
package com.yuanxuan.rokid.toast
import android.content.Context
import android.view.Gravity
import android.widget.Toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ToastManager(
val context: Context,
val scope: CoroutineScope
) {
fun showShort(message: String?) {
message?.let {
scope.launch(Dispatchers.Main) {
Toast.makeText(context, message, Toast.LENGTH_SHORT)
.apply {
setGravity(Gravity.CENTER, 0, 0)
}.show()
}
}
}
fun showLong(message: String?) {
message?.let {
scope.launch(Dispatchers.Main) {
Toast.makeText(context, message, Toast.LENGTH_LONG)
.apply {
setGravity(Gravity.CENTER, 0, 0)
}.show()
}
}
}
}

View File

@ -0,0 +1,19 @@
package com.yuanxuan.rokid.toast
import com.yuanxuan.rokid.dependencies.AppDependencies
object ToastUtils {
private val toastManager by lazy {
AppDependencies.toastManager
}
fun showShort(messages: String?) {
toastManager.showShort(messages)
}
fun showLong(messages: String?) {
toastManager.showLong(messages)
}
}

View File

@ -0,0 +1,68 @@
package com.yuanxuan.rokid.ui
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.yuanxuan.rokid.R
import com.yuanxuan.rokid.databinding.FragmentSettingBinding
import kotlinx.coroutines.launch
class BrightnessFragment : Fragment() {
private lateinit var binding: FragmentSettingBinding
private val viewModel by viewModels<SettingViewModel>()
private val adapter by lazy {
SeekBarAdapter()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentSettingBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.title.text = resources.getString(R.string.setting_brightness_title)
binding.tip.text = resources.getString(R.string.setting_brightness_tip)
binding.recyclerView.adapter = adapter
viewLifecycleOwner.lifecycleScope.launch {
viewModel.brightnessFlow.collect { data ->
adapter.submitList(data)
}
}
binding.root.requestFocus()
binding.root.setOnKeyListener { _, _, event ->
return@setOnKeyListener if (event.action == KeyEvent.ACTION_UP) {
when (event.keyCode) {
KeyEvent.KEYCODE_DPAD_RIGHT -> {
viewModel.brightnessAdd()
true
}
KeyEvent.KEYCODE_DPAD_LEFT -> {
viewModel.brightnessSubtract()
true
}
else -> {
false
}
}
} else {
false
}
}
}
}

View File

@ -0,0 +1,53 @@
package com.yuanxuan.rokid.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.yuanxuan.rokid.R
import com.yuanxuan.rokid.databinding.FragmentHomeBinding
import kotlinx.coroutines.launch
class HomeFragment : Fragment() {
private lateinit var binding: FragmentHomeBinding
private val viewModel by viewModels<HomeViewModel>()
private val adapter by lazy {
HomeMenuAdapter { item ->
when (item.type) {
is HomeMenuAdapter.HomeMenuBean.Type.Brightness -> findNavController().navigate(R.id.home_to_brightness)
is HomeMenuAdapter.HomeMenuBean.Type.Sn -> findNavController().navigate(R.id.home_to_sn)
is HomeMenuAdapter.HomeMenuBean.Type.Volume -> findNavController().navigate(R.id.home_to_volume)
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerView.adapter = adapter
binding.recyclerView.itemAnimator = null
viewLifecycleOwner.lifecycleScope.launch {
viewModel.homeMenuBeans.collect {
adapter.submitList(it)
}
}
}
}

View File

@ -0,0 +1,128 @@
package com.yuanxuan.rokid.ui
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.yuanxuan.rokid.databinding.ItemHomeBinding
class HomeMenuAdapter(val itemClick: (HomeMenuBean) -> Unit) :
ListAdapter<HomeMenuAdapter.HomeMenuBean, HomeMenuAdapter.ViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
val binding = ItemHomeBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(
binding = binding,
itemClick = itemClick,
)
}
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
val item = getItem(position)
bindTitle(
holder = holder,
item = item,
)
holder.bindClick(item)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: List<Any?>) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads)
} else {
payloads.forEach {
val payload = it as Int
if ((DiffCallback.FLAG_TYPE_VALUE_CHANGED and payload) != 0) {
bindTitle(holder, getItem(position))
}
}
}
}
private fun bindTitle(holder: ViewHolder, item: HomeMenuBean) {
val title = holder.binding.root.context.getString(item.title)
val displayTitle = when (val type = item.type) {
is HomeMenuBean.Type.Sn -> title
is HomeMenuBean.Type.Brightness,
is HomeMenuBean.Type.Volume -> "${title}: ${type.value}"
}
holder.binding.title.text = displayTitle
}
class ViewHolder(val binding: ItemHomeBinding, val itemClick: (HomeMenuBean) -> Unit) :
RecyclerView.ViewHolder(binding.root) {
fun bindClick(item: HomeMenuBean) {
binding.parent.setOnClickListener {
itemClick(item)
}
}
}
data class HomeMenuBean(
@param:StringRes val title: Int,
@param:DrawableRes val icon: Int,
val type: Type,
) {
sealed interface Type {
val value: String
data class Brightness(override val value: String) : Type
data class Volume(override val value: String) : Type
data class Sn(override val value: String) : Type
}
}
class DiffCallback : DiffUtil.ItemCallback<HomeMenuBean>() {
/**
* 其实没有必要 因为只有一个变化 直接返回一个payload就行了 不需要计算
*/
companion object {
const val FLAG_TYPE_VALUE_CHANGED = 1
}
override fun areItemsTheSame(
oldItem: HomeMenuBean,
newItem: HomeMenuBean
): Boolean {
return oldItem.title == newItem.title
}
override fun areContentsTheSame(
oldItem: HomeMenuBean,
newItem: HomeMenuBean
): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: HomeMenuBean, newItem: HomeMenuBean): Any? {
var payload = 0
val oldType = oldItem.type
val newType = newItem.type
// 不会出现类型变化 只有value 变更
when (oldType) {
is HomeMenuBean.Type.Brightness,
is HomeMenuBean.Type.Volume -> oldType.value == newType.value
is HomeMenuBean.Type.Sn -> true
}.let {
if (it.not()) {
payload = 0 or FLAG_TYPE_VALUE_CHANGED
}
}
return if (payload == 0) null else payload
}
}
}

View File

@ -0,0 +1,40 @@
package com.yuanxuan.rokid.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.yuanxuan.rokid.R
import com.yuanxuan.rokid.dependencies.AppDependencies
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.shareIn
import kotlin.time.Duration.Companion.seconds
class HomeViewModel : ViewModel() {
val homeMenuBeans =
combine(
AppDependencies.deviceServiceManager.brightness,
AppDependencies.deviceServiceManager.volume,
) { brightness, volume ->
listOf(
HomeMenuAdapter.HomeMenuBean(
title = R.string.home_item_volume,
icon = R.drawable.icon_battery_100,
type = HomeMenuAdapter.HomeMenuBean.Type.Volume("$volume"),
),
HomeMenuAdapter.HomeMenuBean(
title = R.string.home_item_brightness,
icon = R.drawable.icon_battery_100,
type = HomeMenuAdapter.HomeMenuBean.Type.Brightness("$brightness"),
),
HomeMenuAdapter.HomeMenuBean(
title = R.string.home_item_sn,
icon = R.drawable.icon_battery_100,
type = HomeMenuAdapter.HomeMenuBean.Type.Sn("0"), // 这里用不上 随便填一个
),
)
}.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds),
replay = 1
)
}

View File

@ -0,0 +1,38 @@
package com.yuanxuan.rokid.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.yuanxuan.rokid.databinding.FragmentNoticeBinding
class NoticeFragment : Fragment() {
companion object {
const val TOAST_MESSAGE = "toast_message"
}
private lateinit var binding: FragmentNoticeBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentNoticeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toast = arguments?.getString(TOAST_MESSAGE)
binding.toast.text = toast
}
override fun onPause() {
super.onPause()
findNavController().navigateUp()
}
}

View File

@ -0,0 +1,67 @@
package com.yuanxuan.rokid.ui
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.yuanxuan.rokid.databinding.ItemSeekBarBinding
class SeekBarAdapter :
ListAdapter<SeekBarAdapter.SeekBarBean, SeekBarAdapter.ViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
val binding = ItemSeekBarBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
bindSeekBar(item = getItem(position), holder = holder)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: List<Any?>) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads)
} else {
bindSeekBar(item = getItem(position), holder = holder)
}
}
private fun bindSeekBar(item: SeekBarBean, holder: ViewHolder) {
holder.binding.root.isEnabled = item.checked
}
class ViewHolder(val binding: ItemSeekBarBinding) : RecyclerView.ViewHolder(binding.root)
data class SeekBarBean(
val value: Int,
val checked: Boolean,
)
class DiffCallback : DiffUtil.ItemCallback<SeekBarBean>() {
override fun areItemsTheSame(
oldItem: SeekBarBean,
newItem: SeekBarBean
): Boolean {
return oldItem.value == newItem.value
}
override fun areContentsTheSame(
oldItem: SeekBarBean,
newItem: SeekBarBean
): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: SeekBarBean, newItem: SeekBarBean): Any {
return true
}
}
}

View File

@ -0,0 +1,69 @@
package com.yuanxuan.rokid.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.yuanxuan.rokid.dependencies.AppDependencies
import com.yuanxuan.rokid.device.DeviceServiceManager
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlin.time.Duration.Companion.seconds
class SettingViewModel : ViewModel() {
val volumeFlow: SharedFlow<List<SeekBarAdapter.SeekBarBean>> =
AppDependencies.deviceServiceManager.volume.map { volume ->
buildList {
(1..DeviceServiceManager.MAX_VOLUME).map { i ->
add(
SeekBarAdapter.SeekBarBean(
value = i,
checked = i <= volume
)
)
}
}
}.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds),
replay = 1
)
val brightnessFlow: SharedFlow<List<SeekBarAdapter.SeekBarBean>> =
AppDependencies.deviceServiceManager.brightness.map { volume ->
buildList {
(1..DeviceServiceManager.MAX_BRIGHTNESS).map { i ->
add(
SeekBarAdapter.SeekBarBean(
value = i,
checked = i <= volume
)
)
}
}
}.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds),
replay = 1
)
fun brightnessAdd() {
AppDependencies.deviceServiceManager.brightnessAdd()
}
fun brightnessSubtract() {
AppDependencies.deviceServiceManager.brightnessSubtract()
}
fun volumeAdd() {
AppDependencies.deviceServiceManager.volumeAdd()
}
fun volumeSubtract() {
AppDependencies.deviceServiceManager.volumeSubtract()
}
fun deviceSn() = AppDependencies.deviceServiceManager.sn
}

View File

@ -0,0 +1,35 @@
package com.yuanxuan.rokid.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.yuanxuan.rokid.BuildConfig
import com.yuanxuan.rokid.R
import com.yuanxuan.rokid.databinding.FragmentSnBinding
class SnFragment : Fragment() {
private lateinit var binding: FragmentSnBinding
private val viewModel by viewModels<SettingViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentSnBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.sn.text =
resources.getString(R.string.device_info_sn, viewModel.deviceSn())
binding.appVersion.text =
resources.getString(R.string.device_info_app_version, BuildConfig.VERSION_NAME)
}
}

View File

@ -0,0 +1,69 @@
package com.yuanxuan.rokid.ui
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.yuanxuan.rokid.R
import com.yuanxuan.rokid.databinding.FragmentSettingBinding
import kotlinx.coroutines.launch
class VolumeFragment : Fragment() {
private lateinit var binding: FragmentSettingBinding
private val viewModel by viewModels<SettingViewModel>()
private val adapter by lazy {
SeekBarAdapter()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentSettingBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.title.text = resources.getString(R.string.setting_volume_title)
binding.tip.text = resources.getString(R.string.setting_volume_tip)
binding.recyclerView.adapter = adapter
viewLifecycleOwner.lifecycleScope.launch {
viewModel.volumeFlow.collect { data ->
adapter.submitList(data)
}
}
binding.root.requestFocus()
binding.root.setOnKeyListener { _, _, event ->
return@setOnKeyListener if (event.action == KeyEvent.ACTION_DOWN) {
when (event.keyCode) {
KeyEvent.KEYCODE_DPAD_RIGHT -> {
viewModel.volumeAdd()
true
}
KeyEvent.KEYCODE_DPAD_LEFT -> {
viewModel.volumeSubtract()
true
}
else -> {
false
}
}
} else {
false
}
}
}
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<set>
<objectAnimator
android:duration="200"
android:propertyName="translationY"
android:valueTo="-10dp"
android:valueType="floatType" />
</set>
</item>
<item android:state_focused="false">
<set>
<objectAnimator
android:duration="200"
android:propertyName="translationY"
android:valueTo="0dp"
android:valueType="floatType" />
</set>
</item>
</selector>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/icon_battery_0"
android:minLevel="0"
android:maxLevel="12" />
<item
android:drawable="@drawable/icon_battery_25"
android:minLevel="13"
android:maxLevel="49" />
<item
android:drawable="@drawable/icon_battery_50"
android:minLevel="50"
android:maxLevel="74" />
<item
android:drawable="@drawable/icon_battery_75"
android:minLevel="75"
android:maxLevel="90" />
<item
android:drawable="@drawable/icon_battery_100"
android:minLevel="91"
android:maxLevel="100" />
</level-list>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="10dp" />
<stroke
android:width="1dp"
android:color="@color/white" />
</shape>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false">
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<solid android:color="@color/white_50" />
</shape>
</item>
<item android:state_enabled="true">
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<solid android:color="@color/white" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,6L20,6A2,2 0,0 1,22 8L22,16A2,2 0,0 1,20 18L4,18A2,2 0,0 1,2 16L2,8A2,2 0,0 1,4 6z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
</vector>

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,6L20,6A2,2 0,0 1,22 8L22,16A2,2 0,0 1,20 18L4,18A2,2 0,0 1,2 16L2,8A2,2 0,0 1,4 6z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M5,8L19,8A1,1 0,0 1,20 9L20,15A1,1 0,0 1,19 16L5,16A1,1 0,0 1,4 15L4,9A1,1 0,0 1,5 8z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,6L20,6A2,2 0,0 1,22 8L22,16A2,2 0,0 1,20 18L4,18A2,2 0,0 1,2 16L2,8A2,2 0,0 1,4 6z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M5,8L7,8A1,1 0,0 1,8 9L8,15A1,1 0,0 1,7 16L5,16A1,1 0,0 1,4 15L4,9A1,1 0,0 1,5 8z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,6L20,6A2,2 0,0 1,22 8L22,16A2,2 0,0 1,20 18L4,18A2,2 0,0 1,2 16L2,8A2,2 0,0 1,4 6z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M5,8L11,8A1,1 0,0 1,12 9L12,15A1,1 0,0 1,11 16L5,16A1,1 0,0 1,4 15L4,9A1,1 0,0 1,5 8z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,6L20,6A2,2 0,0 1,22 8L22,16A2,2 0,0 1,20 18L4,18A2,2 0,0 1,2 16L2,8A2,2 0,0 1,4 6z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M5,8L15,8A1,1 0,0 1,16 9L16,15A1,1 0,0 1,15 16L5,16A1,1 0,0 1,4 15L4,9A1,1 0,0 1,5 8z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11.633,20.35C12.338,20.35 12.91,19.779 12.91,19.074C12.91,18.368 12.338,17.797 11.633,17.797C10.928,17.797 10.356,18.368 10.356,19.074C10.356,19.779 10.928,20.35 11.633,20.35Z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11.633,20.35C12.338,20.35 12.91,19.779 12.91,19.074C12.91,18.368 12.338,17.797 11.633,17.797C10.928,17.797 10.356,18.368 10.356,19.074C10.356,19.779 10.928,20.35 11.633,20.35Z"
android:fillColor="#000000"/>
<path
android:pathData="M11.633,13.395C13.209,13.395 14.686,13.751 15.959,14.372L16.211,14.5L16.324,14.569C16.855,14.946 16.888,15.75 16.344,16.156L16.317,16.176H16.31C16.048,16.355 15.722,16.395 15.429,16.291L15.301,16.235C14.25,15.686 12.989,15.361 11.633,15.361C10.386,15.361 9.215,15.634 8.219,16.108C7.881,16.269 7.483,16.229 7.188,15.992H7.181L7.152,15.969C6.613,15.508 6.73,14.648 7.367,14.342L7.848,14.129C8.99,13.66 10.275,13.395 11.633,13.395Z"
android:strokeWidth="0.2"
android:fillColor="#000000"
android:strokeColor="#000000"/>
</vector>

View File

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11.633,20.35C12.338,20.35 12.91,19.779 12.91,19.074C12.91,18.368 12.338,17.797 11.633,17.797C10.928,17.797 10.356,18.368 10.356,19.074C10.356,19.779 10.928,20.35 11.633,20.35Z"
android:fillColor="#000000"/>
<path
android:pathData="M11.633,13.395C13.209,13.395 14.686,13.751 15.959,14.372L16.211,14.5L16.324,14.569C16.855,14.946 16.887,15.75 16.344,16.156L16.317,16.176H16.31C16.048,16.355 15.722,16.395 15.429,16.291L15.301,16.235C14.25,15.686 12.989,15.361 11.633,15.361C10.386,15.361 9.215,15.634 8.219,16.108C7.881,16.269 7.483,16.229 7.188,15.992H7.181L7.152,15.969C6.613,15.508 6.73,14.648 7.367,14.342L7.848,14.129C8.99,13.66 10.275,13.395 11.633,13.395Z"
android:strokeWidth="0.2"
android:fillColor="#000000"
android:strokeColor="#000000"/>
<path
android:pathData="M11.633,8.646C14.609,8.646 17.357,9.487 19.544,10.894C20.117,11.263 20.14,12.095 19.595,12.501C19.271,12.743 18.833,12.759 18.492,12.541C16.641,11.349 14.29,10.628 11.733,10.61V10.612H11.633C9.252,10.612 7.047,11.221 5.25,12.253H5.249C4.891,12.458 4.443,12.413 4.13,12.144C3.619,11.708 3.69,10.892 4.274,10.557C6.363,9.354 8.9,8.646 11.633,8.646Z"
android:strokeWidth="0.2"
android:fillColor="#000000"
android:strokeColor="#000000"/>
</vector>

View File

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11.633,20.35C12.338,20.35 12.91,19.779 12.91,19.074C12.91,18.368 12.338,17.797 11.633,17.797C10.928,17.797 10.356,18.368 10.356,19.074C10.356,19.779 10.928,20.35 11.633,20.35Z"
android:fillColor="#000000"/>
<path
android:pathData="M11.633,13.395C13.209,13.395 14.686,13.751 15.959,14.372L16.211,14.5L16.324,14.569C16.855,14.946 16.887,15.75 16.344,16.156L16.317,16.176H16.31C16.048,16.355 15.722,16.395 15.429,16.291L15.301,16.235C14.25,15.686 12.989,15.361 11.633,15.361C10.386,15.361 9.215,15.634 8.219,16.108C7.881,16.269 7.483,16.229 7.188,15.992H7.181L7.152,15.969C6.613,15.508 6.73,14.648 7.367,14.342L7.848,14.129C8.99,13.66 10.275,13.395 11.633,13.395Z"
android:strokeWidth="0.2"
android:fillColor="#000000"
android:strokeColor="#000000"/>
<path
android:pathData="M11.633,8.646C14.609,8.646 17.357,9.487 19.544,10.894C20.117,11.263 20.14,12.095 19.595,12.501C19.271,12.743 18.833,12.759 18.492,12.541C16.641,11.349 14.29,10.628 11.733,10.61V10.612H11.633C9.252,10.612 7.047,11.221 5.25,12.253H5.249C4.891,12.458 4.443,12.413 4.13,12.144C3.619,11.708 3.69,10.892 4.274,10.557C6.363,9.354 8.9,8.646 11.633,8.646Z"
android:strokeWidth="0.2"
android:fillColor="#000000"
android:strokeColor="#000000"/>
<path
android:pathData="M11.633,3.9C15.844,3.9 19.704,5.162 22.683,7.253L22.778,7.329C23.228,7.734 23.208,8.464 22.707,8.841L22.68,8.861H22.672C22.341,9.089 21.901,9.094 21.569,8.861C18.911,7 15.435,5.866 11.634,5.866C8.368,5.867 5.344,6.702 2.871,8.117L2.384,8.408C2.069,8.603 1.674,8.596 1.366,8.406L1.24,8.313C0.735,7.88 0.805,7.086 1.369,6.735C4.23,4.956 7.784,3.9 11.633,3.9Z"
android:strokeWidth="0.2"
android:fillColor="#000000"
android:strokeColor="#000000"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4.333,4.261C4.755,3.894 5.416,3.917 5.808,4.312L18.721,17.356C19.113,17.751 19.089,18.371 18.667,18.739C18.245,19.106 17.584,19.083 17.192,18.688L4.279,5.644C3.887,5.249 3.911,4.629 4.333,4.261Z"
android:fillColor="#000000"/>
<path
android:pathData="M11.633,17.797C12.338,17.797 12.91,18.368 12.91,19.073C12.91,19.778 12.338,20.35 11.633,20.351C10.928,20.351 10.356,19.778 10.356,19.073C10.356,18.368 10.928,17.797 11.633,17.797ZM11.855,13.297C13.131,13.324 14.338,13.583 15.422,14.023L16.833,15.448C16.812,15.744 16.674,16.035 16.404,16.236L16.351,16.276H16.338C16.012,16.486 15.6,16.503 15.255,16.324H15.254C15.023,16.203 14.779,16.095 14.528,15.997L11.855,13.297ZM12.654,15.525C12.321,15.484 11.981,15.462 11.633,15.462C10.401,15.462 9.245,15.731 8.262,16.198C7.903,16.369 7.479,16.333 7.156,16.093H7.144L7.088,16.045C6.494,15.537 6.623,14.59 7.325,14.252L7.81,14.036C8.643,13.694 9.551,13.462 10.507,13.357L12.654,15.525ZM8.176,16.018C8.098,16.055 8.015,16.079 7.932,16.093C7.974,16.086 8.016,16.078 8.056,16.065C8.097,16.053 8.137,16.037 8.176,16.019V16.018ZM16.375,15.998L16.285,16.076C16.317,16.052 16.348,16.026 16.375,15.999C16.403,15.972 16.427,15.943 16.45,15.914L16.375,15.998ZM7.029,15.667C7.058,15.718 7.093,15.766 7.134,15.811V15.811C7.114,15.788 7.094,15.764 7.077,15.74C7.059,15.716 7.044,15.691 7.029,15.666V15.667ZM8.378,14.034C8.34,14.048 8.302,14.063 8.263,14.077C8.324,14.055 8.385,14.033 8.446,14.013L8.378,14.034ZM11.633,8.546C14.628,8.546 17.394,9.392 19.598,10.81C20.231,11.217 20.256,12.134 19.656,12.581C19.299,12.848 18.816,12.867 18.439,12.626V12.625C17.092,11.757 15.476,11.143 13.708,10.87L11.409,8.549C11.483,8.548 11.558,8.546 11.633,8.546ZM8.301,11.129C7.216,11.408 6.206,11.82 5.3,12.34H5.299C4.904,12.566 4.411,12.516 4.066,12.22C3.503,11.739 3.58,10.84 4.225,10.471L4.625,10.25C5.244,9.921 5.899,9.634 6.584,9.394L8.301,11.129ZM5.2,12.166C5.12,12.212 5.035,12.242 4.949,12.261C4.992,12.252 5.035,12.24 5.077,12.225C5.119,12.209 5.161,12.189 5.201,12.166H5.2ZM12.154,10.723C12.041,10.718 11.927,10.714 11.813,10.713H11.633C10.892,10.713 10.167,10.771 9.466,10.885L7.662,9.062C8.438,8.854 9.245,8.707 10.077,8.624L12.154,10.723ZM11.633,3.8C15.864,3.8 19.745,5.067 22.741,7.171C23.342,7.592 23.355,8.478 22.767,8.921L22.714,8.961H22.701C22.34,9.193 21.871,9.193 21.513,8.943C18.873,7.094 15.416,5.967 11.634,5.967C10.747,5.967 9.878,6.031 9.034,6.149L7.187,4.283C8.606,3.969 10.096,3.8 11.633,3.8ZM3.127,5.666C3.221,5.912 3.367,6.145 3.568,6.348L4.587,7.378C4.009,7.625 3.452,7.901 2.921,8.204L2.437,8.493C2.04,8.739 1.528,8.692 1.175,8.389C0.618,7.91 0.696,7.035 1.317,6.65C1.893,6.292 2.498,5.963 3.127,5.666ZM7.834,6.359C7.064,6.521 6.318,6.73 5.602,6.982L4.278,5.645C4.166,5.532 4.09,5.401 4.045,5.263C4.695,4.997 5.367,4.763 6.059,4.566L7.834,6.359ZM19.045,5.403C19.014,5.391 18.983,5.378 18.952,5.365C18.907,5.348 18.861,5.332 18.816,5.314C18.892,5.344 18.969,5.373 19.045,5.403Z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/icon_wifi_unconnect"
android:maxLevel="0" />
<item
android:drawable="@drawable/icon_wifi_1"
android:maxLevel="1" />
<item
android:drawable="@drawable/icon_wifi_2"
android:maxLevel="2" />
<item
android:drawable="@drawable/icon_wifi_3"
android:maxLevel="3" />
<item
android:drawable="@drawable/icon_wifi_4"
android:maxLevel="4" />
</level-list>

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/lottie_voice_input"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/app_navigation" />
<TextView
android:id="@+id/battery_level"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="@dimen/status_bar_text_size"
app:layout_constraintBottom_toTopOf="@id/bottom"
app:layout_constraintEnd_toEndOf="parent"
tools:text="50%" />
<ImageView
android:id="@+id/wifi_iv"
android:layout_width="@dimen/status_bar_icon_size"
android:layout_height="@dimen/status_bar_icon_size"
android:layout_marginEnd="5dp"
android:src="@drawable/wifi_level_list"
android:tint="@color/white"
app:layout_constraintBottom_toBottomOf="@id/battery_level"
app:layout_constraintEnd_toStartOf="@id/battery_level_iv"
app:layout_constraintTop_toTopOf="@id/battery_level" />
<ImageView
android:id="@+id/battery_level_iv"
android:layout_width="@dimen/status_bar_icon_size"
android:layout_height="@dimen/status_bar_icon_size"
android:layout_marginEnd="5dp"
android:src="@drawable/battery_level_list"
android:tint="@color/white"
app:layout_constraintBottom_toBottomOf="@id/battery_level"
app:layout_constraintEnd_toStartOf="@id/battery_level"
app:layout_constraintTop_toTopOf="@id/battery_level" />
<TextClock
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="@dimen/status_bar_text_size"
app:layout_constraintBottom_toTopOf="@id/bottom"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/socket_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:textColor="@color/white"
android:textSize="@dimen/status_bar_text_size"
app:layout_constraintBottom_toTopOf="@id/bottom"
app:layout_constraintStart_toEndOf="@id/time"
tools:text="已连接" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie_voice_input"
android:layout_width="150dp"
android:layout_height="50dp"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:lottie_autoPlay="false"
app:lottie_loop="true"
app:lottie_rawRes="@raw/voice_white"
tools:visibility="visible" />
<Space
android:id="@+id/bottom"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.leanback.widget.HorizontalGridView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="85dp"
android:clipToPadding="false"
android:paddingStart="10dp"
android:paddingTop="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="RtlSymmetry"
tools:listitem="@layout/item_home" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/toast"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="30.dp"
android:paddingEnd="30.dp"
android:textColor="@color/white"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:descendantFocusability="blocksDescendants">
<TextView
android:id="@+id/tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="50dp"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="@string/setting_volume_tip" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="5dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="RtlSymmetry"
tools:itemCount="15"
tools:listitem="@layout/item_seek_bar" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="70dp"
android:textColor="@color/white"
app:layout_constraintBottom_toTopOf="@id/recycler_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="@string/setting_volume_title" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/sn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="askjdhkjashdkjhsad" />
<TextView
android:id="@+id/app_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textColor="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sn"
tools:text="APP版本号" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/parent"
android:layout_width="50dp"
android:layout_height="75dp"
android:layout_marginEnd="10dp"
android:soundEffectsEnabled="false"
android:background="@drawable/bg_home_menu"
android:stateListAnimator="@animator/focus_animator">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="10sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/home_menu_setting" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="15dp"
android:layout_height="10dp"
android:layout_marginEnd="5dp"
android:background="@drawable/bg_seek_bar" />

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/app_navigation"
app:startDestination="@id/home_fragment">
<fragment
android:id="@+id/home_fragment"
android:name="com.yuanxuan.rokid.ui.HomeFragment"
android:label="home"
tools:layout="@layout/fragment_home">
<action
android:id="@+id/home_to_sn"
app:destination="@id/sn_fragment" />
<action
android:id="@+id/home_to_volume"
app:destination="@id/volume_fragment" />
<action
android:id="@+id/home_to_brightness"
app:destination="@id/brightness_fragment" />
</fragment>
<fragment
android:id="@+id/volume_fragment"
android:name="com.yuanxuan.rokid.ui.VolumeFragment"
android:label="volume"
tools:layout="@layout/fragment_setting" />
<fragment
android:id="@+id/brightness_fragment"
android:name="com.yuanxuan.rokid.ui.BrightnessFragment"
android:label="brightness"
tools:layout="@layout/fragment_setting" />
<fragment
android:id="@+id/sn_fragment"
android:name="com.yuanxuan.rokid.ui.SnFragment"
android:label="sn"
tools:layout="@layout/fragment_sn" />
<fragment
android:id="@+id/notice_fragment"
android:name="com.yuanxuan.rokid.ui.NoticeFragment"
android:label="notice"
tools:layout="@layout/fragment_notice" />
</navigation>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Rokid" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
<item name="android:windowBackground">@android:color/transparent</item>
</style>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="white_50">#50FFFFFF</color>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="status_bar_text_size">12sp</dimen>
<dimen name="status_bar_icon_size">16dp</dimen>
</resources>

View File

@ -0,0 +1,16 @@
<resources>
<string name="app_name">Rokid</string>
<string name="home_menu_setting">设置</string>
<string name="setting_volume_tip">前后划动“触控板”调节音量</string>
<string name="setting_brightness_tip">前后划动“触控板”调节亮度</string>
<string name="setting_volume_title">音量调节</string>
<string name="setting_brightness_title">亮度调节</string>
<string name="home_item_volume">音量</string>
<string name="home_item_brightness">亮度</string>
<string name="home_item_sn">SN</string>
<string name="status_bar_battery">%d%%</string>
<string name="device_info_sn">设备SN:%s</string>
<string name="device_info_app_version">APP版本:%s</string>
<string name="socket_status_connected">已连接</string>
<string name="socket_status_connecting">连接中</string>
</resources>

View File

@ -0,0 +1,10 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Rokid" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="Theme.Rokid" parent="Base.Theme.Rokid" />
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-cache-path
name="external_cache_files"
path="." />
</paths>

View File

@ -0,0 +1,17 @@
package com.yuanxuan.rokid
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)
}
}

5
build.gradle.kts Normal file
View File

@ -0,0 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
}

23
gradle.properties Normal file
View File

@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

45
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,45 @@
[versions]
agp = "8.13.0"
kotlin = "2.2.21"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
material = "1.10.0"
activity = "1.8.0"
constraintlayout = "2.1.4"
timber = "5.0.1"
okhttp = "5.3.0"
worker = "2.11.0"
retrofit = "3.0.0"
gson = "2.13.2"
lottie = "6.6.6"
navigation = "2.9.6"
leanback = "1.2.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
logger-timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "worker" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
lottie = { group = "com.airbnb.android", name = "lottie", version.ref = "lottie" }
androidx-navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigation" }
androidx-navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigation" }
androidx-leanback = { group = "androidx.leanback", name = "leanback", version.ref = "leanback" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,8 @@
#Fri Nov 07 15:46:29 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
gradlew vendored Normal file
View File

@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
gradlew.bat vendored Normal file
View File

@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

4
keystore.properties Normal file
View File

@ -0,0 +1,4 @@
storeFile=keystore/release_keystore.jks
storePassword=S<+*T;KcbR?yMm3b
keyAlias=key0
keyPassword=]+*7;Vn+zpA=r{KX

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More