Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
@file:Suppress("unused", "SpellCheckingInspection")

package org.akanework.gramophone.logic.utils.exoplayer.oem

/**
* Media Kit
* Copyright (C) 2025 Moriafly
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/



import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager

/**
* 小米妙播适配
*/
@UnstableMediaKitApi
object MiPlayAudioSupport {
private const val ACTION_MIPLAY_DETAIL = "miui.intent.action.ACTIVITY_MIPLAY_DETAIL"
private const val AUDIO_RECORD_CLASS = "miui.media.MiuiAudioPlaybackRecorder"
private const val PACKAGE_NAME = "com.milink.service"
private const val SERVICE_NAME = "com.miui.miplay.audio.service.CoreService"
private const val WHITE_TARGET = "com.milink.service:hide_foreground"

/**
* 查询是否支持妙播服务
*
* https://dev.mi.com/xiaomihyperos/documentation/detail?pId=1944
*/
fun supportMiPlay(context: Context): Boolean {
try {
// 未找到抛出 PackageManager.NameNotFoundException
context.packageManager.getServiceInfo(
ComponentName(PACKAGE_NAME, SERVICE_NAME),
PackageManager.MATCH_ALL
)
// 未找到抛出 ClassNotFoundException
context.classLoader.loadClass(AUDIO_RECORD_CLASS)

val isInternationalBuild = isInternationalBuild()
val systemUIReady = systemUIReady(context)
val notificationReady = notificationReady(context)
return !isInternationalBuild && systemUIReady && notificationReady
} catch (_: Exception) {
return false
}
}

/**
* 是否为国际版
*/
private fun isInternationalBuild(): Boolean =
try {
val clazz = Class.forName("miui.os.Build")
val field = clazz.getField("IS_INTERNATIONAL_BUILD")
field.isAccessible = true
field.getBoolean(null)
} catch (_: Exception) {
false
}

/**
* 检查 SystemUI 是否包含处理妙播意图的 Activity
*/
private fun systemUIReady(context: Context): Boolean {
val intent =
Intent(ACTION_MIPLAY_DETAIL).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
// TODO 是否需要 try catch?
return try {
context.packageManager
.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null
} catch (_: ActivityNotFoundException) {
false
}
}

/**
* 检查妙播服务是否在 SystemUI 的前台服务通知白名单中
*/
private fun notificationReady(context: Context): Boolean =
try {
val systemUiAppInfo =
context.packageManager.getApplicationInfo(
"com.android.systemui",
0
)
val resources = context.packageManager.getResourcesForApplication(systemUiAppInfo)
val identifier =
@SuppressLint("DiscouragedApi")
resources.getIdentifier(
"system_foreground_notification_whitelist",
"array",
"com.android.systemui"
)

if (identifier > 0) {
val whiteList = resources.getStringArray(identifier)
val contains = whiteList.contains(WHITE_TARGET)
contains
} else {
false
}
} catch (_: Exception) {
false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package org.akanework.gramophone.logic.utils.exoplayer.oem

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.ResolveInfo
import android.media.MediaRouter2
import android.os.Build
import android.widget.Toast
import androidx.annotation.RequiresApi
import org.akanework.gramophone.R
import org.akanework.gramophone.logic.utils.exoplayer.oem.MiPlayAudioSupport.supportMiPlay

class SystemMediaControlResolver(val context: Context) {
@OptIn(UnstableMediaKitApi::class)
fun intentSystemMediaDialog() {
// val manufacturer = Build.MANUFACTURER.lowercase()
when {
supportMiPlay(context) -> {
val intent = Intent().apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
setClassName(
"miui.systemui.plugin",
"miui.systemui.miplay.MiPlayDetailActivity"
)
}
if (!startIntent(intent)) {
startSystemMediaControl()
}
}
//zh:临时禁用OneUI MediaActivity调用,等待未来确定不会被删除再添加回去
// (getOneUIVersionReadable() != null) -> {
// val intent = Intent().apply {
// flags = Intent.FLAG_ACTIVITY_NEW_TASK
// setClassName(
// "com.samsung.android.mdx.quickboard",
// "com.samsung.android.mdx.quickboard.view.MediaActivity"
// )
// }
// if (!startIntent(intent)) {
// startSystemMediaControl()
// }
// }

else -> {
startSystemMediaControl()
}
}
}

private fun startSystemMediaControl(){
if (Build.VERSION.SDK_INT >= 34) {
// zh: Android 14 及以上
val tag = startNativeMediaDialogForAndroid14(context)
if (!tag) {
Toast.makeText(context, R.string.media_control_text_error, Toast.LENGTH_SHORT).show()
}
} else if (Build.VERSION.SDK_INT >= 31) {
// zh: Android 12 及以上
val intent = Intent().apply {
action = "com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_DIALOG"
setPackage("com.android.systemui")
putExtra("package_name", context.packageName)
}
val tag = startNativeMediaDialog(intent)
if (!tag) {
Toast.makeText(context, R.string.media_control_text_error, Toast.LENGTH_SHORT).show()
}
} else if (Build.VERSION.SDK_INT == 30) {
// Android 11
val tag = startNativeMediaDialogForAndroid11(context)
if (!tag) {
Toast.makeText(context, R.string.media_control_text_error, Toast.LENGTH_SHORT).show()
}
} else {
val intent = Intent().apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
action = "com.android.settings.panel.action.MEDIA_OUTPUT"
putExtra("com.android.settings.panel.extra.PACKAGE_NAME", context.packageName)
}
val tag = startNativeMediaDialog(intent)
if (!tag) {
Toast.makeText(context, R.string.media_control_text_error, Toast.LENGTH_SHORT).show()
}
}
}

private fun startNativeMediaDialog(intent: Intent): Boolean {
val resolveInfoList: List<ResolveInfo> =
context.packageManager.queryIntentActivities(intent, 0)
for (resolveInfo in resolveInfoList) {
val activityInfo = resolveInfo.activityInfo
val applicationInfo: ApplicationInfo? = activityInfo?.applicationInfo
if (applicationInfo != null && (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0) {
context.startActivity(intent)
return true
}
}
return false
}

private fun startNativeMediaDialogForAndroid11(context: Context): Boolean {
val intent = Intent().apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
action = "com.android.settings.panel.action.MEDIA_OUTPUT"
putExtra("com.android.settings.panel.extra.PACKAGE_NAME", context.packageName)
}
val resolveInfoList: List<ResolveInfo> =
context.packageManager.queryIntentActivities(intent, 0)
for (resolveInfo in resolveInfoList) {
val activityInfo = resolveInfo.activityInfo
val applicationInfo: ApplicationInfo? = activityInfo?.applicationInfo
if (applicationInfo != null && (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0) {
context.startActivity(intent)
return true
}
}
return false
}

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private fun startNativeMediaDialogForAndroid14(context: Context): Boolean {
val mediaRouter2 = MediaRouter2.getInstance(context)
return mediaRouter2.showSystemOutputSwitcher()
}

private fun startIntent(intent: Intent): Boolean {
return try {
context.startActivity(intent)
true
} catch (_: Exception) {
false
}
}

// /**
// * zh: 获取 One UI 版本字符串(如 6.0.0),非三星或无此属性则返回 null
// * en: Get One UI version string (e.g. 6.0.0), return null if not Samsung or no such property
// */
// @SuppressLint("PrivateApi")
// private fun getOneUIVersionReadable(): String? {
// return try {
// val systemProperties = Class.forName("android.os.SystemProperties")
// val get = systemProperties.getMethod("get", String::class.java)
// val value = (get.invoke(null, "ro.build.version.oneui") as String).trim()
// if (value.isEmpty()) return null
// val code = value.toIntOrNull() ?: return null
// val major = code / 10000
// val minor = (code / 100) % 100
// val patch = code % 100
// "$major.$minor.$patch"
// } catch (e: Exception) {
// null
// }
// }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@file:Suppress("unused")

package org.akanework.gramophone.logic.utils.exoplayer.oem

/**
* Media Kit
* Copyright (C) 2025 Moriafly
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/

@RequiresOptIn(
message = "This Media Kit API is experimental and is likely to change or be removed in the " +
"future",
level = RequiresOptIn.Level.ERROR,
)
@Retention(AnnotationRetention.BINARY)
annotation class UnstableMediaKitApi
Loading