目录
[📂 前言](#📂 前言)
[AR 眼镜系统版本](#AR 眼镜系统版本)
[1. 🔱 技术方案](#1. 🔱 技术方案)
[1.1 技术方案概述](#1.1 技术方案概述)
[1.2 实现方案](#1.2 实现方案)
[2. 💠 实现系统通知的监听](#2. 💠 实现系统通知的监听)
[2.1 继承 NotificationListenerService](#2.1 继承 NotificationListenerService)
[2.2 在 manifest 中声明这个可接收通知的服务](#2.2 在 manifest 中声明这个可接收通知的服务)
[2.3 让通知应用拥有获取系统通知的权限](#2.3 让通知应用拥有获取系统通知的权限)
[3. ⚛️ 系统通知显示:通知弹窗](#3. ⚛️ 系统通知显示:通知弹窗)
[3.1 统一处理通知](#3.1 统一处理通知)
[1)每条通知到来时由 handleNotification 分发路由](#1)每条通知到来时由 handleNotification 分发路由)
[2)NotificationManagerBean 区分 AR 眼镜通知以及与 AR 眼镜连接的手机通知](#2)NotificationManagerBean 区分 AR 眼镜通知以及与 AR 眼镜连接的手机通知)
[3.2 播放通知音效](#3.2 播放通知音效)
[3.3 showDialog 显示通知弹窗](#3.3 showDialog 显示通知弹窗)
[1)NotificationLayoutDialogBinding 加载通知弹窗 View](#1)NotificationLayoutDialogBinding 加载通知弹窗 View)
[2)getAppName 获取 app 名](#2)getAppName 获取 app 名)
[3)showNotification 显示通知弹窗 View](#3)showNotification 显示通知弹窗 View)
[4. ✅ 小结](#4. ✅ 小结)
[附录1:SystemUI 流程](#附录1:SystemUI 流程)
[附录2:使用 NotificationListenerService 监听通知](#附录2:使用 NotificationListenerService 监听通知)
📂 前言
AR 眼镜系统版本
W517 Android9。
系统通知定制
系统通知的底层 实现主要依赖 Android 原生通知模块 NotificationManagerService ,系统通知的上层 UI 主要依赖于继承 NotificationListenerService 去实现,实现过程如下图所示,主要分为三步:1)应用 A 通过 sendNotification 发送通知;2)Android 通知模块 NotificationManagerService 接收到通知;3、应用 B 通过继承 NotificationListenerService监听到系统通知。对于底层实现感兴趣的同学可自行去深入了解,本文所讨论的系统通知实现方案主要针对于上层 UI。
那么,Android 原生系统通知是怎样实现的呢?答案很简单:通过 SystemUI 应用实现,SystemUI 通过继承 NotificationListenerService 监听系统通知,然后显示在通知栏。
但是,AR 眼镜系统与传统 Android 2D 存在较大显示与交互差异,且根据产品需求综合来看,本文采用类似 SystemUI 的方案,通知应用 通过继承 NotificationListenerService 实现系统通知的监听与显示。
1. 🔱 技术方案
1.1 技术方案概述
通知应用 通过继承 NotificationListenerService 实现系统通知的监听与显示,上层 UI 主要包括:通知弹窗、通知中心,系统通知定制的实现方案将分为两个篇章展开,分别是 通知弹窗篇 与 通知中心篇。
1.2 实现方案
1)实现系统通知的监听
-
继承 NotificationListenerService,实现 onNotificationPosted 方法;
-
在 manifest 中声明这个可接收通知的服务;
-
让通知应用拥有获取系统通知的权限。
2)系统通知显示:通知弹窗
-
统一处理通知;
-
播放通知音效;
-
显示与隐藏通知弹窗。
2. 💠 实现系统通知的监听
2.1 继承 NotificationListenerService
主要实现其中的 onNotificationPosted(sbn: StatusBarNotification) 方法,其他方法可按需实现,如: onNotificationRemoved(sbn: StatusBarNotification)、onListenerConnected()、onListenerDisconnected()。
class AGGNotificationListenerService : NotificationListenerService() {
override fun onNotificationPosted(sbn: StatusBarNotification) {
super.onNotificationPosted(sbn)
Log.i(TAG, "onNotificationPosted: packageName = ${sbn.packageName}")
// 普通通知:未设置Style
// 设置点击 setContentIntent
// 设置按钮 addAction(最多可添加三个)
// 设置进度条 setProgress
// 设置自定义通知 setCustomContentView(RemoteViews)
// 设置自定义通知展开视图 setCustomBigContentView(RemoteViews)
// 设置自定义顶部提醒视图 setCustomHeadsUpContentView(RemoteViews(context.getPackageName(),R.layout.custom_heads_up_layout))
// 带图标样式 setLargeIcon
// 1. 过滤黑名单包名的通知。
if (BLACK_LISTING_PACKAGE_NAME.contains(sbn.packageName)) return
// 2. 过滤空内容消息通知
val title = sbn.notification.extras.getString(Notification.EXTRA_TITLE, "")
val content = sbn.notification.extras.getCharSequence(Notification.EXTRA_TEXT, "")
if (title.isEmpty() && content.isEmpty()) return
if (content == getString(R.string.app_running_notification_text)) return // 去掉通知: "记录"正在运行,点按即可了解详情或停止应用
AGGNotificationManager.handleNotification(
this, NotificationManagerBean(NotificationManagerBean.FROM_GLASS, sbn)
)
}
override fun onNotificationRemoved(sbn: StatusBarNotification) {
super.onNotificationRemoved(sbn)
Log.i(TAG, "onNotificationRemoved: packageName = ${sbn.packageName}")
}
override fun onListenerConnected() {
super.onListenerConnected()
Log.i(TAG, "onListenerConnected: ")
}
override fun onListenerDisconnected() {
super.onListenerDisconnected()
Log.i(TAG, "onListenerDisconnected: ")
}
companion object {
private val TAG = AGGNotificationListenerService::class.java.simpleName
private val BLACK_LISTING_PACKAGE_NAME =
// Android系统通知、Android电话通知
mutableSetOf("android", "com.android.dialer", "com.android.server.telecom")
}
}
2.2 在 manifest 中声明这个可接收通知的服务
<service
android:name=".AGGNotificationListenerService"
android:exported="true"
android:label="AGG Notification"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
2.3 让通知应用拥有获取系统通知的权限
1)通知应用申明可获取系统通知使用权限
<uses-permission android:name="android.permission.MANAGE_NOTIFICATIONS" />
2)判断通知应用是否拥有可获取系统通知的权限
fun isNotificationListenersEnabled(context: Context, packageName: String): Boolean = NotificationManagerCompat.getEnabledListenerPackages(context).contains(packageName)
3)打开通知权限设置页面
fun gotoNotificationAccessSetting(context: Context): Boolean {
return try {
val intent = Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
true
} catch (e: ActivityNotFoundException) {
// 普通情况下找不到的时候需要再特殊处理找一次
try {
val intent = Intent()
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val cn = ComponentName(
"com.android.settings",
"com.android.settings.Settings\$NotificationAccessSettingsActivity"
)
intent.component = cn
intent.putExtra(":settings:show_fragment", "NotificationAccessSettings")
context.startActivity(intent)
return true
} catch (e1: java.lang.Exception) {
e1.printStackTrace()
}
Toast.makeText(this, "对不起,您的手机暂不支持", Toast.LENGTH_SHORT).show()
e.printStackTrace()
false
}
}
注:如若获取不到系统通知,可参考本文末尾的附录2:使用 NotificationListenerService 监听通知。
3. ⚛️ 系统通知显示:通知弹窗
3.1 统一处理通知
1)每条通知到来时由 handleNotification 分发路由
object AGGNotificationManager {
private val TAG = AGGNotificationManager::class.java.simpleName
/**
* 处理通知,每条通知到来时先经过此处路由。
*/
fun handleNotification(context: Context, notificationManagerBean: NotificationManagerBean) {
// ...
}
}
2)NotificationManagerBean 区分 AR 眼镜通知以及与 AR 眼镜连接的手机通知
data class NotificationManagerBean(
@FromType var from: Int = FROM_NONE, // 通知来源:1:眼镜;2:手机
var glassNotification: StatusBarNotification? = null, //眼镜通知
var phoneNotification: MessageReqMsgNoti? = null, // 手机通知
) {
@IntDef(FROM_NONE, FROM_GLASS, FROM_PHONE)
@Retention(AnnotationRetention.SOURCE)
annotation class FromType
companion object {
const val FROM_NONE = -1
const val FROM_GLASS = 1
const val FROM_PHONE = 2
}
}
3)飞行模式时不显示系统通知
object AGGNotificationManager {
private val TAG = AGGNotificationManager::class.java.simpleName
/**
* 处理通知,每条通知到来时先经过此处路由。
*/
fun handleNotification(context: Context, notificationManagerBean: NotificationManagerBean) {
if (isAirPlaneMode(context)) {
Log.i(TAG, "handleNotification: isAirPlaneMode = true.")
return
}
// ...
}
/**
* 是否在飞行模式
*/
fun isAirPlaneMode(context: Context): Boolean = Settings.Global.getInt(
context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0
) == 1
}
3.2 播放通知音效
SoundPoolTools.playNotifyCome(context.applicationContext)
参考系统应用音效播放即可:AR 眼镜之-系统应用音效-实现方案-CSDN博客
3.3 showDialog 显示通知弹窗
1)NotificationLayoutDialogBinding 加载通知弹窗 View
showDialog(
context: Context,
packageName: String,
smallIcon: Drawable?,
title: String,
content: CharSequence
){
val binding = NotificationLayoutDialogBinding.inflate(LayoutInflater.from(context)).apply {
itemInfoLeftIcon.setImageDrawable(smallIcon)
itemInfoMsg.text = getAppName(context, packageName)
itemTitle.text = title
itemContent.text = content
}
AGGSuspensionNotification.showNotification(context, binding.root)
}
2)getAppName 获取 app 名
fun getAppName(context: Context, packageName: String): String {
return try {
val pm = context.packageManager
val pi = pm.getPackageInfo(packageName, 0)
pi?.applicationInfo?.loadLabel(pm)?.toString() ?: packageName
} catch (e: Exception) {
packageName
}
}
3)showNotification 显示通知弹窗 View
object AGGSuspensionNotification {
private val TAG = AGGSuspensionNotification::class.java.simpleName
private var mWindowManager: WindowManager? = null
private var mLayoutParams: WindowManager.LayoutParams? = null
private var mCustomView: View? = null
fun showNotification(context: Context, customView: View) {
mCustomView = customView
initLayoutParams(context)
if (!customView.isAttachedToWindow) {
kotlin.runCatching {
Log.i(TAG, "showNotification: addView")
mWindowManager?.addView(customView, mLayoutParams)
}
}
}
fun removeNotification() {
Log.i(TAG, "removeNotification: ")
if (mCustomView?.isAttachedToWindow == true) {
kotlin.runCatching {
Log.i(TAG, "removeNotification: removeViewImmediate")
mWindowManager?.removeViewImmediate(mCustomView)
}
mCustomView = null
}
}
private fun initLayoutParams(context: Context) {
Log.i(TAG, "initLayoutParams: ")
mWindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
mLayoutParams = WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY
flags =
(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or WindowManager.LayoutParams.FLAG_SPLIT_TOUCH or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
format = PixelFormat.TRANSLUCENT
width = (840 * context.resources.displayMetrics.density + 0.5f).toInt()
height = (840 * context.resources.displayMetrics.density + 0.5f).toInt()
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
x = -100
y = -100
title = TAG + "_MASK"
// dofIndex = 0
// setTranslationZ(TRANSLATION_Z_150CM)
}
}
}
注:对于通知弹窗的消失,以及通知中心显示与交互,由于篇幅问题,将放在下一篇章。 AR 眼镜之-系统通知定制(通知中心)-实现方案-CSDN博客
4. ✅ 小结
对于系统通知定制(通知弹窗),本文只是一个基础实现方案,更多业务细节请参考产品逻辑去实现。
另外,由于本人能力有限,如有错误,敬请批评指正,谢谢。