Compare commits

...

2 Commits

Author SHA1 Message Date
3bdc40df94 Phase 1 complete. build Ok, apk OK
Signed-off-by: chaoq <chaoq@gxtech.ltd>
2025-09-17 22:50:29 +08:00
8875a5def1 dev plan base version
Signed-off-by: chaoq <chaoq@gxtech.ltd>
2025-09-17 20:48:56 +08:00
15 changed files with 3849 additions and 6 deletions

View File

@@ -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'

View File

@@ -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()
}

View File

@@ -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
}

View 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()
}

View File

@@ -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
)

View File

@@ -0,0 +1,6 @@
package com.xsynergy.android.data.model
data class LoginRequest(
val phone: String,
val code: String
)

View File

@@ -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
)

View 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()
)

View File

@@ -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>>>
}

View File

@@ -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)
}

View File

@@ -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)
}

View 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)
}
}

View File

@@ -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"
}
}

1530
docs/dev-plan-ds.md Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff