XuqmGroup-AndroidSDK/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt

319 行
12 KiB
Kotlin

2026-04-21 22:07:29 +08:00
package com.xuqm.sdk.push
import android.content.Context
import android.os.Build
import android.util.Log
import com.google.firebase.messaging.FirebaseMessaging
import com.xuqm.sdk.XuqmLoginSession
2026-04-21 22:07:29 +08:00
import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.core.ServiceEndpointRegistry
2026-04-21 22:07:29 +08:00
import com.xuqm.sdk.network.ApiClient
import com.xuqm.sdk.push.api.PushApi
import com.xuqm.sdk.push.api.PushConfigApi
import com.xuqm.sdk.push.model.PushRegistrationSnapshot
import com.xuqm.sdk.push.model.PushVendor
import com.xuqm.sdk.push.model.PushVendorConfig
import com.xuqm.sdk.push.storage.PushRegistrationStore
import com.xuqm.sdk.push.vendor.FcmPushService
import com.xuqm.sdk.push.vendor.HonorPushService
import com.xuqm.sdk.push.vendor.HuaweiPushService
import com.xuqm.sdk.push.vendor.OppoPushService
import com.xuqm.sdk.push.vendor.PushVendorInterface
import com.xuqm.sdk.push.vendor.VivoPushService
import com.xuqm.sdk.push.vendor.XiaomiPushService
2026-04-21 22:07:29 +08:00
import com.xuqm.sdk.utils.DeviceUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
2026-04-21 22:07:29 +08:00
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicReference
2026-04-21 22:07:29 +08:00
object PushSDK {
private val api: PushApi get() = ApiClient.create(PushApi::class.java, ServiceEndpointRegistry.pushBaseUrl)
private val configApi: PushConfigApi get() = ApiClient.create(PushConfigApi::class.java, ServiceEndpointRegistry.controlBaseUrl)
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val registeredUserId = AtomicReference<String?>(null)
@Volatile
private var cachedVendorConfig: PushVendorConfig? = null
2026-04-21 22:07:29 +08:00
fun currentRegistration(context: Context): PushRegistrationSnapshot? {
2026-04-21 22:07:29 +08:00
XuqmSDK.requireInit()
val deviceId = DeviceUtils.getDeviceId(context)
return store(context).load(deviceId = deviceId, fallbackVendor = detectVendor())
}
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 refreshNativePushToken(
context: Context,
vendor: PushVendor = detectVendor(),
) {
XuqmSDK.requireInit()
ensureNativePushToken(context.applicationContext, vendor)
}
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.appKey,
userId = resolvedUserId,
deviceId = DeviceUtils.getDeviceId(context),
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
}
2026-04-21 22:07:29 +08:00
scope.launch {
runCatching {
api.registerDevice(
appId = XuqmSDK.appKey,
userId = userId,
vendor = vendor.name,
token = pushToken,
deviceId = DeviceUtils.getDeviceId(context),
brand = Build.MANUFACTURER.orEmpty(),
model = Build.MODEL.orEmpty(),
osVersion = DeviceUtils.getOsVersion(),
appVersion = appVersion(context),
)
registeredUserId.set(userId)
store(context).updateLastUserId(userId)
Log.i(
"XuqmPushSDK",
"Registered push device for userId=$userId vendor=${vendor.name}",
)
2026-04-21 22:07:29 +08:00
}
}
}
fun unregisterDevice(userId: String) {
2026-04-21 22:07:29 +08:00
XuqmSDK.requireInit()
scope.launch {
runCatching {
val reg = store(XuqmSDK.appContext).load(
deviceId = DeviceUtils.getDeviceId(XuqmSDK.appContext),
fallbackVendor = detectVendor(),
)
api.unregisterDevice(
XuqmSDK.appKey,
userId,
reg?.vendor?.name ?: detectVendor().name,
DeviceUtils.getDeviceId(XuqmSDK.appContext),
)
registeredUserId.compareAndSet(userId, null)
store(XuqmSDK.appContext).updateLastUserId(null)
}
2026-04-21 22:07:29 +08:00
}
}
private val vendorServices: List<PushVendorInterface> = listOf(
HuaweiPushService(),
XiaomiPushService(),
OppoPushService(),
VivoPushService(),
HonorPushService(),
FcmPushService(),
)
fun detectVendor(): PushVendor {
return when (Build.MANUFACTURER.uppercase()) {
"HUAWEI" -> PushVendor.HUAWEI
"XIAOMI" -> PushVendor.XIAOMI
"REDMI", "POCO" -> PushVendor.XIAOMI
"OPPO" -> PushVendor.OPPO
"REALME", "ONEPLUS" -> PushVendor.OPPO
"VIVO" -> PushVendor.VIVO
"IQOO" -> PushVendor.VIVO
"HONOR" -> PushVendor.HONOR
else -> PushVendor.FCM
}
}
fun initializeVendors(context: Context) {
val detectedVendor = detectVendor()
Log.i("XuqmPushSDK", "Detected push vendor: ${detectedVendor.name}")
scope.launch {
val config = loadVendorConfig()
vendorServices.forEach { service ->
if (service.vendor == detectedVendor && service.isAvailable(context)) {
Log.i("XuqmPushSDK", "Initializing push vendor: ${service.vendor.name}")
service.register(context, config)
}
}
if (detectedVendor == PushVendor.FCM) {
ensureNativePushToken(context)
}
}
}
fun onSdkLogin(session: XuqmLoginSession) {
val context = runCatching { XuqmSDK.appContext }.getOrNull() ?: return
if (registeredUserId.get() == session.userId) return
initializeVendors(context)
bindImUser(context, session.userId)
}
fun onSdkLogout() {
val userId = registeredUserId.getAndSet(null) ?: return
unregisterDevice(userId)
}
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) {
ensureNativePushToken(context, detectVendor())
}
private fun ensureNativePushToken(
context: Context,
vendor: PushVendor,
) {
val registration = currentRegistration(context)
if (registration?.pushToken?.isNotBlank() == true) return
if (vendor == PushVendor.FCM) {
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}")
}
} else {
val service = vendorServices.firstOrNull { it.vendor == vendor }
if (service != null && service.isAvailable(context)) {
scope.launch {
service.register(context, loadVendorConfig())
}
} else {
Log.w(
"XuqmPushSDK",
"Vendor ${vendor.name} service not available, falling back to FCM",
)
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}")
}
}
}
}
private fun appVersion(context: Context): String? =
runCatching {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
"${packageInfo.versionName ?: ""}(${packageInfo.longVersionCode})"
} else {
@Suppress("DEPRECATION")
"${packageInfo.versionName ?: ""}(${packageInfo.versionCode})"
}
}.getOrNull()
private suspend fun loadVendorConfig(): PushVendorConfig {
cachedVendorConfig?.let { return it }
val loaded = runCatching {
val pushConfig = configApi.sdkConfig(XuqmSDK.appKey).data?.pushConfig
PushVendorConfig(
huaweiAppId = pushConfig?.getAsJsonObject("huawei")?.get("appId")?.asString.orEmpty(),
xiaomiAppId = pushConfig?.getAsJsonObject("xiaomi")?.get("appId")?.asString.orEmpty(),
xiaomiAppKey = pushConfig?.getAsJsonObject("xiaomi")?.get("appKey")?.asString.orEmpty(),
oppoAppKey = pushConfig?.getAsJsonObject("oppo")?.get("appKey")?.asString.orEmpty(),
oppoAppSecret = pushConfig?.getAsJsonObject("oppo")?.get("masterSecret")?.asString.orEmpty(),
vivoAppId = pushConfig?.getAsJsonObject("vivo")?.get("appId")?.asString.orEmpty(),
vivoAppKey = pushConfig?.getAsJsonObject("vivo")?.get("appKey")?.asString.orEmpty(),
honorAppId = pushConfig?.getAsJsonObject("honor")?.get("appId")?.asString.orEmpty(),
)
}.getOrElse { error ->
Log.w("XuqmPushSDK", "Unable to load tenant push config: ${error.message}")
PushVendorConfig()
}
cachedVendorConfig = loaded
return loaded
}
2026-04-21 22:07:29 +08:00
}