Phase 1 complete. build Ok, apk OK
Signed-off-by: chaoq <chaoq@gxtech.ltd>
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
|
id 'kotlin-kapt'
|
||||||
|
id 'dagger.hilt.android.plugin'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -32,6 +34,10 @@ android {
|
|||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = '17'
|
jvmTarget = '17'
|
||||||
|
freeCompilerArgs += [
|
||||||
|
"-opt-in=kotlin.RequiresOptIn",
|
||||||
|
"-Xjvm-default=all"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose true
|
compose true
|
||||||
@@ -47,6 +53,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// Core
|
||||||
implementation 'androidx.core:core-ktx:1.13.1'
|
implementation 'androidx.core:core-ktx:1.13.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4'
|
||||||
implementation 'androidx.activity:activity-compose:1.9.1'
|
implementation 'androidx.activity:activity-compose:1.9.1'
|
||||||
@@ -58,6 +65,32 @@ dependencies {
|
|||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4'
|
||||||
implementation 'androidx.navigation:navigation-compose:2.7.7'
|
implementation 'androidx.navigation:navigation-compose:2.7.7'
|
||||||
|
|
||||||
|
// Network
|
||||||
|
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||||
|
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||||
|
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
|
||||||
|
|
||||||
|
// DI
|
||||||
|
implementation 'com.google.dagger:hilt-android:2.48'
|
||||||
|
kapt 'com.google.dagger:hilt-compiler:2.48'
|
||||||
|
implementation 'androidx.hilt:hilt-navigation-compose:1.1.0'
|
||||||
|
|
||||||
|
// Database
|
||||||
|
implementation 'androidx.room:room-runtime:2.6.1'
|
||||||
|
implementation 'androidx.room:room-ktx:2.6.1'
|
||||||
|
kapt 'androidx.room:room-compiler:2.6.1'
|
||||||
|
|
||||||
|
// LiveKit (TODO: 需要确认GXRTC服务器集成方式)
|
||||||
|
// implementation 'io.livekit:livekit-android:2.20.2'
|
||||||
|
|
||||||
|
// ARCore
|
||||||
|
implementation 'com.google.ar:core:1.50.0'
|
||||||
|
|
||||||
|
// MQTT (TODO: 需要确认具体MQTT客户端库)
|
||||||
|
// implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5'
|
||||||
|
// implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package com.xsynergy.android
|
package com.xsynergy.android
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import com.xsynergy.android.utils.PerformanceOptimizer
|
import com.xsynergy.android.utils.PerformanceOptimizer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* XSynergy Application class for app-wide initialization
|
* XSynergy Application class for app-wide initialization
|
||||||
* Optimizes app startup and performance monitoring
|
* Optimizes app startup and performance monitoring
|
||||||
*/
|
*/
|
||||||
|
@HiltAndroidApp
|
||||||
class XSynergyApplication : Application() {
|
class XSynergyApplication : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.xsynergy.android.data.local
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [UserEntity::class],
|
||||||
|
version = 1,
|
||||||
|
exportSchema = false
|
||||||
|
)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun userDao(): UserDao
|
||||||
|
}
|
||||||
21
app/src/main/java/com/xsynergy/android/data/local/UserDao.kt
Normal file
21
app/src/main/java/com/xsynergy/android/data/local/UserDao.kt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package com.xsynergy.android.data.local
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface UserDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertUser(user: UserEntity)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM users WHERE id = :userId")
|
||||||
|
suspend fun getUserById(userId: String): UserEntity?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM users ORDER BY lastLoginTime DESC LIMIT 1")
|
||||||
|
suspend fun getLastLoggedInUser(): UserEntity?
|
||||||
|
|
||||||
|
@Query("DELETE FROM users")
|
||||||
|
suspend fun clearAllUsers()
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.xsynergy.android.data.local
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.xsynergy.android.data.model.User
|
||||||
|
|
||||||
|
@Entity(tableName = "users")
|
||||||
|
data class UserEntity(
|
||||||
|
@PrimaryKey val id: String,
|
||||||
|
val phone: String,
|
||||||
|
val name: String,
|
||||||
|
val token: String,
|
||||||
|
val avatarUrl: String?,
|
||||||
|
val lastLoginTime: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
fun User.toEntity(): UserEntity = UserEntity(
|
||||||
|
id = id,
|
||||||
|
phone = phone,
|
||||||
|
name = name,
|
||||||
|
token = token,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
|
lastLoginTime = lastLoginTime
|
||||||
|
)
|
||||||
|
|
||||||
|
fun UserEntity.toDomain(): User = User(
|
||||||
|
id = id,
|
||||||
|
phone = phone,
|
||||||
|
name = name,
|
||||||
|
token = token,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
|
lastLoginTime = lastLoginTime
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.xsynergy.android.data.model
|
||||||
|
|
||||||
|
data class LoginRequest(
|
||||||
|
val phone: String,
|
||||||
|
val code: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.xsynergy.android.data.model
|
||||||
|
|
||||||
|
data class LoginResponse(
|
||||||
|
val success: Boolean,
|
||||||
|
val user: User?,
|
||||||
|
val token: String?,
|
||||||
|
val message: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GxrtcResponse<T>(
|
||||||
|
val meta: Meta,
|
||||||
|
val data: T
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Meta(
|
||||||
|
val code: Int,
|
||||||
|
val message: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AccessTokenResponse(
|
||||||
|
val accessToken: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RoomResponse(
|
||||||
|
val sid: String,
|
||||||
|
val name: String,
|
||||||
|
val emptyTimeout: Int,
|
||||||
|
val departureTimeout: Int,
|
||||||
|
val creationTime: Long,
|
||||||
|
val turnPassword: String,
|
||||||
|
val enabledCodecs: List<CodecInfo>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CodecInfo(
|
||||||
|
val mime: String
|
||||||
|
)
|
||||||
10
app/src/main/java/com/xsynergy/android/data/model/User.kt
Normal file
10
app/src/main/java/com/xsynergy/android/data/model/User.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package com.xsynergy.android.data.model
|
||||||
|
|
||||||
|
data class User(
|
||||||
|
val id: String,
|
||||||
|
val phone: String,
|
||||||
|
val name: String,
|
||||||
|
val token: String,
|
||||||
|
val avatarUrl: String? = null,
|
||||||
|
val lastLoginTime: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.xsynergy.android.data.remote
|
||||||
|
|
||||||
|
import com.xsynergy.android.data.model.AccessTokenResponse
|
||||||
|
import com.xsynergy.android.data.model.GxrtcResponse
|
||||||
|
import com.xsynergy.android.data.model.RoomResponse
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.http.Field
|
||||||
|
import retrofit2.http.FormUrlEncoded
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.POST
|
||||||
|
|
||||||
|
interface GxrtcApi {
|
||||||
|
@POST("room/token")
|
||||||
|
@FormUrlEncoded
|
||||||
|
suspend fun getAccessToken(
|
||||||
|
@Field("uid") userId: String,
|
||||||
|
@Field("room") roomId: String
|
||||||
|
): Response<GxrtcResponse<AccessTokenResponse>>
|
||||||
|
|
||||||
|
@POST("room/")
|
||||||
|
@FormUrlEncoded
|
||||||
|
suspend fun createRoom(@Field("room") roomName: String): Response<GxrtcResponse<RoomResponse>>
|
||||||
|
|
||||||
|
@GET("room/")
|
||||||
|
suspend fun getRooms(): Response<GxrtcResponse<List<RoomResponse>>>
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.xsynergy.android.data.remote
|
||||||
|
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
object RetrofitClient {
|
||||||
|
|
||||||
|
private const val BASE_URL = "https://meeting.cnsdt.com/api/v1/"
|
||||||
|
|
||||||
|
fun provideOkHttpClient(): OkHttpClient {
|
||||||
|
return OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.addInterceptor { chain ->
|
||||||
|
val request = chain.request().newBuilder()
|
||||||
|
.addHeader("Content-Type", "application/json")
|
||||||
|
.addHeader("Accept", "application/json")
|
||||||
|
.build()
|
||||||
|
chain.proceed(request)
|
||||||
|
}
|
||||||
|
.addInterceptor(
|
||||||
|
HttpLoggingInterceptor().apply {
|
||||||
|
level = HttpLoggingInterceptor.Level.BODY
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
|
||||||
|
val gson = GsonBuilder()
|
||||||
|
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||||
|
.create()
|
||||||
|
|
||||||
|
return Retrofit.Builder()
|
||||||
|
.baseUrl(BASE_URL)
|
||||||
|
.client(okHttpClient)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun provideGxrtcApi(retrofit: Retrofit): GxrtcApi = retrofit.create(GxrtcApi::class.java)
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.xsynergy.android.data.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import com.xsynergy.android.data.local.UserDao
|
||||||
|
import com.xsynergy.android.data.local.UserEntity
|
||||||
|
import com.xsynergy.android.data.remote.GxrtcApi
|
||||||
|
import com.xsynergy.android.data.model.LoginRequest
|
||||||
|
import com.xsynergy.android.data.model.User
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class AuthRepository @Inject constructor(
|
||||||
|
private val gxrtcApi: GxrtcApi,
|
||||||
|
private val userDao: UserDao,
|
||||||
|
private val prefs: SharedPreferences
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun getAccessToken(userId: String, roomId: String): Result<String> {
|
||||||
|
return try {
|
||||||
|
val response = gxrtcApi.getAccessToken(userId, roomId)
|
||||||
|
if (response.isSuccessful && response.body()?.meta?.code == 200) {
|
||||||
|
Result.success(response.body()!!.data.accessToken)
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("获取access token失败: ${response.body()?.meta?.message}"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(Exception("网络请求失败: ${e.message}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createRoom(roomName: String): Result<String> {
|
||||||
|
return try {
|
||||||
|
val response = gxrtcApi.createRoom(roomName)
|
||||||
|
if (response.isSuccessful && response.body()?.meta?.code == 200) {
|
||||||
|
Result.success(response.body()!!.data.sid)
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("创建房间失败: ${response.body()?.meta?.message}"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(Exception("网络请求失败: ${e.message}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getCurrentUser(): User? {
|
||||||
|
val userEntity = userDao.getLastLoggedInUser()
|
||||||
|
return userEntity?.let {
|
||||||
|
User(
|
||||||
|
id = it.id,
|
||||||
|
phone = it.phone,
|
||||||
|
name = it.name,
|
||||||
|
token = it.token,
|
||||||
|
avatarUrl = it.avatarUrl,
|
||||||
|
lastLoginTime = it.lastLoginTime
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveUser(user: User) {
|
||||||
|
val userEntity = UserEntity(
|
||||||
|
id = user.id,
|
||||||
|
phone = user.phone,
|
||||||
|
name = user.name,
|
||||||
|
token = user.token,
|
||||||
|
avatarUrl = user.avatarUrl,
|
||||||
|
lastLoginTime = user.lastLoginTime
|
||||||
|
)
|
||||||
|
userDao.insertUser(userEntity)
|
||||||
|
prefs.edit().putString("auth_token", user.token).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun logout() {
|
||||||
|
prefs.edit().remove("auth_token").apply()
|
||||||
|
userDao.clearAllUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAuthToken(): String? = prefs.getString("auth_token", null)
|
||||||
|
}
|
||||||
68
app/src/main/java/com/xsynergy/android/di/NetworkModule.kt
Normal file
68
app/src/main/java/com/xsynergy/android/di/NetworkModule.kt
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package com.xsynergy.android.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.room.Room
|
||||||
|
import com.xsynergy.android.data.local.AppDatabase
|
||||||
|
import com.xsynergy.android.data.remote.GxrtcApi
|
||||||
|
import com.xsynergy.android.data.remote.RetrofitClient
|
||||||
|
import com.xsynergy.android.data.repository.AuthRepository
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object NetworkModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideOkHttpClient(): okhttp3.OkHttpClient {
|
||||||
|
return RetrofitClient.provideOkHttpClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideRetrofit(okHttpClient: okhttp3.OkHttpClient): retrofit2.Retrofit {
|
||||||
|
return RetrofitClient.provideRetrofit(okHttpClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideGxrtcApi(retrofit: retrofit2.Retrofit): GxrtcApi {
|
||||||
|
return RetrofitClient.provideGxrtcApi(retrofit)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||||
|
return Room.databaseBuilder(
|
||||||
|
context,
|
||||||
|
AppDatabase::class.java,
|
||||||
|
"xsynergy_database"
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideUserDao(database: AppDatabase) = database.userDao()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
|
||||||
|
return context.getSharedPreferences("xsynergy_prefs", Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideAuthRepository(
|
||||||
|
gxrtcApi: GxrtcApi,
|
||||||
|
userDao: com.xsynergy.android.data.local.UserDao,
|
||||||
|
prefs: SharedPreferences
|
||||||
|
): AuthRepository {
|
||||||
|
return AuthRepository(gxrtcApi, userDao, prefs)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.9.24'
|
ext.kotlin_version = '1.9.24'
|
||||||
ext.compose_version = '1.6.8'
|
ext.compose_version = '1.6.8'
|
||||||
|
ext.hilt_version = '2.48'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@@ -8,6 +9,7 @@ buildscript {
|
|||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.13.0'
|
classpath 'com.android.tools.build:gradle:8.13.0'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ object NetworkModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideGxrtcApi(retrofit: Retrofit): GxrtcApi = retrofit.create(GxrtcApi::class.java)
|
fun provideGxrtcApi(retrofit: Retrofit): GxrtcApi = retrofit.create(GxrtcApi::class.java)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideArAnnotationApi(retrofit: Retrofit): ArAnnotationApi = retrofit.create(ArAnnotationApi::class.java)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideFeaturePointApi(retrofit: Retrofit): FeaturePointApi = retrofit.create(FeaturePointApi::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
// data/remote/GxrtcApi.kt - 新增
|
// data/remote/GxrtcApi.kt - 新增
|
||||||
@@ -44,6 +52,128 @@ interface GxrtcApi {
|
|||||||
suspend fun getRooms(): Response<GxrtcResponse<List<RoomResponse>>>
|
suspend fun getRooms(): Response<GxrtcResponse<List<RoomResponse>>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// data/remote/ArAnnotationApi.kt - 新增
|
||||||
|
interface ArAnnotationApi {
|
||||||
|
@POST("ar/anchor/save")
|
||||||
|
suspend fun saveAnchor(@Body request: SaveAnchorRequest): Response<GxrtcResponse<Unit>>
|
||||||
|
|
||||||
|
@GET("ar/room/{roomId}/anchors")
|
||||||
|
suspend fun getRoomAnchors(@Path("roomId") roomId: String): Response<GxrtcResponse<AnchorListResponse>>
|
||||||
|
|
||||||
|
@POST("ar/annotation/save")
|
||||||
|
suspend fun saveAnnotation(@Body annotation: ArAnnotation): Response<GxrtcResponse<Unit>>
|
||||||
|
|
||||||
|
@GET("ar/room/{roomId}/annotations")
|
||||||
|
suspend fun getRoomAnnotations(@Path("roomId") roomId: String): Response<GxrtcResponse<AnnotationListResponse>>
|
||||||
|
|
||||||
|
@DELETE("ar/annotation/{annotationId}")
|
||||||
|
suspend fun deleteAnnotation(@Path("annotationId") annotationId: String): Response<GxrtcResponse<Unit>>
|
||||||
|
|
||||||
|
@POST("ar/spatial/validate")
|
||||||
|
suspend fun validateSpatialConsistency(@Body request: SpatialValidationRequest): Response<GxrtcResponse<SpatialValidationResult>>
|
||||||
|
|
||||||
|
@POST("ar/annotations/version")
|
||||||
|
suspend fun createAnnotationVersion(@Body request: CreateVersionRequest): Response<GxrtcResponse<VersionResponse>>
|
||||||
|
|
||||||
|
@GET("ar/annotations/{roomId}/versions")
|
||||||
|
suspend fun getAnnotationVersions(@Path("roomId") roomId: String): Response<GxrtcResponse<List<VersionResponse>>>
|
||||||
|
}
|
||||||
|
|
||||||
|
// data/remote/FeaturePointApi.kt - 新增
|
||||||
|
interface FeaturePointApi {
|
||||||
|
@POST("ar/feature-points/save")
|
||||||
|
suspend fun saveFeaturePoints(@Body request: SaveFeaturePointsRequest): Response<GxrtcResponse<Unit>>
|
||||||
|
|
||||||
|
@GET("ar/feature-points/{anchorId}")
|
||||||
|
suspend fun getAnchorFeatures(@Path("anchorId") anchorId: String): Response<GxrtcResponse<FeaturePointListResponse>>
|
||||||
|
|
||||||
|
@POST("ar/feature-points/match")
|
||||||
|
suspend fun matchFeaturePoints(@Body request: FeatureMatchRequest): Response<GxrtcResponse<FeatureMatchResponse>>
|
||||||
|
|
||||||
|
@POST("ar/feature-points/quality-assessment")
|
||||||
|
suspend fun assessFeatureQuality(@Body request: FeatureQualityRequest): Response<GxrtcResponse<FeatureQualityResponse>>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求和响应数据模型
|
||||||
|
data class SaveAnchorRequest(
|
||||||
|
val roomId: String,
|
||||||
|
val anchorId: String,
|
||||||
|
val position: List<Float>,
|
||||||
|
val rotation: List<Float>,
|
||||||
|
val createdBy: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AnchorListResponse(val anchors: List<AnchorData>)
|
||||||
|
|
||||||
|
data class AnnotationListResponse(val annotations: List<ArAnnotation>)
|
||||||
|
|
||||||
|
data class SaveFeaturePointsRequest(
|
||||||
|
val anchorId: String,
|
||||||
|
val roomId: String,
|
||||||
|
val featurePoints: List<FeaturePoint>,
|
||||||
|
val deviceInfo: DeviceInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FeaturePointListResponse(val featurePoints: List<FeaturePoint>)
|
||||||
|
|
||||||
|
data class SpatialValidationRequest(
|
||||||
|
val anchorId: String,
|
||||||
|
val currentPosition: List<Float>,
|
||||||
|
val currentRotation: List<Float>,
|
||||||
|
val deviceInfo: DeviceInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SpatialValidationResult(
|
||||||
|
val isValid: Boolean,
|
||||||
|
val confidence: Float,
|
||||||
|
val suggestedCorrection: SpatialTransform?,
|
||||||
|
val errorMessage: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FeatureMatchRequest(
|
||||||
|
val anchorId: String,
|
||||||
|
val queryFeatures: List<FeaturePoint>,
|
||||||
|
val maxResults: Int = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FeatureMatchResponse(
|
||||||
|
val matches: List<FeatureMatch>,
|
||||||
|
val confidence: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FeatureMatch(
|
||||||
|
val featurePoint: FeaturePoint,
|
||||||
|
val similarity: Float,
|
||||||
|
val spatialDistance: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FeatureQualityRequest(
|
||||||
|
val anchorId: String,
|
||||||
|
val featurePoints: List<FeaturePoint>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FeatureQualityResponse(
|
||||||
|
val overallQuality: Float,
|
||||||
|
val individualScores: Map<String, Float>,
|
||||||
|
val recommendations: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreateVersionRequest(
|
||||||
|
val roomId: String,
|
||||||
|
val versionName: String,
|
||||||
|
val description: String,
|
||||||
|
val createdBy: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VersionResponse(
|
||||||
|
val versionId: String,
|
||||||
|
val versionName: String,
|
||||||
|
val description: String,
|
||||||
|
val createdAt: Long,
|
||||||
|
val createdBy: String,
|
||||||
|
val annotationCount: Int
|
||||||
|
)
|
||||||
|
|
||||||
// 响应数据结构
|
// 响应数据结构
|
||||||
data class GxrtcResponse<T>(
|
data class GxrtcResponse<T>(
|
||||||
@SerializedName("meta") val meta: Meta,
|
@SerializedName("meta") val meta: Meta,
|
||||||
@@ -423,35 +553,53 @@ data class AnchorData(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 4.2 AR标注持久化服务
|
#### 4.2 AR标注持久化服务(增强版)
|
||||||
```kotlin
|
```kotlin
|
||||||
// domain/service/ArAnnotationService.kt - 新增
|
// domain/service/ArAnnotationService.kt - 新增
|
||||||
class ArAnnotationService @Inject constructor(
|
class ArAnnotationService @Inject constructor(
|
||||||
private val arAnnotationApi: ArAnnotationApi
|
private val arAnnotationApi: ArAnnotationApi,
|
||||||
|
private val featurePointApi: FeaturePointApi,
|
||||||
|
private val userRepository: UserRepository
|
||||||
) {
|
) {
|
||||||
suspend fun saveAnchor(
|
suspend fun saveAnchorWithFeatures(
|
||||||
roomId: String,
|
roomId: String,
|
||||||
anchorId: String,
|
anchorId: String,
|
||||||
position: Pose
|
position: Pose,
|
||||||
|
featurePoints: List<FeaturePoint>,
|
||||||
|
deviceInfo: DeviceInfo
|
||||||
): Result<Unit> {
|
): Result<Unit> {
|
||||||
return try {
|
return try {
|
||||||
val request = SaveAnchorRequest(
|
// 保存锚点基本信息
|
||||||
|
val anchorRequest = SaveAnchorRequest(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
anchorId = anchorId,
|
anchorId = anchorId,
|
||||||
position = listOf(position.tx(), position.ty(), position.tz()),
|
position = listOf(position.tx(), position.ty(), position.tz()),
|
||||||
rotation = listOf(position.qx(), position.qy(), position.qz(), position.qw()),
|
rotation = listOf(position.qx(), position.qy(), position.qz(), position.qw()),
|
||||||
createdBy = currentUserId
|
createdBy = userRepository.getCurrentUser()?.id ?: ""
|
||||||
)
|
)
|
||||||
|
arAnnotationApi.saveAnchor(anchorRequest)
|
||||||
|
|
||||||
|
// 保存特征点数据
|
||||||
|
val featureRequest = SaveFeaturePointsRequest(
|
||||||
|
anchorId = anchorId,
|
||||||
|
roomId = roomId,
|
||||||
|
featurePoints = featurePoints,
|
||||||
|
deviceInfo = deviceInfo
|
||||||
|
)
|
||||||
|
featurePointApi.saveFeaturePoints(featureRequest)
|
||||||
|
|
||||||
arAnnotationApi.saveAnchor(request)
|
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadRoomAnchors(roomId: String): List<AnchorData> {
|
suspend fun loadRoomAnchorsWithFeatures(roomId: String): List<AnchorWithFeatures> {
|
||||||
return arAnnotationApi.getRoomAnchors(roomId).anchors
|
val anchors = arAnnotationApi.getRoomAnchors(roomId).anchors
|
||||||
|
return anchors.map { anchor ->
|
||||||
|
val features = featurePointApi.getAnchorFeatures(anchor.anchorId).featurePoints
|
||||||
|
AnchorWithFeatures(anchor, features)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveAnnotation(annotation: ArAnnotation): Result<Unit> {
|
suspend fun saveAnnotation(annotation: ArAnnotation): Result<Unit> {
|
||||||
@@ -466,9 +614,59 @@ class ArAnnotationService @Inject constructor(
|
|||||||
suspend fun loadRoomAnnotations(roomId: String): List<ArAnnotation> {
|
suspend fun loadRoomAnnotations(roomId: String): List<ArAnnotation> {
|
||||||
return arAnnotationApi.getRoomAnnotations(roomId).annotations
|
return arAnnotationApi.getRoomAnnotations(roomId).annotations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun validateSpatialConsistency(
|
||||||
|
anchorId: String,
|
||||||
|
currentPose: Pose,
|
||||||
|
deviceInfo: DeviceInfo
|
||||||
|
): Result<SpatialValidationResult> {
|
||||||
|
return try {
|
||||||
|
val request = SpatialValidationRequest(
|
||||||
|
anchorId = anchorId,
|
||||||
|
currentPosition = listOf(currentPose.tx(), currentPose.ty(), currentPose.tz()),
|
||||||
|
currentRotation = listOf(currentPose.qx(), currentPose.qy(), currentPose.qz(), currentPose.qw()),
|
||||||
|
deviceInfo = deviceInfo
|
||||||
|
)
|
||||||
|
val result = arAnnotationApi.validateSpatialConsistency(request)
|
||||||
|
Result.success(result)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// data/model/ArAnnotation.kt - 新增
|
// 数据模型增强
|
||||||
|
data class AnchorWithFeatures(
|
||||||
|
val anchor: AnchorData,
|
||||||
|
val featurePoints: List<FeaturePoint>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FeaturePoint(
|
||||||
|
val position: List<Float>,
|
||||||
|
val descriptor: String, // base64编码的特征描述符
|
||||||
|
val qualityScore: Float,
|
||||||
|
val timestamp: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class DeviceInfo(
|
||||||
|
val model: String,
|
||||||
|
val arCoreVersion: String,
|
||||||
|
val cameraCalibration: CameraCalibration?,
|
||||||
|
val imuCalibration: ImuCalibration?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CameraCalibration(
|
||||||
|
val focalLength: List<Float>,
|
||||||
|
val principalPoint: List<Float>,
|
||||||
|
val distortionCoefficients: List<Float>?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ImuCalibration(
|
||||||
|
val gyroBias: List<Float>?,
|
||||||
|
val accelBias: List<Float>?,
|
||||||
|
val magnetometerBias: List<Float>?
|
||||||
|
)
|
||||||
|
|
||||||
data class ArAnnotation(
|
data class ArAnnotation(
|
||||||
val id: String,
|
val id: String,
|
||||||
val anchorId: String,
|
val anchorId: String,
|
||||||
@@ -478,12 +676,21 @@ data class ArAnnotation(
|
|||||||
val size: Float,
|
val size: Float,
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val createdBy: String,
|
val createdBy: String,
|
||||||
|
val spatialTransform: SpatialTransform? = null,
|
||||||
val createdAt: Long = System.currentTimeMillis(),
|
val createdAt: Long = System.currentTimeMillis(),
|
||||||
val updatedAt: Long = System.currentTimeMillis()
|
val updatedAt: Long = System.currentTimeMillis(),
|
||||||
|
val version: Int = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SpatialTransform(
|
||||||
|
val position: List<Float>,
|
||||||
|
val rotation: List<Float>,
|
||||||
|
val scale: List<Float>,
|
||||||
|
val confidence: Float
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class AnnotationType {
|
enum class AnnotationType {
|
||||||
ARROW, CIRCLE, RECTANGLE, TEXT, HIGHLIGHT
|
ARROW, CIRCLE, RECTANGLE, TEXT, HIGHLIGHT, MEASUREMENT, NOTE
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1025,6 +1232,139 @@ GET /ar/analytics/accuracy?room_id={roomId}
|
|||||||
GET /ar/analytics/compatibility
|
GET /ar/analytics/compatibility
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 7. 特征点管理API(关键补充)
|
||||||
|
```kotlin
|
||||||
|
// 保存ARCore特征点数据
|
||||||
|
POST /ar/feature-points/save
|
||||||
|
Content-Type: application/json
|
||||||
|
{
|
||||||
|
"anchor_id": "string",
|
||||||
|
"room_id": "string",
|
||||||
|
"feature_points": [
|
||||||
|
{
|
||||||
|
"position": [0.0, 0.0, 0.0],
|
||||||
|
"descriptor": "base64_encoded_descriptor",
|
||||||
|
"quality_score": 0.95
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"device_info": {
|
||||||
|
"model": "Pixel 6",
|
||||||
|
"ar_core_version": "1.50.0",
|
||||||
|
"camera_calibration": {
|
||||||
|
"focal_length": [1000.0, 1000.0],
|
||||||
|
"principal_point": [500.0, 500.0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取特征点数据用于重定位
|
||||||
|
GET /ar/feature-points/{anchorId}
|
||||||
|
|
||||||
|
// 批量特征点匹配(跨设备空间对齐)
|
||||||
|
POST /ar/feature-points/match
|
||||||
|
{
|
||||||
|
"anchor_id": "string",
|
||||||
|
"query_features": [
|
||||||
|
{
|
||||||
|
"position": [0.0, 0.0, 0.0],
|
||||||
|
"descriptor": "base64_encoded_descriptor"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_results": 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特征点质量评估
|
||||||
|
POST /ar/feature-points/quality-assessment
|
||||||
|
{
|
||||||
|
"anchor_id": "string",
|
||||||
|
"feature_points": [
|
||||||
|
{
|
||||||
|
"position": [0.0, 0.0, 0.0],
|
||||||
|
"descriptor": "base64_encoded_descriptor"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. 空间一致性验证API
|
||||||
|
```kotlin
|
||||||
|
// 空间坐标转换验证
|
||||||
|
POST /ar/spatial/validate-transform
|
||||||
|
{
|
||||||
|
"source_anchor": "anchor_123",
|
||||||
|
"target_anchor": "anchor_456",
|
||||||
|
"transform_matrix": [
|
||||||
|
[1.0, 0.0, 0.0, 0.0],
|
||||||
|
[0.0, 1.0, 0.0, 0.0],
|
||||||
|
[0.0, 0.0, 1.0, 0.0],
|
||||||
|
[1.5, 2.0, 0.5, 1.0]
|
||||||
|
],
|
||||||
|
"confidence": 0.92
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多设备空间对齐状态
|
||||||
|
GET /ar/spatial/alignment-status?room_id={roomId}
|
||||||
|
|
||||||
|
// 强制空间重新对齐
|
||||||
|
POST /ar/spatial/realign
|
||||||
|
{
|
||||||
|
"room_id": "string",
|
||||||
|
"anchor_ids": ["anchor1", "anchor2", "anchor3"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. 标注版本管理API
|
||||||
|
```kotlin
|
||||||
|
// 创建标注版本
|
||||||
|
POST /ar/annotations/version
|
||||||
|
{
|
||||||
|
"room_id": "string",
|
||||||
|
"version_name": "v1.0",
|
||||||
|
"description": "初始标注版本",
|
||||||
|
"created_by": "user123"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取标注版本历史
|
||||||
|
GET /ar/annotations/{roomId}/versions
|
||||||
|
|
||||||
|
// 恢复到特定版本
|
||||||
|
POST /ar/annotations/{roomId}/restore/{versionId}
|
||||||
|
|
||||||
|
// 标注差异比较
|
||||||
|
GET /ar/annotations/{roomId}/diff?version1={v1}&version2={v2}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. 设备性能优化API
|
||||||
|
```kotlin
|
||||||
|
// 设备性能基准上报
|
||||||
|
POST /ar/device/performance
|
||||||
|
{
|
||||||
|
"device_model": "Pixel 6",
|
||||||
|
"ar_core_version": "1.50.0",
|
||||||
|
"performance_metrics": {
|
||||||
|
"frame_rate": 60.0,
|
||||||
|
"tracking_quality": 0.95,
|
||||||
|
"memory_usage": 256,
|
||||||
|
"battery_drain": 15.0
|
||||||
|
},
|
||||||
|
"recommended_settings": {
|
||||||
|
"max_annotations": 50,
|
||||||
|
"feature_point_density": 0.8,
|
||||||
|
"texture_quality": "medium"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备优化配置
|
||||||
|
GET /ar/device/{deviceModel}/optimization
|
||||||
|
|
||||||
|
// 性能问题诊断
|
||||||
|
POST /ar/device/diagnose
|
||||||
|
{
|
||||||
|
"device_info": {...},
|
||||||
|
"performance_issues": ["low_framerate", "high_memory"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 阶段8:优化和兼容性(增强版)
|
## 📋 阶段8:优化和兼容性(增强版)
|
||||||
|
|||||||
1938
docs/ui.html
1938
docs/ui.html
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user