diff --git a/sample-app/src/androidTest/java/com/xuqm/sdk/sample/PushSdkTest.kt b/sample-app/src/androidTest/java/com/xuqm/sdk/sample/PushSdkTest.kt index 1f57cc9..7978794 100644 --- a/sample-app/src/androidTest/java/com/xuqm/sdk/sample/PushSdkTest.kt +++ b/sample-app/src/androidTest/java/com/xuqm/sdk/sample/PushSdkTest.kt @@ -76,8 +76,11 @@ class PushSdkTest { val expected = when (manufacturer) { "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 } @@ -102,11 +105,17 @@ class PushSdkTest { // 1. initializeVendors 不应抛出任何异常 PushSDK.initializeVendors(appCtx) - // 2. 模拟器上 FCM token 通常为 null - val reg = PushSDK.currentRegistration(appCtx) + // 2. 模拟器上 FCM token 通常为 null;真机只校验厂商映射和流程不崩溃 val vendor = PushSDK.detectVendor() - // 不做 pushToken 非空断言(emulator 无 Firebase),仅验证 vendor 正确 - assertEquals("模拟器应检测到 FCM", PushVendor.FCM, vendor) + val expected = when (Build.MANUFACTURER.uppercase()) { + "HUAWEI" -> PushVendor.HUAWEI + "XIAOMI", "REDMI", "POCO" -> PushVendor.XIAOMI + "OPPO", "REALME", "ONEPLUS" -> PushVendor.OPPO + "VIVO", "IQOO" -> PushVendor.VIVO + "HONOR" -> PushVendor.HONOR + else -> PushVendor.FCM + } + assertEquals("MANUFACTURER=${Build.MANUFACTURER} 应检测到 $expected", expected, vendor) // 3. setReceivePush 调用不应崩溃 PushSDK.setReceivePush(appCtx, USER_A, enabled = false) diff --git a/sdk-push/build.gradle.kts b/sdk-push/build.gradle.kts index 48a7a55..1fc5548 100644 --- a/sdk-push/build.gradle.kts +++ b/sdk-push/build.gradle.kts @@ -11,6 +11,9 @@ android { defaultConfig { minSdk = libs.versions.minSdk.get().toInt() consumerProguardFiles("consumer-rules.pro") + manifestPlaceholders["XUQM_VIVO_APP_ID"] = "" + manifestPlaceholders["XUQM_VIVO_APP_KEY"] = "" + manifestPlaceholders["XUQM_HONOR_APP_ID"] = "" } compileOptions { @@ -25,14 +28,12 @@ android { dependencies { api(project(":sdk-core")) + api(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar")))) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.messaging) - - // Optional vendor push SDKs — add the ones you need in your app module. - // These are NOT declared here because they require proprietary Maven repos: - // Huawei: com.huawei.hms:push (via Huawei Maven repo) - // Xiaomi: com.xiaomi.mipush:mipush (via Xiaomi Maven repo) - // OPPO: com.heytap.mcs:push (via OPPO Maven repo) - // vivo: com.vivo.pushsdk:pushsdk (via vivo Maven repo) - // Honor: com.hihonor.mcs:push (via Honor Maven repo) + api("com.huawei.hms:push:6.12.0.300") + api("com.hihonor.mcs:push:7.0.41.301") + api("io.github.hebeiliang.mipush:Push:2.0.0") + api("com.umeng.umsdk:oppo-push:3.0.0") + api("com.umeng.umsdk:vivo-push:4.0.6.0") } diff --git a/sdk-push/consumer-rules.pro b/sdk-push/consumer-rules.pro index 5ff765d..89d0231 100644 --- a/sdk-push/consumer-rules.pro +++ b/sdk-push/consumer-rules.pro @@ -9,6 +9,10 @@ # ── Firebase Messaging Service — Android resolves it by class name ──────────── -keep class com.xuqm.sdk.push.fcm.XuqmFirebaseMessagingService { *; } +-keep class com.xuqm.sdk.push.huawei.XuqmHuaweiPushService { *; } +-keep class com.xuqm.sdk.push.honor.XuqmHonorPushService { *; } +-keep class com.xuqm.sdk.push.vivo.XuqmVivoPushReceiver { *; } +-keep class com.xuqm.sdk.push.xiaomi.XuqmXiaomiPushReceiver { *; } # ── Vendor push services — instantiated via reflection inside PushSDK ───────── -keep class com.xuqm.sdk.push.vendor.** { *; } diff --git a/sdk-push/libs/README.md b/sdk-push/libs/README.md new file mode 100644 index 0000000..5148f28 --- /dev/null +++ b/sdk-push/libs/README.md @@ -0,0 +1,20 @@ +Vendor push SDK binaries can live here when a vendor only provides a console +download. + +The Xuqm push SDK owns the vendor dependency graph. Huawei, Honor, Xiaomi, +OPPO, and vivo dependencies are declared by `sdk-push/build.gradle.kts`, so app +integrators do not need to add vendor SDKs themselves. + +If a vendor SDK must be pinned to an official console-only binary for a tenant, +put the vendor `*.aar` or `*.jar` here before publishing the Xuqm push SDK. +Those binaries are picked up by `api(fileTree(...))`. + +Xiaomi's official current AAR download page requires a Xiaomi Push console login +session. If an app must use that exact official package, download it from the +tenant's Xiaomi console and publish this SDK with the AAR in this directory. + +`sdk-push` owns the Android manifest integration: permissions, push services, +notification click activity, vendor receivers and metadata placeholders are +declared in `src/main/AndroidManifest.xml`. Host apps should integrate the Xuqm +push SDK; vendor credentials are loaded from the tenant platform when the vendor +SDK supports runtime configuration. diff --git a/sdk-push/src/main/AndroidManifest.xml b/sdk-push/src/main/AndroidManifest.xml index b07524d..b17fd6f 100644 --- a/sdk-push/src/main/AndroidManifest.xml +++ b/sdk-push/src/main/AndroidManifest.xml @@ -1,6 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -8,5 +52,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt index aff63b2..28800ab 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt @@ -9,8 +9,10 @@ import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.core.ServiceEndpointRegistry 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 @@ -29,8 +31,11 @@ import java.util.concurrent.atomic.AtomicReference 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(null) + @Volatile + private var cachedVendorConfig: PushVendorConfig? = null fun currentRegistration(context: Context): PushRegistrationSnapshot? { XuqmSDK.requireInit() @@ -59,6 +64,14 @@ object PushSDK { registerDevice(context, userId) } + fun refreshNativePushToken( + context: Context, + vendor: PushVendor = detectVendor(), + ) { + XuqmSDK.requireInit() + ensureNativePushToken(context.applicationContext, vendor) + } + fun unbindImUser(userId: String) { unregisterDevice(userId) } @@ -77,6 +90,7 @@ object PushSDK { api.setReceivePush( appId = XuqmSDK.appKey, userId = resolvedUserId, + deviceId = DeviceUtils.getDeviceId(context), enabled = enabled, ) if (enabled) { @@ -107,6 +121,11 @@ object PushSDK { 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) @@ -126,7 +145,12 @@ object PushSDK { deviceId = DeviceUtils.getDeviceId(XuqmSDK.appContext), fallbackVendor = detectVendor(), ) - api.unregisterDevice(XuqmSDK.appKey, userId, reg?.vendor?.name ?: detectVendor().name) + api.unregisterDevice( + XuqmSDK.appKey, + userId, + reg?.vendor?.name ?: detectVendor().name, + DeviceUtils.getDeviceId(XuqmSDK.appContext), + ) registeredUserId.compareAndSet(userId, null) store(XuqmSDK.appContext).updateLastUserId(null) } @@ -146,8 +170,11 @@ object PushSDK { 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 } @@ -156,14 +183,17 @@ object PushSDK { fun initializeVendors(context: Context) { val detectedVendor = detectVendor() Log.i("XuqmPushSDK", "Detected push vendor: ${detectedVendor.name}") - vendorServices.forEach { service -> - if (service.vendor == detectedVendor && service.isAvailable(context)) { - Log.i("XuqmPushSDK", "Initializing push vendor: ${service.vendor.name}") - service.register(context) + 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) } - } - if (detectedVendor == PushVendor.FCM) { - ensureNativePushToken(context) } } @@ -189,7 +219,13 @@ object PushSDK { )?.receivePush ?: true private fun ensureNativePushToken(context: Context) { - val vendor = detectVendor() + ensureNativePushToken(context, detectVendor()) + } + + private fun ensureNativePushToken( + context: Context, + vendor: PushVendor, + ) { val registration = currentRegistration(context) if (registration?.pushToken?.isNotBlank() == true) return @@ -216,7 +252,9 @@ object PushSDK { } else { val service = vendorServices.firstOrNull { it.vendor == vendor } if (service != null && service.isAvailable(context)) { - service.register(context) + scope.launch { + service.register(context, loadVendorConfig()) + } } else { Log.w( "XuqmPushSDK", @@ -244,4 +282,37 @@ object PushSDK { } } } + + 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 + } } diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushApi.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushApi.kt index 621be87..08a65c2 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushApi.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushApi.kt @@ -12,6 +12,12 @@ interface PushApi { @Query("userId") userId: String, @Query("vendor") vendor: String, @Query("token") token: String, + @Query("deviceId") deviceId: String, + @Query("brand") brand: String, + @Query("model") model: String, + @Query("osVersion") osVersion: String, + @Query("platform") platform: String = "ANDROID", + @Query("appVersion") appVersion: String? = null, ) @DELETE("api/push/unregister") @@ -19,12 +25,14 @@ interface PushApi { @Query("appId") appId: String, @Query("userId") userId: String, @Query("vendor") vendor: String, + @Query("deviceId") deviceId: String? = null, ) @POST("api/push/receive-push") suspend fun setReceivePush( @Query("appId") appId: String, @Query("userId") userId: String, + @Query("deviceId") deviceId: String? = null, @Query("enabled") enabled: Boolean, ) } diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushConfigApi.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushConfigApi.kt new file mode 100644 index 0000000..66e27b5 --- /dev/null +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushConfigApi.kt @@ -0,0 +1,21 @@ +package com.xuqm.sdk.push.api + +import com.google.gson.JsonObject +import retrofit2.http.GET +import retrofit2.http.Query + +interface PushConfigApi { + @GET("api/sdk/config") + suspend fun sdkConfig( + @Query("appId") appId: String, + @Query("platform") platform: String = "ANDROID", + ): SdkConfigResponse +} + +data class SdkConfigResponse( + val data: SdkConfigData? = null, +) + +data class SdkConfigData( + val pushConfig: JsonObject? = null, +) diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/honor/XuqmHonorPushService.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/honor/XuqmHonorPushService.kt new file mode 100644 index 0000000..e3facc6 --- /dev/null +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/honor/XuqmHonorPushService.kt @@ -0,0 +1,23 @@ +package com.xuqm.sdk.push.honor + +import android.util.Log +import com.hihonor.push.sdk.HonorMessageService +import com.xuqm.sdk.push.PushSDK +import com.xuqm.sdk.push.model.PushVendor + +class XuqmHonorPushService : HonorMessageService() { + + override fun onNewToken(token: String?) { + super.onNewToken(token) + if (token.isNullOrBlank()) return + runCatching { + PushSDK.updateNativePushToken(applicationContext, PushVendor.HONOR, token) + }.onFailure { error -> + Log.w(TAG, "Unable to persist Honor push token: ${error.message}") + } + } + + companion object { + private const val TAG = "XuqmHonorPush" + } +} diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/huawei/XuqmHuaweiPushService.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/huawei/XuqmHuaweiPushService.kt new file mode 100644 index 0000000..8ee35c9 --- /dev/null +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/huawei/XuqmHuaweiPushService.kt @@ -0,0 +1,39 @@ +package com.xuqm.sdk.push.huawei + +import android.os.Bundle +import android.util.Log +import com.huawei.hms.push.HmsMessageService +import com.huawei.hms.push.RemoteMessage +import com.xuqm.sdk.push.PushSDK +import com.xuqm.sdk.push.model.PushVendor + +class XuqmHuaweiPushService : HmsMessageService() { + + override fun onNewToken(token: String?) { + super.onNewToken(token) + persistToken(token) + } + + override fun onNewToken(token: String?, bundle: Bundle?) { + super.onNewToken(token, bundle) + persistToken(token) + } + + override fun onMessageReceived(message: RemoteMessage?) { + super.onMessageReceived(message) + Log.d(TAG, "Huawei push message received") + } + + private fun persistToken(token: String?) { + if (token.isNullOrBlank()) return + runCatching { + PushSDK.updateNativePushToken(applicationContext, PushVendor.HUAWEI, token) + }.onFailure { error -> + Log.w(TAG, "Unable to persist Huawei push token: ${error.message}") + } + } + + companion object { + private const val TAG = "XuqmHuaweiPush" + } +} diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/model/PushVendorConfig.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/model/PushVendorConfig.kt new file mode 100644 index 0000000..dac46dd --- /dev/null +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/model/PushVendorConfig.kt @@ -0,0 +1,12 @@ +package com.xuqm.sdk.push.model + +data class PushVendorConfig( + val huaweiAppId: String = "", + val xiaomiAppId: String = "", + val xiaomiAppKey: String = "", + val oppoAppKey: String = "", + val oppoAppSecret: String = "", + val vivoAppId: String = "", + val vivoAppKey: String = "", + val honorAppId: String = "", +) diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/FcmPushService.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/FcmPushService.kt index d7853af..c8444c6 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/FcmPushService.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/FcmPushService.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.Log import com.xuqm.sdk.push.PushSDK import com.xuqm.sdk.push.model.PushVendor +import com.xuqm.sdk.push.model.PushVendorConfig /** * FCM (Firebase Cloud Messaging) push integration. @@ -23,7 +24,7 @@ class FcmPushService : PushVendorInterface { }.getOrDefault(false) } - override fun register(context: Context) { + override fun register(context: Context, config: PushVendorConfig) { runCatching { val fcmClass = Class.forName("com.google.firebase.messaging.FirebaseMessaging") val instance = fcmClass.getMethod("getInstance").invoke(null) diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HonorPushService.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HonorPushService.kt index 6e0ee84..4a668a0 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HonorPushService.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HonorPushService.kt @@ -4,6 +4,8 @@ import android.content.Context import android.util.Log import com.xuqm.sdk.push.PushSDK import com.xuqm.sdk.push.model.PushVendor +import com.xuqm.sdk.push.model.PushVendorConfig +import java.lang.reflect.Proxy /** * 荣耀推送集成框架 @@ -31,13 +33,21 @@ class HonorPushService : PushVendorInterface { }.getOrDefault(false) } - override fun register(context: Context) { + override fun register(context: Context, config: PushVendorConfig) { runCatching { val honorPushClass = Class.forName("com.hihonor.push.sdk.HonorPushClient") val instance = honorPushClass.getMethod("getInstance").invoke(null) - // HonorPushClient.getInstance().init(context, true) + val supported = runCatching { + honorPushClass.getMethod("checkSupportHonorPush", Context::class.java) + .invoke(instance, context) as? Boolean + }.getOrDefault(true) + if (supported != true) { + Log.w(TAG, "Honor push is not supported on this device") + return + } honorPushClass.getMethod("init", Context::class.java, Boolean::class.javaPrimitiveType) - .invoke(instance, context, true) + .invoke(instance, context, false) + requestToken(context, honorPushClass, instance) Log.i(TAG, "Honor push registration requested") }.onFailure { error -> Log.w(TAG, "Honor push registration failed: ${error.message}") @@ -58,4 +68,36 @@ class HonorPushService : PushVendorInterface { companion object { private const val TAG = "HonorPushService" } + + private fun requestToken( + context: Context, + honorPushClass: Class<*>, + instance: Any, + ) { + runCatching { + val callbackClass = Class.forName("com.hihonor.push.sdk.HonorPushCallback") + val callback = Proxy.newProxyInstance( + callbackClass.classLoader, + arrayOf(callbackClass), + ) { _, method, args -> + when (method.name) { + "onSuccess" -> { + val token = args?.firstOrNull() as? String + if (!token.isNullOrBlank()) { + PushSDK.updateNativePushToken(context, vendor, token) + Log.i(TAG, "Honor push token acquired") + } + } + "onFailure" -> { + Log.w(TAG, "Honor push token failed: ${args?.joinToString()}") + } + } + null + } + honorPushClass.getMethod("getPushToken", callbackClass) + .invoke(instance, callback) + }.onFailure { error -> + Log.w(TAG, "Honor push token request failed: ${error.message}") + } + } } diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HuaweiPushService.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HuaweiPushService.kt index f42bee4..e609e24 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HuaweiPushService.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HuaweiPushService.kt @@ -4,9 +4,10 @@ import android.content.Context import android.util.Log import com.xuqm.sdk.push.PushSDK import com.xuqm.sdk.push.model.PushVendor +import com.xuqm.sdk.push.model.PushVendorConfig /** - * 华为推送集成框架 + * Android 华为 HMS 推送集成框架。 * * 需要添加 HMS Push SDK 依赖: * ```groovy @@ -36,12 +37,12 @@ class HuaweiPushService : PushVendorInterface { }.getOrDefault(false) } - override fun register(context: Context) { + override fun register(context: Context, config: PushVendorConfig) { runCatching { val hmsClass = Class.forName("com.huawei.hms.aaid.HmsInstanceId") val instance = hmsClass.getMethod("getInstance", Context::class.java) .invoke(null, context) - val appId = getAppId(context) + val appId = config.huaweiAppId.ifBlank { getAppId(context) } if (appId.isNotBlank()) { val token = hmsClass.getMethod( "getToken", diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/OppoPushService.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/OppoPushService.kt index 7a00274..aa1e1f4 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/OppoPushService.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/OppoPushService.kt @@ -6,21 +6,11 @@ import android.os.Bundle import android.util.Log import com.xuqm.sdk.push.PushSDK import com.xuqm.sdk.push.model.PushVendor +import com.xuqm.sdk.push.model.PushVendorConfig import java.lang.reflect.Proxy /** - * OPPO 推送集成框架 - * - * 需要添加 OPPO Push SDK 依赖: - * ```groovy - * implementation 'com.heytap.mcs:push:3.x.x' - * ``` - * - * 在 `AndroidManifest.xml` 的 `` 下配置: - * ```xml - * - * - * ``` + * OPPO 推送集成。OPPO Push 依赖由 sdk-push 自身声明,注册参数优先来自租户 PUSH 配置。 */ class OppoPushService : PushVendorInterface { @@ -28,49 +18,26 @@ class OppoPushService : PushVendorInterface { override fun isAvailable(context: Context): Boolean { return runCatching { - Class.forName("com.heytap.mcssdk.PushManager") + resolvePushManagerClass() true }.getOrDefault(false) } - override fun register(context: Context) { + override fun register(context: Context, config: PushVendorConfig) { runCatching { - val pushManagerClass = Class.forName("com.heytap.mcssdk.PushManager") - val instance = pushManagerClass.getMethod("getInstance").invoke(null) - val meta = context.packageManager.getApplicationInfo( context.packageName, PackageManager.GET_META_DATA ).metaData ?: Bundle.EMPTY - val appKey = meta.getString("XUQM_OPPO_APP_KEY", "") - val appSecret = meta.getString("XUQM_OPPO_APP_SECRET", "") + val appKey = config.oppoAppKey.ifBlank { meta.getString("XUQM_OPPO_APP_KEY", "") } + val appSecret = config.oppoAppSecret.ifBlank { meta.getString("XUQM_OPPO_APP_SECRET", "") } if (appKey.isNotBlank() && appSecret.isNotBlank()) { - val callbackClass = Class.forName("com.heytap.mcssdk.callback.PushCallback") - val proxy = Proxy.newProxyInstance( - callbackClass.classLoader, - arrayOf(callbackClass) - ) { _, method, args -> - when (method.name) { - "onRegister" -> { - val regId = args?.getOrNull(1) as? String - if (!regId.isNullOrBlank()) { - PushSDK.updateNativePushToken(context, vendor, regId) - Log.i(TAG, "OPPO push token acquired") - } - } - } - null + if (!registerWithHeytapMsp(context, appKey, appSecret)) { + registerWithLegacyMcs(context, appKey, appSecret) } - pushManagerClass.getMethod( - "register", - Context::class.java, - String::class.java, - String::class.java, - callbackClass, - ).invoke(instance, context, appKey, appSecret, proxy) Log.i(TAG, "OPPO push registration requested") } else { - Log.w(TAG, "OPPO appKey/appSecret not configured in meta-data, skipping registration") + Log.w(TAG, "OPPO appKey/appSecret not configured, skipping registration") } }.onFailure { error -> Log.w(TAG, "OPPO push registration failed: ${error.message}") @@ -79,10 +46,13 @@ class OppoPushService : PushVendorInterface { override fun unregister(context: Context) { runCatching { - val pushManagerClass = Class.forName("com.heytap.mcssdk.PushManager") - val instance = pushManagerClass.getMethod("getInstance").invoke(null) - pushManagerClass.getMethod("unRegister", Context::class.java) - .invoke(instance, context) + val pushManagerClass = resolvePushManagerClass() + if (pushManagerClass.name == "com.heytap.msp.push.HeytapPushManager") { + pushManagerClass.getMethod("unRegister").invoke(null) + } else { + val instance = pushManagerClass.getMethod("getInstance").invoke(null) + pushManagerClass.getMethod("unRegister", Context::class.java).invoke(instance, context) + } Log.i(TAG, "OPPO push unregistered") }.onFailure { error -> Log.w(TAG, "OPPO push unregistration failed: ${error.message}") @@ -91,5 +61,90 @@ class OppoPushService : PushVendorInterface { companion object { private const val TAG = "OppoPushService" + + private fun resolvePushManagerClass(): Class<*> { + return runCatching { + Class.forName("com.heytap.msp.push.HeytapPushManager") + }.getOrElse { + Class.forName("com.heytap.mcssdk.PushManager") + } + } + } + + private fun registerWithHeytapMsp( + context: Context, + appKey: String, + appSecret: String, + ): Boolean { + val pushManagerClass = runCatching { + Class.forName("com.heytap.msp.push.HeytapPushManager") + }.getOrNull() ?: return false + + runCatching { + pushManagerClass.getMethod("init", Context::class.java, java.lang.Boolean.TYPE) + .invoke(null, context, false) + } + + val supported = runCatching { + pushManagerClass.getMethod("isSupportPush", Context::class.java) + .invoke(null, context) as? Boolean + }.getOrElse { + runCatching { pushManagerClass.getMethod("isSupportPush").invoke(null) as? Boolean } + .getOrDefault(true) + } ?: true + + if (!supported) { + Log.w(TAG, "OPPO push is not supported on this device") + return true + } + + val callbackClass = Class.forName("com.heytap.msp.push.callback.ICallBackResultService") + val callback = buildCallback(context.applicationContext, callbackClass) + pushManagerClass.getMethod( + "register", + Context::class.java, + String::class.java, + String::class.java, + callbackClass, + ).invoke(null, context, appKey, appSecret, callback) + runCatching { pushManagerClass.getMethod("requestNotificationPermission").invoke(null) } + return true + } + + private fun registerWithLegacyMcs( + context: Context, + appKey: String, + appSecret: String, + ) { + val pushManagerClass = Class.forName("com.heytap.mcssdk.PushManager") + val instance = pushManagerClass.getMethod("getInstance").invoke(null) + val callbackClass = Class.forName("com.heytap.mcssdk.callback.PushCallback") + val callback = buildCallback(context.applicationContext, callbackClass) + pushManagerClass.getMethod( + "register", + Context::class.java, + String::class.java, + String::class.java, + callbackClass, + ).invoke(instance, context, appKey, appSecret, callback) + } + + private fun buildCallback(context: Context, callbackClass: Class<*>): Any { + return Proxy.newProxyInstance( + callbackClass.classLoader, + arrayOf(callbackClass) + ) { _, method, args -> + if (method.name == "onRegister") { + val code = args?.getOrNull(0) as? Int + val regId = args?.getOrNull(1) as? String + if ((code == null || code == 0) && !regId.isNullOrBlank()) { + PushSDK.updateNativePushToken(context, vendor, regId) + Log.i(TAG, "OPPO push token acquired") + } else { + Log.w(TAG, "OPPO push register callback code=$code") + } + } + null + } } } diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/PushVendorInterface.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/PushVendorInterface.kt index 6b32f8c..3bb56a9 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/PushVendorInterface.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/PushVendorInterface.kt @@ -2,6 +2,7 @@ package com.xuqm.sdk.push.vendor import android.content.Context import com.xuqm.sdk.push.model.PushVendor +import com.xuqm.sdk.push.model.PushVendorConfig /** * 厂商推送服务接口 @@ -22,7 +23,7 @@ interface PushVendorInterface { /** * 注册厂商推送服务,触发获取推送 Token 的流程。 */ - fun register(context: Context) + fun register(context: Context, config: PushVendorConfig = PushVendorConfig()) /** * 注销厂商推送服务。 diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/VivoPushService.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/VivoPushService.kt index 11348e6..e15d7b7 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/VivoPushService.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/VivoPushService.kt @@ -4,6 +4,8 @@ import android.content.Context import android.util.Log import com.xuqm.sdk.push.PushSDK import com.xuqm.sdk.push.model.PushVendor +import com.xuqm.sdk.push.model.PushVendorConfig +import java.lang.reflect.Proxy /** * vivo 推送集成框架 @@ -31,13 +33,33 @@ class VivoPushService : PushVendorInterface { }.getOrDefault(false) } - override fun register(context: Context) { + override fun register(context: Context, config: PushVendorConfig) { runCatching { val pushClientClass = Class.forName("com.vivo.push.PushClient") val instance = pushClientClass.getMethod("getInstance", Context::class.java) .invoke(null, context) - // PushClient.getInstance(context).initialize() pushClientClass.getMethod("initialize").invoke(instance) + val listenerClass = Class.forName("com.vivo.push.IPushActionListener") + val listener = Proxy.newProxyInstance( + listenerClass.classLoader, + arrayOf(listenerClass), + ) { _, method, args -> + if (method.name == "onStateChanged") { + val state = args?.firstOrNull() as? Int + if (state == 0) { + persistRegId(context, pushClientClass, instance) + } else { + Log.w(TAG, "Vivo push turnOnPush state=$state") + } + } + null + } + runCatching { + pushClientClass.getMethod("turnOnPush", listenerClass) + .invoke(instance, listener) + }.onFailure { + persistRegId(context, pushClientClass, instance) + } Log.i(TAG, "Vivo push registration requested") }.onFailure { error -> Log.w(TAG, "Vivo push registration failed: ${error.message}") @@ -59,4 +81,18 @@ class VivoPushService : PushVendorInterface { companion object { private const val TAG = "VivoPushService" } + + private fun persistRegId( + context: Context, + pushClientClass: Class<*>, + instance: Any, + ) { + val regId = runCatching { + pushClientClass.getMethod("getRegId").invoke(instance) as? String + }.getOrNull() + if (!regId.isNullOrBlank()) { + PushSDK.updateNativePushToken(context, vendor, regId) + Log.i(TAG, "Vivo push token acquired") + } + } } diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/XiaomiPushService.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/XiaomiPushService.kt index 389199a..fc3ad93 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/XiaomiPushService.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/XiaomiPushService.kt @@ -6,26 +6,13 @@ import android.os.Bundle import android.util.Log import com.xuqm.sdk.push.PushSDK import com.xuqm.sdk.push.model.PushVendor +import com.xuqm.sdk.push.model.PushVendorConfig /** * 小米推送集成框架 * - * 需要添加 MiPush SDK 依赖: - * ```groovy - * implementation 'com.xiaomi.mipush:mipush:5.x.x' - * ``` - * - * 在 `AndroidManifest.xml` 的 `` 下配置: - * ```xml - * - * - * ``` - * - * 并在自定义 `PushMessageReceiver` 的 `onReceiveRegisterResult()` 回调中获取 token, - * 然后调用: - * ```kotlin - * PushSDK.updateNativePushToken(context, PushVendor.XIAOMI, token) - * ``` + * MiPush AAR 需要随 sdk-push 发布。注册参数优先来自租户 PUSH 配置, + * 旧版本 manifest meta-data 仅作为兼容兜底。 */ class XiaomiPushService : PushVendorInterface { @@ -38,14 +25,14 @@ class XiaomiPushService : PushVendorInterface { }.getOrDefault(false) } - override fun register(context: Context) { + override fun register(context: Context, config: PushVendorConfig) { runCatching { val miPushClass = Class.forName("com.xiaomi.mipush.sdk.MiPushClient") val meta = context.packageManager.getApplicationInfo( context.packageName, PackageManager.GET_META_DATA ).metaData ?: Bundle.EMPTY - val appId = meta.getString("XUQM_XIAOMI_APP_ID", "") - val appKey = meta.getString("XUQM_XIAOMI_APP_KEY", "") + val appId = config.xiaomiAppId.ifBlank { meta.getString("XUQM_XIAOMI_APP_ID", "") } + val appKey = config.xiaomiAppKey.ifBlank { meta.getString("XUQM_XIAOMI_APP_KEY", "") } if (appId.isNotBlank() && appKey.isNotBlank()) { miPushClass.getMethod( "registerPush", @@ -54,8 +41,9 @@ class XiaomiPushService : PushVendorInterface { String::class.java, ).invoke(null, context, appId, appKey) Log.i(TAG, "Xiaomi push registration requested") + pollRegId(context, miPushClass) } else { - Log.w(TAG, "Xiaomi appId/appKey not configured in meta-data, skipping registration") + Log.w(TAG, "Xiaomi appId/appKey not configured, skipping registration") } }.onFailure { error -> Log.w(TAG, "Xiaomi push registration failed: ${error.message}") @@ -73,6 +61,24 @@ class XiaomiPushService : PushVendorInterface { } } + private fun pollRegId(context: Context, miPushClass: Class<*>) { + Thread { + repeat(8) { + val regId = runCatching { + miPushClass.getMethod("getRegId", Context::class.java) + .invoke(null, context) as? String + }.getOrNull() + if (!regId.isNullOrBlank()) { + PushSDK.updateNativePushToken(context, vendor, regId) + Log.i(TAG, "Xiaomi push token acquired") + return@Thread + } + Thread.sleep(1000) + } + Log.w(TAG, "Xiaomi push token not ready after registration") + }.start() + } + companion object { private const val TAG = "XiaomiPushService" } diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vivo/XuqmVivoPushReceiver.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vivo/XuqmVivoPushReceiver.kt new file mode 100644 index 0000000..c214d71 --- /dev/null +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vivo/XuqmVivoPushReceiver.kt @@ -0,0 +1,47 @@ +package com.xuqm.sdk.push.vivo + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import com.xuqm.sdk.push.PushSDK +import com.xuqm.sdk.push.model.PushVendor + +class XuqmVivoPushReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent?) { + if (intent == null) return + val appContext = context.applicationContext + val regId = extractRegId(intent) ?: currentRegId(appContext) + if (regId.isNullOrBlank()) return + runCatching { + PushSDK.updateNativePushToken(appContext, PushVendor.VIVO, regId) + }.onFailure { error -> + Log.w(TAG, "Unable to persist vivo push token: ${error.message}") + } + } + + private fun extractRegId(intent: Intent): String? { + val extras = intent.extras ?: return null + for (key in extras.keySet()) { + val value = extras.get(key) as? String ?: continue + if (key.contains("reg", ignoreCase = true) || key.contains("token", ignoreCase = true)) { + return value.takeIf { it.isNotBlank() } + } + } + return null + } + + private fun currentRegId(context: Context): String? { + return runCatching { + val pushClientClass = Class.forName("com.vivo.push.PushClient") + val instance = pushClientClass.getMethod("getInstance", Context::class.java) + .invoke(null, context) + pushClientClass.getMethod("getRegId").invoke(instance) as? String + }.getOrNull() + } + + companion object { + private const val TAG = "XuqmVivoPush" + } +} diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/xiaomi/XuqmXiaomiPushReceiver.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/xiaomi/XuqmXiaomiPushReceiver.kt new file mode 100644 index 0000000..8c3c05f --- /dev/null +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/xiaomi/XuqmXiaomiPushReceiver.kt @@ -0,0 +1,60 @@ +package com.xuqm.sdk.push.xiaomi + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import com.xuqm.sdk.push.PushSDK +import com.xuqm.sdk.push.model.PushVendor + +class XuqmXiaomiPushReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent?) { + if (intent == null) return + val appContext = context.applicationContext + val regId = extractRegId(intent) + if (!regId.isNullOrBlank()) { + runCatching { + PushSDK.updateNativePushToken(appContext, PushVendor.XIAOMI, regId) + }.onFailure { error -> + Log.w(TAG, "Unable to persist Xiaomi push token: ${error.message}") + } + return + } + runCatching { + PushSDK.refreshNativePushToken(appContext, PushVendor.XIAOMI) + }.onFailure { error -> + Log.d(TAG, "Xiaomi push callback ignored before SDK init: ${error.message}") + } + } + + private fun extractRegId(intent: Intent): String? { + val extras = intent.extras ?: return null + for (key in extras.keySet()) { + val value = extras.get(key) ?: continue + if (value is String && key.contains("reg", ignoreCase = true)) { + return value.takeIf { it.isNotBlank() } + } + if (value.javaClass.name == "com.xiaomi.mipush.sdk.MiPushCommandMessage") { + val resultCode = runCatching { + value.javaClass.getMethod("getResultCode").invoke(value) as? Long + }.getOrNull() + val command = runCatching { + value.javaClass.getMethod("getCommand").invoke(value) as? String + }.getOrNull() + val arguments = runCatching { + @Suppress("UNCHECKED_CAST") + value.javaClass.getMethod("getCommandArguments").invoke(value) as? List + }.getOrNull() + if ((resultCode == null || resultCode == 0L) && command.equals("register", ignoreCase = true)) { + return arguments?.firstOrNull { it.isNotBlank() } + } + } + } + return null + } + + companion object { + private const val TAG = "XuqmMiPushReceiver" + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b28fa77..c469d64 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,7 @@ pluginManagement { repositories { maven(url = "https://nexus.xuqinmin.com/repository/android/") + maven(url = "https://developer.hihonor.com/repo") google() mavenCentral() gradlePluginPortal() @@ -11,6 +12,7 @@ dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven(url = "https://nexus.xuqinmin.com/repository/android/") + maven(url = "https://developer.hihonor.com/repo") google() mavenCentral() }