Compare commits

...

3 Commits

Author SHA1 Message Date
9eb3cb6f6b comments for login test, will revert to real token login soon.
Signed-off-by: chaoq <chaoq@gxtech.ltd>
2025-09-17 22:58:53 +08:00
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
16 changed files with 3856 additions and 12 deletions

View File

@@ -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'
@@ -57,7 +64,33 @@ dependencies {
implementation 'androidx.compose.material3:material3' implementation 'androidx.compose.material3:material3'
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'

View File

@@ -1,20 +1,22 @@
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() {
super.onCreate() super.onCreate()
// Initialize performance monitoring // Initialize performance monitoring
PerformanceOptimizer.initialize(this) PerformanceOptimizer.initialize(this)
// Optimize network settings for low latency // Optimize network settings for low latency
PerformanceOptimizer.optimizeNetworkLatency() 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

@@ -43,13 +43,13 @@ class LoginViewModel : ViewModel() {
fun login(onLoginSuccess: () -> Unit) { fun login(onLoginSuccess: () -> Unit) {
if (phoneNumber.isEmpty() || !isValidPhoneNumber(phoneNumber)) { if (phoneNumber.isEmpty() || !isValidPhoneNumber(phoneNumber)) {
errorMessage = "请输入有效的手机号" //errorMessage = "请输入有效的手机号"
return //return
} }
if (verificationCode.isEmpty() || verificationCode.length != 6) { if (verificationCode.isEmpty() || verificationCode.length != 6) {
errorMessage = "请输入6位验证码" //errorMessage = "请输入6位验证码"
return //return
} }
isLoading = true isLoading = true
@@ -59,7 +59,7 @@ class LoginViewModel : ViewModel() {
delay(1500) delay(1500)
// TODO: Implement actual Firebase verification // TODO: Implement actual Firebase verification
if (verificationCode == "123456") { // Mock verification if (verificationCode == "") { // Mock verification
errorMessage = null errorMessage = null
onLoginSuccess() onLoginSuccess()
} else { } else {
@@ -84,7 +84,8 @@ class LoginViewModel : ViewModel() {
} }
private fun isValidPhoneNumber(phone: String): Boolean { private fun isValidPhoneNumber(phone: String): Boolean {
return phone.matches(Regex("^1[3-9]\\d{9}$")) //return phone.matches(Regex("^1[3-9]\\d{9}$"))
return true // Mock verification
} }
fun clearError() { fun clearError() {

View File

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

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