Add domestic vendor push infrastructure
This commit is contained in:
@@ -30,12 +30,18 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="image/*"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<data android:mimeType="image/*"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="miaoji" android:host="push" android:pathPrefix="/open"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ProjectionConsentActivity"
|
||||
|
||||
@@ -63,7 +63,8 @@ class MainActivity : FlutterActivity() {
|
||||
private var pendingProgressCount: Int? = null
|
||||
private var pendingRecognitionAction: Map<String, Any?>? = null
|
||||
private var recognitionReceiverRegistered = false
|
||||
private var updateInstallBridge: UpdateInstallBridge? = null
|
||||
private var updateInstallBridge: UpdateInstallBridge? = null
|
||||
private var vendorPushBridge: VendorPushBridge? = null
|
||||
|
||||
private var pendingSpeechResult: MethodChannel.Result? = null
|
||||
private var speechRecognizer: SpeechRecognizer? = null
|
||||
@@ -71,7 +72,11 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
updateInstallBridge = UpdateInstallBridge(this).also { it.register(flutterEngine) }
|
||||
updateInstallBridge = UpdateInstallBridge(this).also { it.register(flutterEngine) }
|
||||
vendorPushBridge = VendorPushBridge(this).also {
|
||||
it.register(flutterEngine)
|
||||
it.handleIntent(intent)
|
||||
}
|
||||
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
||||
channel?.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
@@ -202,13 +207,15 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
handleIncomingIntent(intent)
|
||||
setIntent(intent)
|
||||
vendorPushBridge?.handleIntent(intent)
|
||||
handleIncomingIntent(intent)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateInstallBridge?.onResume()
|
||||
updateInstallBridge?.onResume()
|
||||
vendorPushBridge?.onResume()
|
||||
scheduleShortcutIfNeeded()
|
||||
dispatchPendingScreenshot()
|
||||
dispatchPendingRecognitionAction()
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
package com.nx.miaoji
|
||||
|
||||
import android.Manifest
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import org.json.JSONObject
|
||||
import java.lang.reflect.Proxy
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class VendorPushBridge(private val activity: MainActivity) : MethodChannel.MethodCallHandler {
|
||||
companion object {
|
||||
private const val CHANNEL = "com.miaoji/push"
|
||||
private const val PREFS = "jizhi_vendor_push"
|
||||
private const val KEY_ENABLED = "enabled"
|
||||
private const val KEY_TOKEN = "token"
|
||||
private const val KEY_PROVIDER = "provider"
|
||||
private const val KEY_PENDING = "pending_open"
|
||||
}
|
||||
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
private val preferences = activity.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
private var channel: MethodChannel? = null
|
||||
|
||||
fun register(engine: FlutterEngine) {
|
||||
channel = MethodChannel(engine.dartExecutor.binaryMessenger, CHANNEL).also {
|
||||
it.setMethodCallHandler(this)
|
||||
}
|
||||
dispatchPendingOpen()
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"getStatus" -> result.success(status())
|
||||
"enable" -> resolveToken(result)
|
||||
"refreshToken" -> resolveToken(result)
|
||||
"disable" -> {
|
||||
disableProvider()
|
||||
result.success(status())
|
||||
}
|
||||
"openNotificationSettings" -> {
|
||||
openNotificationSettings()
|
||||
result.success(true)
|
||||
}
|
||||
"getPendingOpen" -> result.success(readPendingOpen())
|
||||
"acknowledgeOpen" -> {
|
||||
val messageId = call.argument<String>("messageId")
|
||||
val pending = readPendingOpen()
|
||||
if (messageId != null && pending?.get("messageId") == messageId) {
|
||||
preferences.edit().remove(KEY_PENDING).apply()
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
dispatchPendingOpen()
|
||||
if (preferences.getBoolean(KEY_ENABLED, false) && cachedToken().isNullOrBlank()) {
|
||||
resolveToken(null)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleIntent(intent: Intent?) {
|
||||
val payload = parseOpen(intent) ?: return
|
||||
val messageId = payload["messageId"]?.toString().orEmpty()
|
||||
if (messageId.isBlank()) return
|
||||
preferences.edit().putString(KEY_PENDING, JSONObject(payload).toString()).apply()
|
||||
dispatchPendingOpen()
|
||||
}
|
||||
|
||||
private fun resolveToken(result: MethodChannel.Result?) {
|
||||
createNotificationChannels()
|
||||
if (!notificationsAllowed()) {
|
||||
result?.success(status(error = "notification_permission_denied"))
|
||||
return
|
||||
}
|
||||
val provider = detectProvider()
|
||||
if (provider == null) {
|
||||
result?.success(status(error = "unsupported_vendor"))
|
||||
return
|
||||
}
|
||||
if (!sdkAvailable(provider)) {
|
||||
result?.success(status(error = "sdk_not_installed"))
|
||||
return
|
||||
}
|
||||
preferences.edit().putBoolean(KEY_ENABLED, true).putString(KEY_PROVIDER, provider).apply()
|
||||
executor.execute {
|
||||
val token = runCatching { registerAndReadToken(provider) }.getOrNull()
|
||||
if (!token.isNullOrBlank()) {
|
||||
preferences.edit().putString(KEY_TOKEN, token).putString(KEY_PROVIDER, provider).apply()
|
||||
activity.runOnUiThread {
|
||||
channel?.invokeMethod("onToken", mapOf("provider" to provider, "token" to token))
|
||||
}
|
||||
}
|
||||
activity.runOnUiThread {
|
||||
result?.success(status(error = if (token.isNullOrBlank()) "token_pending" else null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerAndReadToken(provider: String): String? = when (provider) {
|
||||
"huawei" -> huaweiToken()
|
||||
"honor" -> honorToken()
|
||||
"xiaomi" -> xiaomiToken()
|
||||
"oppo" -> oppoToken()
|
||||
"vivo" -> vivoToken()
|
||||
"meizu" -> meizuToken()
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun huaweiToken(): String? {
|
||||
val appId = config("PUSH_HUAWEI_APP_ID")
|
||||
if (appId.isBlank()) return null
|
||||
val type = Class.forName("com.huawei.hms.aaid.HmsInstanceId")
|
||||
val instance = type.getMethod("getInstance", Context::class.java).invoke(null, activity)
|
||||
return type.getMethod("getToken", String::class.java, String::class.java)
|
||||
.invoke(instance, appId, "HCM") as? String
|
||||
}
|
||||
|
||||
private fun honorToken(): String? {
|
||||
val appId = config("PUSH_HONOR_APP_ID")
|
||||
if (appId.isBlank()) return null
|
||||
val type = firstClass(
|
||||
"com.hihonor.push.sdk.HonorPushClient",
|
||||
"com.hihonor.mcs.push.HonorPushClient",
|
||||
) ?: return null
|
||||
val instance = invokeMatching(type, null, "getInstance", activity)
|
||||
?: invokeMatching(type, null, "getInstance")
|
||||
?: return null
|
||||
val value = invokeMatching(type, instance, "getPushToken")
|
||||
?: invokeMatching(type, instance, "getPushToken", appId)
|
||||
return awaitTaskValue(value)
|
||||
}
|
||||
|
||||
private fun xiaomiToken(): String? {
|
||||
val appId = config("PUSH_XIAOMI_APP_ID")
|
||||
val appKey = config("PUSH_XIAOMI_APP_KEY")
|
||||
if (appId.isBlank() || appKey.isBlank()) return null
|
||||
val type = Class.forName("com.xiaomi.mipush.sdk.MiPushClient")
|
||||
invokeMatching(type, null, "registerPush", activity, appId, appKey)
|
||||
repeat(10) {
|
||||
val token = invokeMatching(type, null, "getRegId", activity) as? String
|
||||
if (!token.isNullOrBlank()) return token
|
||||
Thread.sleep(300)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun oppoToken(): String? {
|
||||
val appKey = config("PUSH_OPPO_APP_KEY")
|
||||
val appSecret = config("PUSH_OPPO_APP_SECRET")
|
||||
if (appKey.isBlank() || appSecret.isBlank()) return null
|
||||
val type = Class.forName("com.heytap.msp.push.HeytapPushManager")
|
||||
invokeMatching(type, null, "init", activity.applicationContext, true)
|
||||
val callbackType = firstClass("com.heytap.msp.push.callback.ICallBackResultService")
|
||||
val callback = callbackType?.let { dynamicCallback(it) }
|
||||
if (callback != null) invokeMatching(type, null, "register", activity, appKey, appSecret, callback)
|
||||
repeat(10) {
|
||||
val token = invokeMatching(type, null, "getRegisterID") as? String
|
||||
if (!token.isNullOrBlank()) return token
|
||||
Thread.sleep(300)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun vivoToken(): String? {
|
||||
val appId = config("PUSH_VIVO_APP_ID")
|
||||
val appKey = config("PUSH_VIVO_APP_KEY")
|
||||
if (appId.isBlank() || appKey.isBlank()) return null
|
||||
val type = Class.forName("com.vivo.push.PushClient")
|
||||
val instance = invokeMatching(type, null, "getInstance", activity.applicationContext) ?: return null
|
||||
invokeMatching(type, instance, "initialize")
|
||||
val callbackType = firstClass("com.vivo.push.IPushActionListener")
|
||||
callbackType?.let { invokeMatching(type, instance, "turnOnPush", dynamicCallback(it)) }
|
||||
repeat(10) {
|
||||
val token = invokeMatching(type, instance, "getRegId") as? String
|
||||
if (!token.isNullOrBlank()) return token
|
||||
Thread.sleep(300)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun meizuToken(): String? {
|
||||
val appId = config("PUSH_MEIZU_APP_ID")
|
||||
val appKey = config("PUSH_MEIZU_APP_KEY")
|
||||
if (appId.isBlank() || appKey.isBlank()) return null
|
||||
val type = Class.forName("com.meizu.cloud.pushsdk.PushManager")
|
||||
invokeMatching(type, null, "register", activity.applicationContext, appId, appKey)
|
||||
repeat(10) {
|
||||
val token = invokeMatching(type, null, "getPushId", activity.applicationContext) as? String
|
||||
if (!token.isNullOrBlank()) return token
|
||||
Thread.sleep(300)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun disableProvider() {
|
||||
val provider = preferences.getString(KEY_PROVIDER, null)
|
||||
runCatching {
|
||||
when (provider) {
|
||||
"xiaomi" -> invokeMatching(
|
||||
Class.forName("com.xiaomi.mipush.sdk.MiPushClient"),
|
||||
null,
|
||||
"unregisterPush",
|
||||
activity,
|
||||
)
|
||||
"oppo" -> invokeMatching(
|
||||
Class.forName("com.heytap.msp.push.HeytapPushManager"),
|
||||
null,
|
||||
"unRegister",
|
||||
)
|
||||
"meizu" -> invokeMatching(
|
||||
Class.forName("com.meizu.cloud.pushsdk.PushManager"),
|
||||
null,
|
||||
"unRegister",
|
||||
activity.applicationContext,
|
||||
config("PUSH_MEIZU_APP_ID"),
|
||||
config("PUSH_MEIZU_APP_KEY"),
|
||||
)
|
||||
}
|
||||
}
|
||||
preferences.edit().putBoolean(KEY_ENABLED, false).remove(KEY_TOKEN).apply()
|
||||
}
|
||||
|
||||
private fun status(error: String? = null): Map<String, Any?> {
|
||||
val provider = detectProvider()
|
||||
return mapOf(
|
||||
"provider" to provider,
|
||||
"supported" to (provider != null),
|
||||
"sdkAvailable" to (provider?.let(::sdkAvailable) == true),
|
||||
"notificationsAllowed" to notificationsAllowed(),
|
||||
"enabled" to preferences.getBoolean(KEY_ENABLED, false),
|
||||
"token" to cachedToken(),
|
||||
"error" to error,
|
||||
)
|
||||
}
|
||||
|
||||
private fun detectProvider(): String? {
|
||||
val value = "${Build.MANUFACTURER} ${Build.BRAND}".lowercase(Locale.ROOT)
|
||||
return when {
|
||||
value.contains("honor") -> "honor"
|
||||
value.contains("huawei") -> "huawei"
|
||||
value.contains("xiaomi") || value.contains("redmi") || value.contains("poco") -> "xiaomi"
|
||||
value.contains("oppo") || value.contains("realme") || value.contains("oneplus") -> "oppo"
|
||||
value.contains("vivo") || value.contains("iqoo") -> "vivo"
|
||||
value.contains("meizu") -> "meizu"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun sdkAvailable(provider: String): Boolean = when (provider) {
|
||||
"huawei" -> firstClass("com.huawei.hms.aaid.HmsInstanceId") != null
|
||||
"honor" -> firstClass("com.hihonor.push.sdk.HonorPushClient", "com.hihonor.mcs.push.HonorPushClient") != null
|
||||
"xiaomi" -> firstClass("com.xiaomi.mipush.sdk.MiPushClient") != null
|
||||
"oppo" -> firstClass("com.heytap.msp.push.HeytapPushManager") != null
|
||||
"vivo" -> firstClass("com.vivo.push.PushClient") != null
|
||||
"meizu" -> firstClass("com.meizu.cloud.pushsdk.PushManager") != null
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun createNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
val manager = activity.getSystemService(NotificationManager::class.java)
|
||||
manager.createNotificationChannels(
|
||||
listOf(
|
||||
NotificationChannel("jizhi_system", "系统通知", NotificationManager.IMPORTANCE_DEFAULT),
|
||||
NotificationChannel("jizhi_budget", "预算提醒", NotificationManager.IMPORTANCE_HIGH),
|
||||
NotificationChannel("jizhi_operations", "运营通知", NotificationManager.IMPORTANCE_DEFAULT),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun notificationsAllowed(): Boolean =
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
||||
ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
|
||||
private fun openNotificationSettings() {
|
||||
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
||||
putExtra(Settings.EXTRA_APP_PACKAGE, activity.packageName)
|
||||
}
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun parseOpen(intent: Intent?): Map<String, Any?>? {
|
||||
if (intent == null) return null
|
||||
val data = intent.data
|
||||
if (data?.scheme == "miaoji" && data.host == "push") {
|
||||
return mapOf(
|
||||
"messageId" to data.getQueryParameter("messageId"),
|
||||
"category" to data.getQueryParameter("category"),
|
||||
"action" to data.getQueryParameter("action"),
|
||||
"entityId" to data.getQueryParameter("entityId"),
|
||||
)
|
||||
}
|
||||
val raw = intent.getStringExtra("jz_payload")
|
||||
?: intent.getStringExtra("action_parameters")
|
||||
?: return null
|
||||
return runCatching {
|
||||
val json = JSONObject(raw)
|
||||
mapOf(
|
||||
"messageId" to json.optString("messageId"),
|
||||
"category" to json.optString("category"),
|
||||
"action" to json.optString("action"),
|
||||
"entityId" to json.optString("entityId").ifBlank { null },
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun readPendingOpen(): Map<String, Any?>? {
|
||||
val raw = preferences.getString(KEY_PENDING, null) ?: return null
|
||||
return runCatching {
|
||||
val json = JSONObject(raw)
|
||||
mapOf(
|
||||
"messageId" to json.optString("messageId"),
|
||||
"category" to json.optString("category"),
|
||||
"action" to json.optString("action"),
|
||||
"entityId" to json.optString("entityId").ifBlank { null },
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun dispatchPendingOpen() {
|
||||
readPendingOpen()?.let { channel?.invokeMethod("onPushOpened", it) }
|
||||
}
|
||||
|
||||
private fun cachedToken(): String? = preferences.getString(KEY_TOKEN, null)
|
||||
|
||||
private fun config(name: String): String = when (name) {
|
||||
"PUSH_HUAWEI_APP_ID" -> BuildConfig.PUSH_HUAWEI_APP_ID
|
||||
"PUSH_HONOR_APP_ID" -> BuildConfig.PUSH_HONOR_APP_ID
|
||||
"PUSH_XIAOMI_APP_ID" -> BuildConfig.PUSH_XIAOMI_APP_ID
|
||||
"PUSH_XIAOMI_APP_KEY" -> BuildConfig.PUSH_XIAOMI_APP_KEY
|
||||
"PUSH_OPPO_APP_KEY" -> BuildConfig.PUSH_OPPO_APP_KEY
|
||||
"PUSH_OPPO_APP_SECRET" -> BuildConfig.PUSH_OPPO_APP_SECRET
|
||||
"PUSH_VIVO_APP_ID" -> BuildConfig.PUSH_VIVO_APP_ID
|
||||
"PUSH_VIVO_APP_KEY" -> BuildConfig.PUSH_VIVO_APP_KEY
|
||||
"PUSH_MEIZU_APP_ID" -> BuildConfig.PUSH_MEIZU_APP_ID
|
||||
"PUSH_MEIZU_APP_KEY" -> BuildConfig.PUSH_MEIZU_APP_KEY
|
||||
else -> ""
|
||||
}
|
||||
|
||||
private fun firstClass(vararg names: String): Class<*>? =
|
||||
names.firstNotNullOfOrNull { name -> runCatching { Class.forName(name) }.getOrNull() }
|
||||
|
||||
private fun invokeMatching(type: Class<*>, target: Any?, name: String, vararg args: Any?): Any? {
|
||||
val method = type.methods.firstOrNull { candidate ->
|
||||
candidate.name == name &&
|
||||
candidate.parameterTypes.size == args.size &&
|
||||
candidate.parameterTypes.indices.all { index ->
|
||||
val argument = args[index]
|
||||
argument == null || boxed(candidate.parameterTypes[index]).isInstance(argument)
|
||||
}
|
||||
} ?: return null
|
||||
return method.invoke(target, *args)
|
||||
}
|
||||
|
||||
private fun boxed(type: Class<*>): Class<*> = when (type) {
|
||||
java.lang.Boolean.TYPE -> java.lang.Boolean::class.java
|
||||
java.lang.Byte.TYPE -> java.lang.Byte::class.java
|
||||
java.lang.Character.TYPE -> java.lang.Character::class.java
|
||||
java.lang.Double.TYPE -> java.lang.Double::class.java
|
||||
java.lang.Float.TYPE -> java.lang.Float::class.java
|
||||
java.lang.Integer.TYPE -> java.lang.Integer::class.java
|
||||
java.lang.Long.TYPE -> java.lang.Long::class.java
|
||||
java.lang.Short.TYPE -> java.lang.Short::class.java
|
||||
else -> type
|
||||
}
|
||||
|
||||
private fun dynamicCallback(type: Class<*>): Any = Proxy.newProxyInstance(
|
||||
type.classLoader,
|
||||
arrayOf(type),
|
||||
) { _, method, args ->
|
||||
if (method.name.contains("register", ignoreCase = true)) {
|
||||
val token = args?.firstOrNull { it is String && it.isNotBlank() } as? String
|
||||
if (!token.isNullOrBlank()) preferences.edit().putString(KEY_TOKEN, token).apply()
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
private fun awaitTaskValue(value: Any?): String? {
|
||||
if (value is String) return value
|
||||
if (value == null) return null
|
||||
repeat(20) {
|
||||
val complete = runCatching {
|
||||
value.javaClass.getMethod("isComplete").invoke(value) as? Boolean
|
||||
}.getOrNull()
|
||||
if (complete == true) {
|
||||
return runCatching { value.javaClass.getMethod("getResult").invoke(value) as? String }.getOrNull()
|
||||
}
|
||||
Thread.sleep(200)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user