diff --git a/app/build.gradle b/app/build.gradle index d754371..05f7144 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,8 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' + id 'dagger.hilt.android.plugin' } android { @@ -32,6 +34,10 @@ android { } kotlinOptions { jvmTarget = '17' + freeCompilerArgs += [ + "-opt-in=kotlin.RequiresOptIn", + "-Xjvm-default=all" + ] } buildFeatures { compose true @@ -47,6 +53,7 @@ android { } dependencies { + // Core implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4' implementation 'androidx.activity:activity-compose:1.9.1' @@ -57,7 +64,33 @@ dependencies { implementation 'androidx.compose.material3:material3' implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4' 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' androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' diff --git a/app/src/main/java/com/xsynergy/android/XSynergyApplication.kt b/app/src/main/java/com/xsynergy/android/XSynergyApplication.kt index 673fd9b..5b47a76 100644 --- a/app/src/main/java/com/xsynergy/android/XSynergyApplication.kt +++ b/app/src/main/java/com/xsynergy/android/XSynergyApplication.kt @@ -1,20 +1,22 @@ package com.xsynergy.android import android.app.Application +import dagger.hilt.android.HiltAndroidApp import com.xsynergy.android.utils.PerformanceOptimizer /** * XSynergy Application class for app-wide initialization * Optimizes app startup and performance monitoring */ +@HiltAndroidApp class XSynergyApplication : Application() { - + override fun onCreate() { super.onCreate() - + // Initialize performance monitoring PerformanceOptimizer.initialize(this) - + // Optimize network settings for low latency PerformanceOptimizer.optimizeNetworkLatency() } diff --git a/app/src/main/java/com/xsynergy/android/data/local/AppDatabase.kt b/app/src/main/java/com/xsynergy/android/data/local/AppDatabase.kt new file mode 100644 index 0000000..d553a7e --- /dev/null +++ b/app/src/main/java/com/xsynergy/android/data/local/AppDatabase.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/xsynergy/android/data/local/UserDao.kt b/app/src/main/java/com/xsynergy/android/data/local/UserDao.kt new file mode 100644 index 0000000..4f7cb83 --- /dev/null +++ b/app/src/main/java/com/xsynergy/android/data/local/UserDao.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/com/xsynergy/android/data/local/UserEntity.kt b/app/src/main/java/com/xsynergy/android/data/local/UserEntity.kt new file mode 100644 index 0000000..c7ef9b9 --- /dev/null +++ b/app/src/main/java/com/xsynergy/android/data/local/UserEntity.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/xsynergy/android/data/model/LoginRequest.kt b/app/src/main/java/com/xsynergy/android/data/model/LoginRequest.kt new file mode 100644 index 0000000..539db55 --- /dev/null +++ b/app/src/main/java/com/xsynergy/android/data/model/LoginRequest.kt @@ -0,0 +1,6 @@ +package com.xsynergy.android.data.model + +data class LoginRequest( + val phone: String, + val code: String +) \ No newline at end of file diff --git a/app/src/main/java/com/xsynergy/android/data/model/LoginResponse.kt b/app/src/main/java/com/xsynergy/android/data/model/LoginResponse.kt new file mode 100644 index 0000000..aac4266 --- /dev/null +++ b/app/src/main/java/com/xsynergy/android/data/model/LoginResponse.kt @@ -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( + 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 +) + +data class CodecInfo( + val mime: String +) \ No newline at end of file diff --git a/app/src/main/java/com/xsynergy/android/data/model/User.kt b/app/src/main/java/com/xsynergy/android/data/model/User.kt new file mode 100644 index 0000000..604f5b7 --- /dev/null +++ b/app/src/main/java/com/xsynergy/android/data/model/User.kt @@ -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() +) \ No newline at end of file diff --git a/app/src/main/java/com/xsynergy/android/data/remote/GxrtcApi.kt b/app/src/main/java/com/xsynergy/android/data/remote/GxrtcApi.kt new file mode 100644 index 0000000..76530e0 --- /dev/null +++ b/app/src/main/java/com/xsynergy/android/data/remote/GxrtcApi.kt @@ -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> + + @POST("room/") + @FormUrlEncoded + suspend fun createRoom(@Field("room") roomName: String): Response> + + @GET("room/") + suspend fun getRooms(): Response>> +} \ No newline at end of file diff --git a/app/src/main/java/com/xsynergy/android/data/remote/RetrofitClient.kt b/app/src/main/java/com/xsynergy/android/data/remote/RetrofitClient.kt new file mode 100644 index 0000000..1e2f001 --- /dev/null +++ b/app/src/main/java/com/xsynergy/android/data/remote/RetrofitClient.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/xsynergy/android/data/repository/AuthRepository.kt b/app/src/main/java/com/xsynergy/android/data/repository/AuthRepository.kt new file mode 100644 index 0000000..e19c065 --- /dev/null +++ b/app/src/main/java/com/xsynergy/android/data/repository/AuthRepository.kt @@ -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 { + 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 { + 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) +} \ No newline at end of file diff --git a/app/src/main/java/com/xsynergy/android/di/NetworkModule.kt b/app/src/main/java/com/xsynergy/android/di/NetworkModule.kt new file mode 100644 index 0000000..d3568a0 --- /dev/null +++ b/app/src/main/java/com/xsynergy/android/di/NetworkModule.kt @@ -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) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index a967598..24a82b0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ buildscript { ext.kotlin_version = '1.9.24' ext.compose_version = '1.6.8' + ext.hilt_version = '2.48' repositories { google() mavenCentral() @@ -8,6 +9,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.13.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" } } diff --git a/docs/dev-plan-ds.md b/docs/dev-plan-ds.md index 404fc98..ccdb374 100644 --- a/docs/dev-plan-ds.md +++ b/docs/dev-plan-ds.md @@ -25,6 +25,14 @@ object NetworkModule { @Provides @Singleton 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 - 新增 @@ -44,6 +52,128 @@ interface GxrtcApi { suspend fun getRooms(): Response>> } +// data/remote/ArAnnotationApi.kt - 新增 +interface ArAnnotationApi { + @POST("ar/anchor/save") + suspend fun saveAnchor(@Body request: SaveAnchorRequest): Response> + + @GET("ar/room/{roomId}/anchors") + suspend fun getRoomAnchors(@Path("roomId") roomId: String): Response> + + @POST("ar/annotation/save") + suspend fun saveAnnotation(@Body annotation: ArAnnotation): Response> + + @GET("ar/room/{roomId}/annotations") + suspend fun getRoomAnnotations(@Path("roomId") roomId: String): Response> + + @DELETE("ar/annotation/{annotationId}") + suspend fun deleteAnnotation(@Path("annotationId") annotationId: String): Response> + + @POST("ar/spatial/validate") + suspend fun validateSpatialConsistency(@Body request: SpatialValidationRequest): Response> + + @POST("ar/annotations/version") + suspend fun createAnnotationVersion(@Body request: CreateVersionRequest): Response> + + @GET("ar/annotations/{roomId}/versions") + suspend fun getAnnotationVersions(@Path("roomId") roomId: String): Response>> +} + +// data/remote/FeaturePointApi.kt - 新增 +interface FeaturePointApi { + @POST("ar/feature-points/save") + suspend fun saveFeaturePoints(@Body request: SaveFeaturePointsRequest): Response> + + @GET("ar/feature-points/{anchorId}") + suspend fun getAnchorFeatures(@Path("anchorId") anchorId: String): Response> + + @POST("ar/feature-points/match") + suspend fun matchFeaturePoints(@Body request: FeatureMatchRequest): Response> + + @POST("ar/feature-points/quality-assessment") + suspend fun assessFeatureQuality(@Body request: FeatureQualityRequest): Response> +} + +// 请求和响应数据模型 +data class SaveAnchorRequest( + val roomId: String, + val anchorId: String, + val position: List, + val rotation: List, + val createdBy: String +) + +data class AnchorListResponse(val anchors: List) + +data class AnnotationListResponse(val annotations: List) + +data class SaveFeaturePointsRequest( + val anchorId: String, + val roomId: String, + val featurePoints: List, + val deviceInfo: DeviceInfo +) + +data class FeaturePointListResponse(val featurePoints: List) + +data class SpatialValidationRequest( + val anchorId: String, + val currentPosition: List, + val currentRotation: List, + 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, + val maxResults: Int = 10 +) + +data class FeatureMatchResponse( + val matches: List, + val confidence: Float +) + +data class FeatureMatch( + val featurePoint: FeaturePoint, + val similarity: Float, + val spatialDistance: Float +) + +data class FeatureQualityRequest( + val anchorId: String, + val featurePoints: List +) + +data class FeatureQualityResponse( + val overallQuality: Float, + val individualScores: Map, + val recommendations: List +) + +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( @SerializedName("meta") val meta: Meta, @@ -423,35 +553,53 @@ data class AnchorData( ) ``` -#### 4.2 AR标注持久化服务 +#### 4.2 AR标注持久化服务(增强版) ```kotlin // domain/service/ArAnnotationService.kt - 新增 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, anchorId: String, - position: Pose + position: Pose, + featurePoints: List, + deviceInfo: DeviceInfo ): Result { return try { - val request = SaveAnchorRequest( + // 保存锚点基本信息 + val anchorRequest = SaveAnchorRequest( roomId = roomId, anchorId = anchorId, position = listOf(position.tx(), position.ty(), position.tz()), 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) } catch (e: Exception) { Result.failure(e) } } - suspend fun loadRoomAnchors(roomId: String): List { - return arAnnotationApi.getRoomAnchors(roomId).anchors + suspend fun loadRoomAnchorsWithFeatures(roomId: String): List { + 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 { @@ -466,9 +614,59 @@ class ArAnnotationService @Inject constructor( suspend fun loadRoomAnnotations(roomId: String): List { return arAnnotationApi.getRoomAnnotations(roomId).annotations } + + suspend fun validateSpatialConsistency( + anchorId: String, + currentPose: Pose, + deviceInfo: DeviceInfo + ): Result { + 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 +) + +data class FeaturePoint( + val position: List, + 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, + val principalPoint: List, + val distortionCoefficients: List? +) + +data class ImuCalibration( + val gyroBias: List?, + val accelBias: List?, + val magnetometerBias: List? +) + data class ArAnnotation( val id: String, val anchorId: String, @@ -478,12 +676,21 @@ data class ArAnnotation( val size: Float, val roomId: String, val createdBy: String, + val spatialTransform: SpatialTransform? = null, 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, + val rotation: List, + val scale: List, + val confidence: Float ) 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 ``` +### 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:优化和兼容性(增强版) diff --git a/docs/ui.html b/docs/ui.html index 62912fc..ceb490a 100644 --- a/docs/ui.html +++ b/docs/ui.html @@ -1493,6 +1493,817 @@ border-radius: 6px; cursor: pointer; } + + /* Settings Screen Styles */ + .settings-group { + margin-bottom: var(--spacing-xxxl); + } + + .settings-subtitle { + font-size: var(--font-size-base); + font-weight: 600; + color: var(--text-primary); + margin-bottom: var(--spacing-lg); + padding-left: var(--spacing-sm); + border-left: 3px solid var(--primary); + } + + .setting-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-md) 0; + border-bottom: 1px solid var(--border-primary); + } + + .setting-label { + font-size: var(--font-size-base); + color: var(--text-primary); + } + + .setting-select { + padding: var(--spacing-sm); + border: 1px solid var(--border-primary); + border-radius: var(--radius-sm); + background: var(--bg-primary); + } + + .setting-slider { + width: 100px; + } + + /* Switch Toggle */ + .switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; + } + + .switch input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--text-tertiary); + transition: .4s; + border-radius: 24px; + } + + .slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: .4s; + border-radius: 50%; + } + + input:checked + .slider { + background-color: var(--primary); + } + + input:checked + .slider:before { + transform: translateX(26px); + } + + /* AI Assistant Styles */ + .ai-screen { + flex: 1; + padding: var(--spacing-lg); + display: flex; + flex-direction: column; + } + + .ai-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--spacing-lg); + } + + .back-btn { + background: none; + border: none; + font-size: var(--font-size-xl); + cursor: pointer; + color: var(--text-primary); + } + + .voice-btn { + background: none; + border: none; + font-size: var(--font-size-lg); + cursor: pointer; + } + + .chat-container { + flex: 1; + overflow-y: auto; + margin-bottom: var(--spacing-lg); + } + + .message { + display: flex; + margin-bottom: var(--spacing-md); + align-items: flex-start; + } + + .message-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: var(--primary); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + margin-right: var(--spacing-sm); + } + + .message-content { + background: var(--secondary); + padding: var(--spacing-md); + border-radius: var(--radius-lg); + max-width: 70%; + } + + .input-container { + display: flex; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-lg); + } + + .ai-input { + flex: 1; + padding: var(--spacing-md); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + } + + .send-btn { + padding: var(--spacing-md) var(--spacing-lg); + background: var(--primary); + color: white; + border: none; + border-radius: var(--radius-md); + cursor: pointer; + } + + .quick-questions { + margin-top: var(--spacing-lg); + } + + .question-chips { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-sm); + margin-top: var(--spacing-md); + } + + .question-chip { + padding: var(--spacing-sm) var(--spacing-md); + background: var(--secondary); + border: 1px solid var(--primary-light); + border-radius: 20px; + font-size: var(--font-size-sm); + cursor: pointer; + transition: all var(--transition-fast); + } + + .question-chip:hover { + background: var(--primary-light); + color: white; + } + + /* Whiteboard Styles */ + .whiteboard-container { + flex: 1; + display: flex; + flex-direction: column; + padding: var(--spacing-lg); + } + + .whiteboard-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--spacing-lg); + } + + .whiteboard-tools { + display: flex; + gap: var(--spacing-sm); + } + + #whiteboard-canvas { + flex: 1; + border: 2px solid var(--border-primary); + border-radius: var(--radius-md); + background: white; + cursor: crosshair; + } + + .color-palette { + display: flex; + gap: var(--spacing-sm); + margin: var(--spacing-md) 0; + } + + .brush-size { + display: flex; + align-items: center; + gap: var(--spacing-md); + } + + .size-slider { + flex: 1; + } + + /* File Sharing Styles */ + .file-screen { + flex: 1; + padding: var(--spacing-lg); + } + + .file-header { + display: flex; + align-items: center; + gap: var(--spacing-md); + margin-bottom: var(--spacing-xl); + } + + .file-upload-area { + border: 2px dashed var(--border-primary); + border-radius: var(--radius-lg); + padding: var(--spacing-xxxxl); + text-align: center; + margin-bottom: var(--spacing-xl); + cursor: pointer; + transition: all var(--transition-fast); + } + + .file-upload-area:hover { + border-color: var(--primary); + background: var(--secondary); + } + + .upload-icon { + font-size: var(--font-size-xxxl); + margin-bottom: var(--spacing-md); + } + + .upload-btn { + padding: var(--spacing-sm) var(--spacing-lg); + background: var(--primary); + color: white; + border: none; + border-radius: var(--radius-md); + cursor: pointer; + margin-top: var(--spacing-md); + } + + .file-list { + margin-bottom: var(--spacing-xl); + } + + .file-item { + display: flex; + align-items: center; + padding: var(--spacing-md); + border: 1px solid var(--border-primary); + border-radius: var(--radius-md); + margin-bottom: var(--spacing-sm); + background: var(--bg-primary); + } + + .file-icon { + font-size: var(--font-size-xl); + margin-right: var(--spacing-md); + } + + .file-info { + flex: 1; + } + + .file-name { + font-weight: 600; + color: var(--text-primary); + } + + .file-size { + font-size: var(--font-size-sm); + color: var(--text-secondary); + } + + .file-action { + padding: var(--spacing-sm) var(--spacing-md); + background: var(--primary); + color: white; + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + } + + .share-options { + margin-top: var(--spacing-xl); + } + + .share-option { + margin-bottom: var(--spacing-md); + } + + .participant-select { + width: 100%; + padding: var(--spacing-sm); + border: 1px solid var(--border-primary); + border-radius: var(--radius-sm); + margin-top: var(--spacing-sm); + } + + /* Playback Styles */ + .playback-screen { + flex: 1; + display: flex; + flex-direction: column; + } + + .playback-header { + padding: var(--spacing-lg); + border-bottom: 1px solid var(--border-primary); + } + + .playback-info { + display: flex; + gap: var(--spacing-lg); + color: var(--text-secondary); + font-size: var(--font-size-sm); + margin-top: var(--spacing-sm); + } + + .video-player { + flex: 1; + position: relative; + background: black; + } + + .playback-video { + width: 100%; + height: 100%; + object-fit: contain; + } + + .playback-controls { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0,0,0,0.8); + padding: var(--spacing-md); + display: flex; + align-items: center; + gap: var(--spacing-md); + } + + .progress-bar { + flex: 1; + } + + .time-display { + color: white; + font-size: var(--font-size-sm); + } + + .playback-sidebar { + width: 300px; + border-left: 1px solid var(--border-primary); + display: flex; + flex-direction: column; + } + + .sidebar-tabs { + display: flex; + border-bottom: 1px solid var(--border-primary); + } + + .tab-btn { + flex: 1; + padding: var(--spacing-md); + background: none; + border: none; + cursor: pointer; + border-bottom: 2px solid transparent; + } + + .tab-btn.active { + border-bottom-color: var(--primary); + color: var(--primary); + } + + .tab-content { + flex: 1; + padding: var(--spacing-md); + overflow-y: auto; + } + + .annotation-item { + padding: var(--spacing-sm); + border-bottom: 1px solid var(--border-primary); + cursor: pointer; + } + + .annotation-time { + color: var(--primary); + font-weight: 600; + margin-right: var(--spacing-sm); + } + + .transcript-item { + padding: var(--spacing-sm); + border-bottom: 1px solid var(--border-primary); + } + + .speaker { + font-weight: 600; + color: var(--primary); + } + + /* Drag and Drop Styles */ + .drop-zone.dragover { + background: var(--secondary); + border-color: var(--primary); + } + + /* Real-time Subtitle Overlay */ + .subtitle-overlay { + position: absolute; + bottom: 100px; + left: 50%; + transform: translateX(-50%); + background: rgba(0,0,0,0.8); + color: white; + padding: var(--spacing-md); + border-radius: var(--radius-md); + max-width: 80%; + text-align: center; + z-index: 20; + display: none; + } + + .subtitle-overlay.visible { + display: block; + } + + /* Screen Sharing Indicator */ + .screen-sharing-indicator { + position: absolute; + top: 10px; + right: 10px; + background: var(--danger); + color: white; + padding: var(--spacing-sm) var(--spacing-md); + border-radius: var(--radius-sm); + font-size: var(--font-size-sm); + z-index: 25; + display: none; + } + + .screen-sharing-indicator.visible { + display: block; + } + + /* New Components for Missing UI Elements */ + .participant-selector { + margin-bottom: var(--spacing-xl); + } + + .selected-participants { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-md); + } + + .participant-chip { + display: flex; + align-items: center; + gap: var(--spacing-xs); + background: var(--secondary); + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--radius-xl); + font-size: var(--font-size-sm); + } + + .remove-btn { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + font-size: var(--font-size-lg); + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + } + + .meeting-meta-info { + background: var(--bg-tertiary); + padding: var(--spacing-lg); + border-radius: var(--radius-md); + margin-bottom: var(--spacing-xl); + } + + .meta-item { + display: flex; + align-items: flex-start; + margin-bottom: var(--spacing-md); + } + + .meta-label { + font-weight: 600; + color: var(--text-primary); + min-width: 60px; + } + + .meta-value { + color: var(--text-secondary); + flex: 1; + } + + .minutes-section { + margin-bottom: var(--spacing-xl); + } + + .minutes-content { + background: var(--bg-tertiary); + padding: var(--spacing-lg); + border-radius: var(--radius-md); + line-height: 1.6; + } + + .decisions-list { + list-style: none; + padding: 0; + } + + .decisions-list li { + padding: var(--spacing-sm) 0; + border-bottom: 1px solid var(--border-primary); + position: relative; + padding-left: var(--spacing-lg); + } + + .decisions-list li::before { + content: '✓'; + position: absolute; + left: 0; + color: var(--success); + font-weight: bold; + } + + .todo-item { + display: flex; + align-items: center; + padding: var(--spacing-sm) 0; + border-bottom: 1px solid var(--border-primary); + } + + .todo-item input { + margin-right: var(--spacing-sm); + } + + .action-buttons { + display: flex; + gap: var(--spacing-md); + margin-top: var(--spacing-xl); + } + + .contact-info { + background: var(--bg-tertiary); + padding: var(--spacing-lg); + border-radius: var(--radius-md); + margin-bottom: var(--spacing-xl); + } + + .info-item { + display: flex; + margin-bottom: var(--spacing-md); + } + + .info-label { + font-weight: 600; + color: var(--text-primary); + min-width: 60px; + } + + .info-value { + color: var(--text-secondary); + flex: 1; + } + + .quick-actions { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + } + + /* Modal Styles */ + .modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: var(--z-modal-backdrop); + } + + .modal-container { + max-width: 90%; + max-height: 80%; + overflow: hidden; + } + + .modal { + background: var(--bg-primary); + border-radius: var(--radius-lg); + overflow: hidden; + box-shadow: var(--shadow-xl); + animation: material-enter 0.3s ease; + } + + .modal-header { + padding: var(--spacing-lg); + border-bottom: 1px solid var(--border-primary); + display: flex; + align-items: center; + justify-content: space-between; + } + + .modal-header h3 { + margin: 0; + font-size: var(--font-size-lg); + } + + .modal-close { + background: none; + border: none; + font-size: var(--font-size-xl); + cursor: pointer; + color: var(--text-secondary); + } + + .modal-content { + padding: var(--spacing-lg); + max-height: 400px; + overflow-y: auto; + } + + .modal-footer { + padding: var(--spacing-lg); + border-top: 1px solid var(--border-primary); + display: flex; + gap: var(--spacing-md); + justify-content: flex-end; + } + + .participant-item { + display: flex; + align-items: center; + padding: var(--spacing-md) 0; + border-bottom: 1px solid var(--border-primary); + } + + .participant-item:last-child { + border-bottom: none; + } + + .participant-item input { + margin-right: var(--spacing-md); + } + + .participant-info { + flex: 1; + display: flex; + flex-direction: column; + } + + .participant-info .name { + font-weight: 600; + color: var(--text-primary); + } + + .participant-info .role { + font-size: var(--font-size-sm); + color: var(--text-secondary); + } + + .file-upload-options { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + gap: var(--spacing-lg); + } + + .upload-option { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-lg); + border: 2px dashed var(--border-primary); + border-radius: var(--radius-md); + background: var(--bg-tertiary); + cursor: pointer; + transition: all var(--transition-fast); + } + + .upload-option:hover { + border-color: var(--primary); + background: var(--secondary); + } + + .upload-icon { + font-size: var(--font-size-xxl); + } + + /* Toast Styles */ + .toast-container { + position: fixed; + top: 100px; + right: var(--spacing-lg); + z-index: var(--z-tooltip); + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + } + + .toast { + background: var(--bg-primary); + border-radius: var(--radius-md); + padding: var(--spacing-md) var(--spacing-lg); + box-shadow: var(--shadow-lg); + display: flex; + align-items: center; + gap: var(--spacing-sm); + animation: slideInUp 0.3s ease; + } + + .toast.success { + border-left: 4px solid var(--success); + } + + .toast.error { + border-left: 4px solid var(--danger); + } + + .toast.warning { + border-left: 4px solid var(--warning); + } + + .toast.info { + border-left: 4px solid var(--primary); + } + + /* Accessibility High Contrast Mode */ + .high-contrast .ar-annotation { + background: yellow; + color: black; + border: 2px solid black; + } + + .high-contrast .control-btn { + border: 2px solid white; + } + + /* Language Switch Indicator */ + .language-indicator { + position: absolute; + top: 10px; + left: 10px; + background: rgba(0,0,0,0.7); + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + z-index: 15; + } @@ -1853,7 +2664,7 @@ 个人资料