feat(sdk): 添加认证仓库和登录会话管理功能
- 新增 AuthRepository 处理用户认证和加密存储 - 实现 SDK 登录会话管理和自动通知模块 - 添加 IM SDK 登录集成和会话传递 - 更新 API 响应结构支持 userSig 字段 - 添加文件存储服务和上传功能 - 完善文档说明 SDK 架构和集成方式
这个提交包含在:
父节点
00f2ad04b7
当前提交
087753075e
21
README.md
21
README.md
@ -56,18 +56,23 @@ dependencies {
|
||||
XuqmSDK.initialize(
|
||||
context = this,
|
||||
appId = "ak_your_app_id",
|
||||
debug = BuildConfig.DEBUG
|
||||
logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN
|
||||
)
|
||||
```
|
||||
|
||||
### 2. 用户登录后初始化 IM
|
||||
|
||||
```kotlin
|
||||
// 调用业务登录接口,拿到 userSig 后再登录 IM
|
||||
// 调用业务登录接口,拿到 userSig 后只需要登录一次 SDK
|
||||
val userSig = api.getUserSig(userId)
|
||||
ImSDK.login(userId = userId, userSig = userSig)
|
||||
XuqmSDK.login(
|
||||
userId = userId,
|
||||
userSig = userSig,
|
||||
nickname = profile.nickname,
|
||||
avatar = profile.avatar,
|
||||
)
|
||||
|
||||
// Push 设备注册在 PushSDK.initialize() 内部自动完成
|
||||
// 如果工程里集成了 sdk-im,SDK 会自动完成 IM 登录
|
||||
```
|
||||
|
||||
---
|
||||
@ -79,18 +84,18 @@ ImSDK.login(userId = userId, userSig = userSig)
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `appId` | String | 应用标识(租户平台获取) |
|
||||
| `debug` | Boolean | 开启日志 |
|
||||
| `logLevel` | LogLevel | 日志等级 |
|
||||
|
||||
### TokenStore
|
||||
|
||||
基于 `DataStore<Preferences>` 持久化存储。
|
||||
基于 `EncryptedSharedPreferences` 持久化存储。
|
||||
|
||||
```kotlin
|
||||
// 存储
|
||||
XuqmSDK.tokenStore.save("eyJ...")
|
||||
XuqmSDK.tokenStore.saveToken("eyJ...")
|
||||
|
||||
// 读取(协程)
|
||||
val token = XuqmSDK.tokenStore.get()
|
||||
val token = XuqmSDK.tokenStore.getToken()
|
||||
|
||||
// 清除(登出)
|
||||
XuqmSDK.tokenStore.clear()
|
||||
|
||||
@ -4,6 +4,7 @@ import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
@ -36,7 +37,8 @@ data class AuthProfile(
|
||||
|
||||
data class AuthResult(
|
||||
val demoToken: String,
|
||||
val imToken: String? = null,
|
||||
@SerializedName(value = "imToken", alternate = ["userSig"])
|
||||
val userSig: String? = null,
|
||||
val profile: AuthProfile,
|
||||
)
|
||||
|
||||
|
||||
@ -3,12 +3,12 @@ package com.xuqm.sdk.sample.data.repo
|
||||
import android.content.Context
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKeys
|
||||
import com.xuqm.sdk.im.ImSDK
|
||||
import com.xuqm.sdk.sample.data.api.DemoApi
|
||||
import com.xuqm.sdk.sample.data.api.DEMO_APP_ID
|
||||
import com.xuqm.sdk.sample.data.api.DemoApiFactory
|
||||
import com.xuqm.sdk.XuqmSDK
|
||||
import com.xuqm.sdk.sample.data.api.AuthResult
|
||||
import com.xuqm.sdk.sample.data.api.ChangePasswordRequest
|
||||
import com.xuqm.sdk.sample.data.api.DEMO_APP_ID
|
||||
import com.xuqm.sdk.sample.data.api.DemoApi
|
||||
import com.xuqm.sdk.sample.data.api.DemoApiFactory
|
||||
import com.xuqm.sdk.sample.data.api.LoginRequest
|
||||
import com.xuqm.sdk.sample.data.api.RegisterRequest
|
||||
import com.xuqm.sdk.sample.data.api.ResetPasswordRequest
|
||||
@ -42,6 +42,7 @@ class AuthRepository(context: Context) {
|
||||
.putString("user_id", profile.userId)
|
||||
.putString("nickname", profile.nickname)
|
||||
.putString("avatar", profile.avatar)
|
||||
.putString("user_sig", result.userSig)
|
||||
.apply()
|
||||
}
|
||||
|
||||
@ -51,8 +52,15 @@ class AuthRepository(context: Context) {
|
||||
val res = api.login(LoginRequest(DEMO_APP_ID, userId, password))
|
||||
val data = requireNotNull(res.data) { res.message ?: "Login failed" }
|
||||
saveSession(data)
|
||||
data.imToken?.takeIf { it.isNotBlank() }?.let { ImSDK.loginWithToken(data.profile.userId, it) }
|
||||
?: ImSDK.login(data.profile.userId, data.profile.nickname, data.profile.avatar)
|
||||
val userSig = requireNotNull(data.userSig?.takeIf { it.isNotBlank() }) {
|
||||
"Login succeeded but IM credential is missing"
|
||||
}
|
||||
XuqmSDK.login(
|
||||
userId = data.profile.userId,
|
||||
userSig = userSig,
|
||||
nickname = data.profile.nickname,
|
||||
avatar = data.profile.avatar,
|
||||
)
|
||||
UserData(data.profile.userId, data.profile.nickname, data.profile.avatar, data.profile.gender)
|
||||
}
|
||||
}
|
||||
@ -63,8 +71,15 @@ class AuthRepository(context: Context) {
|
||||
val res = api.register(RegisterRequest(DEMO_APP_ID, userId, password, nickname))
|
||||
val data = requireNotNull(res.data) { res.message ?: "Register failed" }
|
||||
saveSession(data)
|
||||
data.imToken?.takeIf { it.isNotBlank() }?.let { ImSDK.loginWithToken(data.profile.userId, it) }
|
||||
?: ImSDK.login(data.profile.userId, data.profile.nickname, data.profile.avatar)
|
||||
val userSig = requireNotNull(data.userSig?.takeIf { it.isNotBlank() }) {
|
||||
"Register succeeded but IM credential is missing"
|
||||
}
|
||||
XuqmSDK.login(
|
||||
userId = data.profile.userId,
|
||||
userSig = userSig,
|
||||
nickname = data.profile.nickname,
|
||||
avatar = data.profile.avatar,
|
||||
)
|
||||
UserData(data.profile.userId, data.profile.nickname, data.profile.avatar, data.profile.gender)
|
||||
}
|
||||
}
|
||||
@ -94,7 +109,7 @@ class AuthRepository(context: Context) {
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
ImSDK.disconnect()
|
||||
XuqmSDK.logout()
|
||||
prefs.edit().clear().apply()
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
package com.xuqm.sdk
|
||||
|
||||
data class XuqmLoginSession(
|
||||
val appId: String,
|
||||
val userId: String,
|
||||
val userSig: String,
|
||||
val nickname: String? = null,
|
||||
val avatar: String? = null,
|
||||
)
|
||||
@ -5,6 +5,8 @@ import com.xuqm.sdk.auth.TokenStore
|
||||
import com.xuqm.sdk.core.LogLevel
|
||||
import com.xuqm.sdk.core.SDKConfig
|
||||
import com.xuqm.sdk.network.ApiClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object XuqmSDK {
|
||||
|
||||
@ -15,6 +17,8 @@ object XuqmSDK {
|
||||
private set
|
||||
|
||||
private var initialized = false
|
||||
@Volatile
|
||||
private var loginSession: XuqmLoginSession? = null
|
||||
|
||||
fun initialize(
|
||||
context: Context,
|
||||
@ -32,4 +36,72 @@ object XuqmSDK {
|
||||
}
|
||||
|
||||
val appId: String get() = config.appId
|
||||
|
||||
val currentLoginSession: XuqmLoginSession?
|
||||
get() = loginSession
|
||||
|
||||
suspend fun login(
|
||||
userId: String,
|
||||
userSig: String,
|
||||
nickname: String? = null,
|
||||
avatar: String? = null,
|
||||
): XuqmLoginSession = withContext(Dispatchers.IO) {
|
||||
requireInit()
|
||||
val session = XuqmLoginSession(
|
||||
appId = appId,
|
||||
userId = userId,
|
||||
userSig = userSig,
|
||||
nickname = nickname,
|
||||
avatar = avatar,
|
||||
)
|
||||
loginSession = session
|
||||
tokenStore.saveToken(userSig)
|
||||
notifyOptionalModules("onSdkLogin", session)
|
||||
session
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
val session = loginSession
|
||||
loginSession = null
|
||||
tokenStore.clear()
|
||||
if (session != null) {
|
||||
notifyOptionalModulesSync("onSdkLogout")
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyOptionalModules(methodName: String, session: XuqmLoginSession) {
|
||||
listOf(
|
||||
"com.xuqm.sdk.im.ImSDK",
|
||||
"com.xuqm.sdk.push.PushSDK",
|
||||
"com.xuqm.sdk.update.UpdateSDK",
|
||||
).forEach { className ->
|
||||
runCatching {
|
||||
val clazz = Class.forName(className)
|
||||
val instance = clazz.getField("INSTANCE").get(null)
|
||||
val method = clazz.methods.firstOrNull { method ->
|
||||
method.name == methodName &&
|
||||
method.parameterTypes.size == 1 &&
|
||||
method.parameterTypes[0].name == XuqmLoginSession::class.java.name
|
||||
} ?: return@runCatching
|
||||
method.invoke(instance, session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyOptionalModulesSync(methodName: String) {
|
||||
listOf(
|
||||
"com.xuqm.sdk.im.ImSDK",
|
||||
"com.xuqm.sdk.push.PushSDK",
|
||||
"com.xuqm.sdk.update.UpdateSDK",
|
||||
).forEach { className ->
|
||||
runCatching {
|
||||
val clazz = Class.forName(className)
|
||||
val instance = clazz.getField("INSTANCE").get(null)
|
||||
val method = clazz.methods.firstOrNull { method ->
|
||||
method.name == methodName && method.parameterTypes.isEmpty()
|
||||
} ?: return@runCatching
|
||||
method.invoke(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
package com.xuqm.sdk.im
|
||||
|
||||
import com.xuqm.sdk.XuqmLoginSession
|
||||
import com.xuqm.sdk.XuqmSDK
|
||||
import com.xuqm.sdk.core.WS_URL
|
||||
import com.xuqm.sdk.im.api.AddMemberRequest
|
||||
import com.xuqm.sdk.im.api.CreateGroupRequest
|
||||
import com.xuqm.sdk.im.api.ImApi
|
||||
import com.xuqm.sdk.im.api.LoginRequest
|
||||
import com.xuqm.sdk.im.api.SetMutedRequest
|
||||
import com.xuqm.sdk.im.api.SetPinnedRequest
|
||||
import com.xuqm.sdk.im.api.UpdateGroupRequest
|
||||
@ -25,22 +25,32 @@ object ImSDK {
|
||||
var currentUserId: String = ""
|
||||
private set
|
||||
|
||||
suspend fun login(userId: String, nickname: String? = null, avatar: String? = null) =
|
||||
withContext(Dispatchers.IO) {
|
||||
XuqmSDK.requireInit()
|
||||
val res = api.login(LoginRequest(XuqmSDK.appId, userId, nickname, avatar))
|
||||
val token = requireNotNull(res.data?.token) { "IM login failed: ${res.message}" }
|
||||
currentUserId = userId
|
||||
connectWithToken(token)
|
||||
init {
|
||||
XuqmSDK.currentLoginSession?.let { onSdkLogin(it) }
|
||||
}
|
||||
|
||||
suspend fun loginWithToken(userId: String, token: String) =
|
||||
suspend fun login(
|
||||
userId: String,
|
||||
userSig: String,
|
||||
nickname: String? = null,
|
||||
avatar: String? = null,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
XuqmSDK.requireInit()
|
||||
currentUserId = userId
|
||||
connectWithToken(userSig)
|
||||
}
|
||||
|
||||
suspend fun loginWithUserSig(userId: String, userSig: String) =
|
||||
withContext(Dispatchers.IO) {
|
||||
XuqmSDK.requireInit()
|
||||
currentUserId = userId
|
||||
connectWithToken(token)
|
||||
connectWithToken(userSig)
|
||||
}
|
||||
|
||||
@Deprecated("Use loginWithUserSig(userId, userSig) instead.")
|
||||
suspend fun loginWithToken(userId: String, token: String) =
|
||||
loginWithUserSig(userId, token)
|
||||
|
||||
fun sendMessage(toId: String, chatType: String, msgType: String, content: String) {
|
||||
client?.sendMessage(toId, chatType, msgType, content)
|
||||
}
|
||||
@ -105,10 +115,17 @@ object ImSDK {
|
||||
fun removeListener(listener: ImEventListener) = client?.removeListener(listener)
|
||||
|
||||
fun disconnect() {
|
||||
client?.disconnect()
|
||||
client = null
|
||||
currentUserId = ""
|
||||
XuqmSDK.tokenStore.clear()
|
||||
disconnectInternal(clearTokenStore = true)
|
||||
}
|
||||
|
||||
fun onSdkLogin(session: XuqmLoginSession) {
|
||||
XuqmSDK.requireInit()
|
||||
currentUserId = session.userId
|
||||
connectWithToken(session.userSig)
|
||||
}
|
||||
|
||||
fun onSdkLogout() {
|
||||
disconnectInternal(clearTokenStore = false)
|
||||
}
|
||||
|
||||
private fun connectWithToken(token: String) {
|
||||
@ -117,4 +134,13 @@ object ImSDK {
|
||||
client = ImClient(WS_URL, token, XuqmSDK.appId)
|
||||
client?.connect()
|
||||
}
|
||||
|
||||
private fun disconnectInternal(clearTokenStore: Boolean) {
|
||||
client?.disconnect()
|
||||
client = null
|
||||
currentUserId = ""
|
||||
if (clearTokenStore) {
|
||||
XuqmSDK.tokenStore.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户