feat(push): use Firebase token auto registration
这个提交包含在:
父节点
4677717343
当前提交
65bdb352bf
11
README.md
11
README.md
@ -230,7 +230,16 @@ data class ImMessage(
|
|||||||
|
|
||||||
### 推送接入
|
### 推送接入
|
||||||
|
|
||||||
在 `XuqmSDK.login()` 成功后,SDK 会自动完成厂商初始化、设备注册与 token 上传,不再要求业务侧单独调用注册接口。`logout()` 时会自动注销当前设备绑定。
|
在 `XuqmSDK.login()` 成功后,SDK 会自动完成当前系统推送 token 的注册与上传,不再要求业务侧手工传入 token。`logout()` 时会自动注销当前设备绑定。
|
||||||
|
|
||||||
|
还可以按用户设置接收开关:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
PushSDK.setReceivePush(context, enabled = false)
|
||||||
|
PushSDK.setReceivePush(context, enabled = true)
|
||||||
|
```
|
||||||
|
|
||||||
|
如果项目接入了 Firebase Messaging,`sdk-push` 会通过 `FirebaseMessagingService.onNewToken()` 自动接收并上报 FCM token。对应服务已经随库注册,只要应用工程提供 Firebase 配置即可生效。
|
||||||
|
|
||||||
### 与 IM 联动
|
### 与 IM 联动
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ securityCrypto = "1.0.0"
|
|||||||
hiltNavigationCompose = "1.2.0"
|
hiltNavigationCompose = "1.2.0"
|
||||||
viewmodelCompose = "2.10.0"
|
viewmodelCompose = "2.10.0"
|
||||||
material = "1.13.0"
|
material = "1.13.0"
|
||||||
|
firebaseBom = "34.7.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@ -53,6 +54,8 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-
|
|||||||
androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" }
|
androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" }
|
||||||
coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
|
coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
|
||||||
coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" }
|
coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" }
|
||||||
|
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
|
||||||
|
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" }
|
||||||
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||||
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
|
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
|
||||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||||
|
|||||||
@ -2,12 +2,15 @@ package com.xuqm.sdk.sample.ui.environment
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
@ -15,11 +18,13 @@ import androidx.compose.material3.Button
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@ -31,6 +36,9 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.xuqm.sdk.XuqmSDK
|
||||||
|
import com.xuqm.sdk.push.PushSDK
|
||||||
|
import com.xuqm.sdk.push.model.PushRegistrationSnapshot
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@ -116,7 +124,8 @@ fun EnvironmentScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.padding(24.dp)
|
.padding(24.dp)
|
||||||
.imePadding(),
|
.imePadding()
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -187,6 +196,88 @@ fun EnvironmentScreen(
|
|||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
PushRegistrationSection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PushRegistrationSection() {
|
||||||
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
val currentUserId = XuqmSDK.currentLoginSession?.userId ?: AppDependencies.authRepository.getCurrentUserId()
|
||||||
|
var statusMessage by remember { mutableStateOf<String?>(null) }
|
||||||
|
var snapshot by remember { mutableStateOf<PushRegistrationSnapshot?>(null) }
|
||||||
|
|
||||||
|
fun refresh(): PushRegistrationSnapshot? {
|
||||||
|
val current = runCatching { PushSDK.currentRegistration(context) }.getOrNull()
|
||||||
|
snapshot = current
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "推送注册",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = if (currentUserId.isNullOrBlank()) {
|
||||||
|
"当前未登录,登录后会自动请求系统推送 token 并完成绑定。"
|
||||||
|
} else {
|
||||||
|
"当前登录用户:$currentUserId"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = if (snapshot?.hasPushToken == true) {
|
||||||
|
"系统 token:已就绪"
|
||||||
|
} else {
|
||||||
|
"系统 token:等待 Firebase 回调"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Switch(
|
||||||
|
checked = snapshot?.receivePush ?: true,
|
||||||
|
onCheckedChange = { enabled ->
|
||||||
|
PushSDK.setReceivePush(context, currentUserId, enabled)
|
||||||
|
snapshot = snapshot?.copy(receivePush = enabled) ?: snapshot
|
||||||
|
statusMessage = if (enabled) "已允许接收推送" else "已关闭接收推送"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(0.dp))
|
||||||
|
Text(
|
||||||
|
text = "接收推送",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "系统 token 由 `FirebaseMessagingService.onNewToken()` 自动上报;登录后会自动完成绑定。",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
|
||||||
|
statusMessage?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,4 +19,6 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":sdk-core"))
|
api(project(":sdk-core"))
|
||||||
|
implementation(platform(libs.firebase.bom))
|
||||||
|
implementation(libs.firebase.messaging)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application>
|
||||||
|
<service
|
||||||
|
android:name="com.xuqm.sdk.push.fcm.XuqmFirebaseMessagingService"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@ -1,35 +1,111 @@
|
|||||||
package com.xuqm.sdk.push
|
package com.xuqm.sdk.push
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
|
import com.xuqm.sdk.XuqmLoginSession
|
||||||
import com.xuqm.sdk.XuqmSDK
|
import com.xuqm.sdk.XuqmSDK
|
||||||
import com.xuqm.sdk.core.ServiceEndpointRegistry
|
import com.xuqm.sdk.core.ServiceEndpointRegistry
|
||||||
import com.xuqm.sdk.network.ApiClient
|
import com.xuqm.sdk.network.ApiClient
|
||||||
import com.xuqm.sdk.push.api.PushApi
|
import com.xuqm.sdk.push.api.PushApi
|
||||||
|
import com.xuqm.sdk.push.model.PushRegistrationSnapshot
|
||||||
|
import com.xuqm.sdk.push.model.PushVendor
|
||||||
|
import com.xuqm.sdk.push.storage.PushRegistrationStore
|
||||||
import com.xuqm.sdk.utils.DeviceUtils
|
import com.xuqm.sdk.utils.DeviceUtils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
object PushSDK {
|
object PushSDK {
|
||||||
|
|
||||||
private val api: PushApi get() = ApiClient.create(PushApi::class.java, ServiceEndpointRegistry.pushBaseUrl)
|
private val api: PushApi get() = ApiClient.create(PushApi::class.java, ServiceEndpointRegistry.pushBaseUrl)
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private val registeredUserId = AtomicReference<String?>(null)
|
private val registeredUserId = AtomicReference<String?>(null)
|
||||||
|
|
||||||
fun registerDevice(context: Context, userId: String) {
|
fun currentRegistration(context: Context): PushRegistrationSnapshot? {
|
||||||
XuqmSDK.requireInit()
|
XuqmSDK.requireInit()
|
||||||
val vendor = DeviceUtils.getVendor()
|
|
||||||
val deviceId = DeviceUtils.getDeviceId(context)
|
val deviceId = DeviceUtils.getDeviceId(context)
|
||||||
|
return store(context).load(deviceId = deviceId, fallbackVendor = detectVendor())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun updateNativePushToken(
|
||||||
|
context: Context,
|
||||||
|
vendor: PushVendor,
|
||||||
|
pushToken: String,
|
||||||
|
) {
|
||||||
|
XuqmSDK.requireInit()
|
||||||
|
val normalizedToken = pushToken.trim()
|
||||||
|
require(normalizedToken.isNotBlank()) { "pushToken must not be blank" }
|
||||||
|
store(context).save(vendor, normalizedToken)
|
||||||
|
val sessionUserId = XuqmSDK.currentLoginSession?.userId
|
||||||
|
if (sessionUserId != null) {
|
||||||
|
bindImUser(context, sessionUserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bindImUser(context: Context, userId: String) {
|
||||||
|
if (!isReceivePushEnabled(context)) return
|
||||||
|
ensureNativePushToken(context)
|
||||||
|
registerDevice(context, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unbindImUser(userId: String) {
|
||||||
|
unregisterDevice(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setReceivePush(
|
||||||
|
context: Context,
|
||||||
|
userId: String? = XuqmSDK.currentLoginSession?.userId,
|
||||||
|
enabled: Boolean,
|
||||||
|
) {
|
||||||
|
XuqmSDK.requireInit()
|
||||||
|
store(context).setReceivePush(enabled)
|
||||||
|
val resolvedUserId = userId ?: registeredUserId.get()
|
||||||
|
if (resolvedUserId != null) {
|
||||||
|
scope.launch {
|
||||||
|
runCatching {
|
||||||
|
api.setReceivePush(
|
||||||
|
appId = XuqmSDK.appId,
|
||||||
|
userId = resolvedUserId,
|
||||||
|
enabled = enabled,
|
||||||
|
)
|
||||||
|
if (enabled) {
|
||||||
|
bindImUser(context, resolvedUserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerDevice(
|
||||||
|
context: Context,
|
||||||
|
userId: String,
|
||||||
|
) {
|
||||||
|
XuqmSDK.requireInit()
|
||||||
|
val registration = currentRegistration(context)
|
||||||
|
val vendor = registration?.vendor ?: detectVendor()
|
||||||
|
val pushToken = registration?.pushToken?.takeIf { it.isNotBlank() }
|
||||||
|
if (pushToken.isNullOrBlank()) {
|
||||||
|
Log.w("XuqmPushSDK", "Native push token not ready yet, waiting for onNewToken()")
|
||||||
|
ensureNativePushToken(context)
|
||||||
|
return
|
||||||
|
}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
api.registerDevice(
|
api.registerDevice(
|
||||||
appId = XuqmSDK.appId,
|
appId = XuqmSDK.appId,
|
||||||
userId = userId,
|
userId = userId,
|
||||||
vendor = vendor,
|
vendor = vendor.name,
|
||||||
token = deviceId,
|
token = pushToken,
|
||||||
)
|
)
|
||||||
registeredUserId.set(userId)
|
registeredUserId.set(userId)
|
||||||
|
store(context).updateLastUserId(userId)
|
||||||
|
Log.i(
|
||||||
|
"XuqmPushSDK",
|
||||||
|
"Registered push device for userId=$userId vendor=${vendor.name}",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,18 +116,56 @@ object PushSDK {
|
|||||||
runCatching {
|
runCatching {
|
||||||
api.unregisterDevice(XuqmSDK.appId, userId)
|
api.unregisterDevice(XuqmSDK.appId, userId)
|
||||||
registeredUserId.compareAndSet(userId, null)
|
registeredUserId.compareAndSet(userId, null)
|
||||||
|
store(XuqmSDK.appContext).updateLastUserId(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSdkLogin(session: com.xuqm.sdk.XuqmLoginSession) {
|
fun onSdkLogin(session: XuqmLoginSession) {
|
||||||
val context = runCatching { XuqmSDK.appContext }.getOrNull() ?: return
|
val context = runCatching { XuqmSDK.appContext }.getOrNull() ?: return
|
||||||
if (registeredUserId.get() == session.userId) return
|
if (registeredUserId.get() == session.userId) return
|
||||||
registerDevice(context, session.userId)
|
bindImUser(context, session.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSdkLogout() {
|
fun onSdkLogout() {
|
||||||
val userId = registeredUserId.getAndSet(null) ?: return
|
val userId = registeredUserId.getAndSet(null) ?: return
|
||||||
unregisterDevice(userId)
|
unregisterDevice(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun detectVendor(): PushVendor = PushVendor.FCM
|
||||||
|
|
||||||
|
private fun store(context: Context): PushRegistrationStore =
|
||||||
|
PushRegistrationStore(context.applicationContext)
|
||||||
|
|
||||||
|
private fun isReceivePushEnabled(context: Context): Boolean =
|
||||||
|
store(context).load(
|
||||||
|
deviceId = DeviceUtils.getDeviceId(context),
|
||||||
|
fallbackVendor = detectVendor(),
|
||||||
|
)?.receivePush ?: true
|
||||||
|
|
||||||
|
private fun ensureNativePushToken(context: Context) {
|
||||||
|
val vendor = detectVendor()
|
||||||
|
if (vendor != PushVendor.FCM) return
|
||||||
|
val registration = currentRegistration(context)
|
||||||
|
if (registration?.pushToken?.isNotBlank() == true) return
|
||||||
|
runCatching { FirebaseMessaging.getInstance() }
|
||||||
|
.onSuccess { messaging ->
|
||||||
|
messaging.token
|
||||||
|
.addOnSuccessListener { token ->
|
||||||
|
if (token.isNotBlank()) {
|
||||||
|
store(context).save(PushVendor.FCM, token)
|
||||||
|
val sessionUserId = XuqmSDK.currentLoginSession?.userId
|
||||||
|
if (sessionUserId != null) {
|
||||||
|
registerDevice(context, sessionUserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener { error ->
|
||||||
|
Log.w("XuqmPushSDK", "Unable to fetch FCM token: ${error.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure { error ->
|
||||||
|
Log.w("XuqmPushSDK", "Firebase Messaging not available: ${error.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,9 @@
|
|||||||
package com.xuqm.sdk.push.api
|
package com.xuqm.sdk.push.api
|
||||||
|
|
||||||
import retrofit2.http.Body
|
|
||||||
import retrofit2.http.DELETE
|
import retrofit2.http.DELETE
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
data class RegisterDeviceRequest(
|
|
||||||
val userId: String,
|
|
||||||
val appId: String,
|
|
||||||
val platform: String,
|
|
||||||
val vendor: String,
|
|
||||||
val pushToken: String,
|
|
||||||
val deviceId: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
interface PushApi {
|
interface PushApi {
|
||||||
|
|
||||||
@POST("api/push/register")
|
@POST("api/push/register")
|
||||||
@ -29,4 +19,11 @@ interface PushApi {
|
|||||||
@Query("appId") appId: String,
|
@Query("appId") appId: String,
|
||||||
@Query("userId") userId: String,
|
@Query("userId") userId: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@POST("api/push/receive-push")
|
||||||
|
suspend fun setReceivePush(
|
||||||
|
@Query("appId") appId: String,
|
||||||
|
@Query("userId") userId: String,
|
||||||
|
@Query("enabled") enabled: Boolean,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.xuqm.sdk.push.fcm
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
|
import com.xuqm.sdk.push.PushSDK
|
||||||
|
import com.xuqm.sdk.push.model.PushVendor
|
||||||
|
|
||||||
|
class XuqmFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
|
|
||||||
|
override fun onNewToken(token: String) {
|
||||||
|
super.onNewToken(token)
|
||||||
|
Log.d(TAG, "FCM token refreshed length=${token.length}")
|
||||||
|
PushSDK.updateNativePushToken(applicationContext, PushVendor.FCM, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessageReceived(message: RemoteMessage) {
|
||||||
|
super.onMessageReceived(message)
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"FCM message from=${message.from.orEmpty()} dataKeys=${message.data.keys.joinToString(",")}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "XuqmFcmService"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.xuqm.sdk.push.model
|
||||||
|
|
||||||
|
data class PushRegistrationSnapshot(
|
||||||
|
val vendor: PushVendor,
|
||||||
|
val pushToken: String,
|
||||||
|
val deviceId: String,
|
||||||
|
val receivePush: Boolean = true,
|
||||||
|
val lastUserId: String? = null,
|
||||||
|
) {
|
||||||
|
val hasPushToken: Boolean get() = pushToken.isNotBlank()
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.xuqm.sdk.push.model
|
||||||
|
|
||||||
|
enum class PushVendor {
|
||||||
|
HUAWEI,
|
||||||
|
XIAOMI,
|
||||||
|
OPPO,
|
||||||
|
VIVO,
|
||||||
|
HONOR,
|
||||||
|
APNS,
|
||||||
|
FCM,
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
package com.xuqm.sdk.push.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
|
import androidx.security.crypto.MasterKeys
|
||||||
|
import com.xuqm.sdk.push.model.PushRegistrationSnapshot
|
||||||
|
import com.xuqm.sdk.push.model.PushVendor
|
||||||
|
|
||||||
|
internal class PushRegistrationStore(context: Context) {
|
||||||
|
|
||||||
|
private val prefs = EncryptedSharedPreferences.create(
|
||||||
|
PREFS_NAME,
|
||||||
|
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
|
||||||
|
context.applicationContext,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun load(deviceId: String, fallbackVendor: PushVendor): PushRegistrationSnapshot? {
|
||||||
|
val vendorName = prefs.getString(KEY_VENDOR, null) ?: fallbackVendor.name
|
||||||
|
val pushToken = prefs.getString(KEY_PUSH_TOKEN, null).orEmpty()
|
||||||
|
val receivePush = prefs.getBoolean(KEY_RECEIVE_PUSH, true)
|
||||||
|
val lastUserId = prefs.getString(KEY_LAST_USER_ID, null)
|
||||||
|
val vendor = runCatching { PushVendor.valueOf(vendorName) }.getOrDefault(fallbackVendor)
|
||||||
|
if (pushToken.isBlank() && lastUserId.isNullOrBlank()) return null
|
||||||
|
return PushRegistrationSnapshot(
|
||||||
|
vendor = vendor,
|
||||||
|
pushToken = pushToken,
|
||||||
|
deviceId = deviceId,
|
||||||
|
receivePush = receivePush,
|
||||||
|
lastUserId = lastUserId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(vendor: PushVendor, pushToken: String) {
|
||||||
|
prefs.edit()
|
||||||
|
.putString(KEY_VENDOR, vendor.name)
|
||||||
|
.putString(KEY_PUSH_TOKEN, pushToken)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setReceivePush(enabled: Boolean) {
|
||||||
|
prefs.edit()
|
||||||
|
.putBoolean(KEY_RECEIVE_PUSH, enabled)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLastUserId(userId: String?) {
|
||||||
|
prefs.edit()
|
||||||
|
.apply {
|
||||||
|
if (userId.isNullOrBlank()) remove(KEY_LAST_USER_ID) else putString(KEY_LAST_USER_ID, userId)
|
||||||
|
}
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearToken() {
|
||||||
|
prefs.edit()
|
||||||
|
.remove(KEY_VENDOR)
|
||||||
|
.remove(KEY_PUSH_TOKEN)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREFS_NAME = "xuqm_push_settings"
|
||||||
|
private const val KEY_VENDOR = "push_vendor"
|
||||||
|
private const val KEY_PUSH_TOKEN = "push_token"
|
||||||
|
private const val KEY_RECEIVE_PUSH = "receive_push"
|
||||||
|
private const val KEY_LAST_USER_ID = "last_user_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户