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

news2024/9/21 0:27:25

目录

📂 前言

AR 眼镜系统版本

系统通知定制

1. 🔱 技术方案

1.1 技术方案概述

1.2 实现方案

1)实现系统通知的监听

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

2. 💠 实现系统通知的监听

2.1 继承 NotificationListenerService

2.2 在 manifest 中声明这个可接收通知的服务

2.3 让通知应用拥有获取系统通知的权限

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

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

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

3. ⚛️ 系统通知显示:通知弹窗

3.1 统一处理通知

1)每条通知到来时由 handleNotification 分发路由

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

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

3.2 播放通知音效

3.3 showDialog 显示通知弹窗

1)NotificationLayoutDialogBinding 加载通知弹窗 View

2)getAppName 获取 app 名

3)showNotification 显示通知弹窗 View

4. ✅ 小结

附录1:SystemUI 流程

附录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)
        }
    }

}

注:对于通知弹窗的消失,以及通知中心显示与交互,由于篇幅问题,将放在下一篇章。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2087090.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

全场景——(六)Moubus 功能码详解

文章目录 一、功能码概要二、&#xff08;0x01&#xff09;读取线圈2.1 功能说明2.2 查询报文2.3 响应报文 三、 (0x02) 读取离散量输入值3.1 功能说明3.2 查询报文3.3 响应报文 三、&#xff08;0x03&#xff09;读取保持寄存器值3.1 功能说明3.2 查询报文3.3 响应报文 五、 (…

LabVIEW项目中硬件选型与长期需求沟通

在LabVIEW项目中&#xff0c;选择合适的硬件和有效的需求沟通是成功的关键。大品牌硬件通常具备更高的稳定性和完善的售后服务&#xff0c;而小品牌虽然看似便宜&#xff0c;却可能带来通讯不稳定、技术支持不足等问题&#xff0c;增加开发难度。同时&#xff0c;在科研类项目中…

光性能 -- OSNR Loss均衡

什么是OSNR&#xff1f; OSNR&#xff08;Optical Signal Noise Ratio &#xff09;即光信噪比。它是指传输链路中的信号光功率与噪声光功率的比值&#xff0c;OSNR计算公式如下所示&#xff1a; OSNR是决定波分系统性能的最主要因素之一。它决定了传输性能的最高值&#xff0c…

ZaKi:Ingonyama的Prover market基础设施

1. 引言 Ingonyama团队预计在不久的将来会出现大量去中心化证明市场&#xff08;Prover market&#xff09;。这些市场的独特之处在于高可用性和高性能的基础设施&#xff0c;以及强大的安全性和透明度保障。 2. 证明市场的出现 零知识 (ZK) Rollups&#xff0c;如 Starknet…

望繁信科技亮相2024数博会:以流程智能引领数字化转型新未来

在全球瞩目的2024中国国际大数据产业博览会&#xff08;以下简称“数博会”&#xff09;上&#xff0c;上海望繁信科技有限公司&#xff08;简称“望繁信科技”&#xff09;作为大数据流程智能领域的领军企业&#xff0c;隆重亮相并展示了其在数字化转型与人工智能领域的最新科…

如何使用WebSafeCompiler进行网站优化和知识产权保护

关于WebSafeCompiler WebSafeCompiler是一款功能强大的Web安全与优化工具&#xff0c;该工具基于Node.js开发&#xff0c;可以帮助广大研究人员编译静态网站以进行优化或知识产权保护。 WebSafeCompiler&#xff08;WSC&#xff09;是一款高级Node.js实用程序&#xff0c;旨在…

Python 处理 PDF 文件(PyPDF2, ReportLab)

Python 是一门强大的编程语言&#xff0c;在处理PDF文件方面有着丰富的库支持&#xff0c;其中最常用的两个库是 PyPDF2 和 ReportLab。PyPDF2 主要用于读取、拆分、合并和修改已有的PDF文件&#xff0c;而 ReportLab 则擅长生成新的PDF文件。 一、PyPDF2 1. PyPDF2 概述 Py…

Mysql剖析(四)----Mysql的行锁、表锁、间隙锁详解

目录 一丶Mysql的锁分类 二丶乐观锁和悲观锁 三丶共享锁和排它锁 四丶表锁页锁和行锁 五丶Mysql锁超时 简介&#xff1a;编程中的锁是一种同步机制&#xff0c;用于控制对共享资源的访问&#xff0c;确保同一时间只有一个线程可以访问共享资源&#xff0c;从而避免竞态条件…

通信协议——Modbus 讲明白了

目 录 Modbus通信协议一、说明1.1 描述1.2 查询1.3 响应 二、异常三、功能码 Modbus通信协议 Modbus由Modicon公司&#xff08;也是现在的施耐德电气&#xff09;于1979年开发&#xff0c;是一种工业现场总线协议标准。最初是为使用PLC&#xff08;可编程逻辑控制器&#xff0…

