AR 眼镜之-系统通知定制(通知弹窗)-实现方案

目录

[📂 前言](#📂 前言)

[AR 眼镜系统版本](#AR 眼镜系统版本)

系统通知定制

[1. 🔱 技术方案](#1. 🔱 技术方案)

[1.1 技术方案概述](#1.1 技术方案概述)

[1.2 实现方案](#1.2 实现方案)

1)实现系统通知的监听

2)系统通知显示:通知弹窗

[2. 💠 实现系统通知的监听](#2. 💠 实现系统通知的监听)

[2.1 继承 NotificationListenerService](#2.1 继承 NotificationListenerService)

[2.2 在 manifest 中声明这个可接收通知的服务](#2.2 在 manifest 中声明这个可接收通知的服务)

[2.3 让通知应用拥有获取系统通知的权限](#2.3 让通知应用拥有获取系统通知的权限)

1)通知应用申明可获取系统通知使用权限

2)判断通知应用是否拥有可获取系统通知的权限

3)打开通知权限设置页面

[3. ⚛️ 系统通知显示:通知弹窗](#3. ⚛️ 系统通知显示:通知弹窗)

[3.1 统一处理通知](#3.1 统一处理通知)

[1)每条通知到来时由 handleNotification 分发路由](#1)每条通知到来时由 handleNotification 分发路由)

[2)NotificationManagerBean 区分 AR 眼镜通知以及与 AR 眼镜连接的手机通知](#2)NotificationManagerBean 区分 AR 眼镜通知以及与 AR 眼镜连接的手机通知)

3)飞行模式时不显示系统通知

[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)实现系统通知的监听
  1. 继承 NotificationListenerService,实现 onNotificationPosted 方法;

  2. 在 manifest 中声明这个可接收通知的服务;

  3. 让通知应用拥有获取系统通知的权限。

2)系统通知显示:通知弹窗
  1. 统一处理通知;

  2. 播放通知音效;

  3. 显示与隐藏通知弹窗。

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. ✅ 小结

对于系统通知定制(通知弹窗),本文只是一个基础实现方案,更多业务细节请参考产品逻辑去实现。

另外,由于本人能力有限,如有错误,敬请批评指正,谢谢。


附录1:SystemUI 流程

SystemUI流程_systemui启动流程-CSDN博客文章浏览阅读1k次。SystemUI 是系统应用,由 SystemServer 进程进行启动,入口 Application 为SystemUIApplication。常用UI组件有如下几个:状态栏 StatusBar通知栏 NotificationPanel导航栏 NavigationBar最近任务 Recent键盘锁 Keyguard以上从 SystemUI 大概类图,以及自身启动流程开始,到 StatusBar 创建流程,再到系统 Notification 实现流程,一步步去理解 SystemUI 的相关流程。_systemui启动流程https://blog.csdn.net/Agg_bin/article/details/130252705

附录2:使用 NotificationListenerService 监听通知

Android9-W517-使用NotificationListenerService监听通知_android notificationlistenerservice-CSDN博客文章浏览阅读1.2k次,点赞18次,收藏15次。方案一通过Action跳转《系统设置》应用,手动打开通知监听权限:android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS------结果如图:显示在此设备上不能获得此特性------暂不可行;方案二在源码frameworks/base/core/res/res/values/config.xml路径下,修改config_defaultListenerAccessPackages属性的值为应用包名com.***.launcher------_android notificationlistenerservicehttps://blog.csdn.net/Agg_bin/article/details/136483571

相关推荐
AFinalStone1 个月前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
AFinalStone1 个月前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
AFinalStone1 个月前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui
AFinalStone1 个月前
Android7 SystemUI 源码解析(一)全景概览与架构设计
android·systemui
AFinalStone1 个月前
Android7 SystemUI 源码解析(六)QuickSettings快捷设置面板
android·systemui
贾伟康2 个月前
【时光清单|04】HarmonyOS ArkTS 通知服务实战:管理权限、渠道和点击跳转
harmonyos·arkts·系统通知·notificationkit·wantagent
陆业聪7 个月前
SystemUI 里做 Launcher App 列表:四种方案的 Framework 原理与工程取舍
android·framework·systemui·launcher
Just_Paranoid9 个月前
【TaskbarDelegate】屏蔽上滑返回桌面手势功能
android·systemui·navigation·launcher·gesture
江上清风山间明月10 个月前
Android BIND_NOTIFICATION_LISTENER_SERVICE 权限详解
android·notification·service·bind·listener
Just_Paranoid10 个月前
【SystemUI】基于 Android R 实现下拉状态栏毛玻璃背景
android·canvas·systemui·renderscript