Compare commits
20 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
e1ddc14553 | |
|
|
9c29fee24e | |
|
|
84961a367c | |
|
|
0c122b9352 | |
|
|
4712855f6c | |
|
|
ae8209e025 | |
|
|
0b0c863765 | |
|
|
c9a2b78a74 | |
|
|
699bc11c25 | |
|
|
e5d17bc970 | |
|
|
6b234f86e7 | |
|
|
189ffa71bf | |
|
|
4193326b92 | |
|
|
ca99eaa3e4 | |
|
|
1b0e5ec8ab | |
|
|
f8b5f3a395 | |
|
|
4327a8638c | |
|
|
570de3c4e7 | |
|
|
22ff4384ca | |
|
|
41726d5006 |
|
|
@ -23,13 +23,10 @@ misc.xml
|
|||
deploymentTargetDropDown.xml
|
||||
render.experimental.xml
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
app/release
|
||||
|
||||
|
|
|
|||
252
README.md
|
|
@ -1,2 +1,254 @@
|
|||
# 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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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, "启动安装程序失败")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.yuanxuan.rokid.network.http
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
class NetworkException(message: String, cause: Throwable? = null) : IOException(message, cause)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
// )
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.yuanxuan.rokid.network.websocket
|
||||
|
||||
sealed interface WebSocketEvent {
|
||||
data class Notice(val msg: String) : WebSocketEvent
|
||||
|
||||
data object Unknow : WebSocketEvent
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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" />
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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" }
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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" "$@"
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
storeFile=keystore/release_keystore.jks
|
||||
storePassword=S<+*T;KcbR?yMm3b
|
||||
keyAlias=key0
|
||||
keyPassword=]+*7;Vn+zpA=r{KX
|
||||