[激光原理与应用-126]:傅里叶变化与频域分析

目录 一、什么是傅里叶变换 1.1 基本概念 &#xff08;1&#xff09;首先&#xff0c;先看时域图像&#xff1a; &#xff08;2&#xff09;接下来&#xff0c;频域波形 &#xff08;3&#xff09;傅里叶变换 1.2 应用 &#xff08;1&#xff09;离散傅里叶变换在图像处…

Shell脚本格式化打印信息样例

以下是一个基于Centos7安装docker的脚本 logging.sh [rootbogon ~]# cat logging.sh #!/bin/bashNC\033[0m RED\033[31m GREEN\033[32m YELLOW\033[33m BLUE\033[34mlog::err() {printf "[$(date %Y-%m-%dT%H:%M:%S.%2N%z)][${RED}ERROR${NC}] %b\n" "$"…

美团2024年春招第一场笔试【前端移动端方向】编程题题解Java

1、小美的平衡矩阵 前缀和&#xff0c;时间复杂度为O(n^3) 对于每个矩形&#xff0c;已知边长k&#xff0c;只用每次遍历矩形的左上顶点&#xff0c;就可以确定整个矩形范围。然后统计该矩形中01的具体数量&#xff0c;判断是否相等。而这一步可以使用前缀和&#xff0c;建立数…

PN结--

文章目录 准备知识P型 N型半导体多子 少子&#xff08;相关性质受温度影响程度的关键点&#xff09; PN结的形成势垒 PN结的单向导通性正向偏置反向偏置 PN结的电流方程正向特性反向特性反向击穿反向击穿会把二极管搞坏吗 掺杂浓度与其宽度的关系 准备知识 P型 N型半导体 P型…

无人机之使用技巧篇

无人机使用技巧涉及多个方面&#xff0c;从起飞前的准备到飞行中的操控&#xff0c;再到降落后的维护&#xff0c;都需要掌握一定的技巧。以下是一些关键的无人机使用技巧&#xff1a; 一、起飞前准备 检查无人机&#xff1a;确保无人机完好无损&#xff0c;各部件连接牢固&a…

Adobe DC 2022提示无法识别的错误 - 解决方案

Adobe DC 2022提示无法识别的错误 - 解决方案 问题解决方案更改安装&#xff08;推荐&#xff09;重新安装&#xff08;推荐&#xff09;降级安装&#xff08;不推荐&#xff09; 问题 使用Adobe DC 2022合并图片创建PDF时&#xff0c;会提示无法识别的错误&#xff0c;这是因…

OrangePi AIpro 香橙派 昇腾 Ascend C算子开发 - 环境搭建

OrangePi AIpro 香橙派 昇腾 Ascend C算子开发 flyfish 环境搭建部分 OrangePi AIpro 香橙派的npu为310B4&#xff0c;通常配置的时候写 Ascend310B4 社区版资源下载地址 下载 CANN 这里选择的是 CANN 8.0.RC3.alpha002 异构计算架构CANN&#xff08;Compute Architecture …

SpringMVC 笔记篇

2.1 ForkJoinPool底层原理分析 2.1.1 JDK19中的虚拟线程 首先注意要把项目切换成JDK19 首先我们这里有两个线程&#xff0c;一个是newFixedThreadPool&#xff0c;就是一个普通的线程池&#xff0c;另一个是我们的虚拟线程newVirtualThreadPerTaskExecutor。这里只有一个线程…

鸿蒙Harmony开发实战:线程异步JS 机制总结

背景介绍 回调函数是 JavaScript 中常见的一种编程模式&#xff0c;它在异步编程中起到了重要作用。在 NAPI (Node.js API)中&#xff0c;回调函数也被广泛应用&#xff0c;用于处理各种异步操作&#xff0c;例如文件读写、网络请求等。按照触发源或目的线程分为 JS 线程、C 线…

爬虫入门学习

流程 获取网页内容 HTTP请求 Python Requests解析网页内容 HTML网页结构 Python Beautiful Soup储存或分析数据 HTTP (Hypertext Transfer Protocol) 客户端和服务器之间的请求-响应协议 Get方法&#xff1a;获得数据 POST方法&#xff1a;创建数据 HTTP请求 请求行 方法类型…

HTML 总结

一、HTML概述 HTML 指的是超文本标记语言 超文本&#xff1a;是指页面内可以包含图片、链接、声音,视频等内容。标记&#xff1a;标签(通过标记符号来告诉浏览器网页内容该如何显示) 二、HTML的基本语法 1.Head标签 包含了所有的头部标签&#xff0c;例如&#xff1a;<t…