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