<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Raspberry Kan's Blog</title><link>https://blog.raspberrykan.dev/</link><description>Recent content on Raspberry Kan's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Tue, 17 Feb 2026 15:15:00 +0000</lastBuildDate><atom:link href="https://blog.raspberrykan.dev/index.xml" rel="self" type="application/rss+xml"/><item><title>MAUI NotificationListenerService 实现记录</title><link>https://blog.raspberrykan.dev/p/maui-notificationlistenerservice-%E5%AE%9E%E7%8E%B0%E8%AE%B0%E5%BD%95/</link><pubDate>Tue, 17 Feb 2026 15:15:00 +0000</pubDate><guid>https://blog.raspberrykan.dev/p/maui-notificationlistenerservice-%E5%AE%9E%E7%8E%B0%E8%AE%B0%E5%BD%95/</guid><description>&lt;h2 id="写在前面"&gt;写在前面
&lt;/h2&gt;&lt;p&gt;这个项目是我高三时期写的一个小玩意，一直没有动力写Blog。但是这次&lt;a class="link" href="https://developer.spotify.com/blog/2026-02-06-update-on-developer-access-and-platform-security" target="_blank" rel="noopener"
&gt;Spotify更新API规则&lt;/a&gt;极大地影响了Lyricify, 同时激起了我寻找不需要Spotify API获取目前正在播放的曲目的好奇心。便将当时的一些心得记录一下。&lt;/p&gt;
&lt;h2 id="获取mediasession"&gt;获取MediaSession
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://developer.android.com/reference/android/service/notification/NotificationListenerService" target="_blank" rel="noopener"
&gt;NotificationListenerService&lt;/a&gt;是Android于 API level 18 时期添加的Service。通过NotificationListenerService 可以获取软件发布的通知。包括我们要讲的Spotify的MediaSession通知。具体实现可参考仓库中的&lt;a class="link" href="https://github.com/Storyteller-Studios/NotificationListenerServiceDemo-dotNet/blob/main/NotificationListener-MAUI/NotificationListenerService.cs" target="_blank" rel="noopener"
&gt;文件&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;需要注意的是：在Android 12及以下的系统中，我们使用&lt;code&gt;notification?.Extras?.GetParcelable(Notification.ExtraMediaSession);&lt;/code&gt;。在Android 13 及以上的系统中，此方法被弃用, 需要使用&lt;code&gt;Class.FromType(typeof(MediaSession.Token));&lt;/code&gt;获取MediaSession.Token的Java Class, 并且在GetParcelable中传入，如下: &lt;code&gt;(IParcelable?)notification?.Extras?.GetParcelable(Notification.ExtraMediaSession, MediaSessionClass);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个Service需要获取通知访问权，可以通过&lt;code&gt;NotificationManagerCompat.GetEnabledListenerPackages(this).Contains(PackageName!);&lt;/code&gt;获取是否已经取得权限。如果没有取得权限，可以使用&lt;code&gt;Intent(&amp;quot;android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS&amp;quot;);&lt;/code&gt;跳转至通知授权界面。同时，这个Service会被系统自动拉起，所以在部分陆版UI(如ColorOS， HyperOS)上需要允许App自启动。&lt;/p&gt;
&lt;p&gt;这个方法有一个小小的缺陷，就是假如通知在授权前就已经被发布，在下一次通知被发布之前，你是无法从通知渠道获取MediaSession的。不过假如你已经获取了通知权限，你可以通过MediaSessionService直接获取当前活动的MediaSession。代码如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MediaSessionManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;GetSystemService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MediaSessionService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;activeSpotifySession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetActiveSessions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NotificationListenerServiceComponentName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PackageName&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;com.spotify.music&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;获取了MediaSession实际上就已经成功75%，接下来就是耳熟能详的RegisterCallback和获取TransportControls。可以参考&lt;a class="link" href="https://github.com/Storyteller-Studios/NotificationListenerServiceDemo-dotNet/blob/main/NotificationListener-MAUI/MainActivity.cs" target="_blank" rel="noopener"
&gt;Demo&lt;/a&gt;中的 OnMediaSessionCreated 方法。&lt;/p&gt;
&lt;h2 id="接收来自spotify-app的broadcast"&gt;接收来自Spotify App的Broadcast
&lt;/h2&gt;&lt;p&gt;这个操作需要先去Spotify的设置-播放-设备广播状态将开关打开。这样之后才可以收到Spotify的Broadcast。&lt;/p&gt;
&lt;p&gt;大部分操作可以参考&lt;a class="link" href="https://developer.spotify.com/documentation/android/tutorials/android-media-notifications" target="_blank" rel="noopener"
&gt;Spotify提供的示例&lt;/a&gt;。MAUI版本已经在&lt;a class="link" href="https://github.com/Storyteller-Studios/NotificationListenerServiceDemo-dotNet/blob/main/NotificationListener-MAUI/BroadcastReceiver.cs" target="_blank" rel="noopener"
&gt;此处&lt;/a&gt;提供。别忘记RegisterReceiver就可以了。&lt;/p&gt;
&lt;h2 id="为什么我重启app之后没办法使用notificationlistener接收消息"&gt;为什么我重启App之后没办法使用NotificationListener接收消息?
&lt;/h2&gt;&lt;p&gt;在Android 7及以上系统，可以通过&lt;code&gt;NotificationListenerService.RequestRebind(NotificationListenerServiceComponentName);&lt;/code&gt;重新绑定 NotificationListenerService&lt;/p&gt;
&lt;p&gt;而在那之下的系统，可以通过&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PackageManager!.SetComponentEnabledSetting(NotificationListenerServiceComponentName!, Android.Content.PM.ComponentEnabledState.Disabled, Android.Content.PM.ComponentEnableOption.DontKillApp);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PackageManager.SetComponentEnabledSetting(NotificationListenerServiceComponentName!, Android.Content.PM.ComponentEnabledState.Enabled, Android.Content.PM.ComponentEnableOption.DontKillApp);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;进行重新绑定&lt;/p&gt;
&lt;p&gt;不过我得吐槽两句，这个RequestRebind方法为啥在很多中文文章里都没提到？我看到很多文章都是用PackageManager这个魔法操作进行Rebind。还有个解答在OnListenerDisconnected中用RequestRebind，OnListenerDisconnected方法是你撤销了通知使用权的时候才会被调用。结果你在这个方法里调用需要通知使用权的方法，您没事吧？&lt;/p&gt;
&lt;h2 id="写在后面"&gt;写在后面
&lt;/h2&gt;&lt;p&gt;这是我在很久没更新Blog之后第一次写Blog，有什么不好的地方麻烦多多包涵。See you next time!&lt;/p&gt;</description></item><item><title>HyPlayer 的 Win2D 歌词面板的后期改进</title><link>https://blog.raspberrykan.dev/p/hyplayer-%E7%9A%84-win2d-%E6%AD%8C%E8%AF%8D%E9%9D%A2%E6%9D%BF%E7%9A%84%E5%90%8E%E6%9C%9F%E6%94%B9%E8%BF%9B/</link><pubDate>Wed, 22 May 2024 09:10:41 +0000</pubDate><guid>https://blog.raspberrykan.dev/p/hyplayer-%E7%9A%84-win2d-%E6%AD%8C%E8%AF%8D%E9%9D%A2%E6%9D%BF%E7%9A%84%E5%90%8E%E6%9C%9F%E6%94%B9%E8%BF%9B/</guid><description>&lt;h2 id="起因"&gt;起因
&lt;/h2&gt;&lt;p&gt;HyPlayer在24年的时候引入了 Win2D 作为新的歌词面板, 之前主力后端开发者写了一篇&lt;a class="link" href="https://zhuanlan.zhihu.com/p/642338137" target="_blank" rel="noopener"
&gt;知乎文章&lt;/a&gt;做介绍。所以这边就先不讲大部分的初期的实现细节。
那讲啥呢？ 众所周知，在后期版本，为了 &lt;del&gt;高度自定义&lt;/del&gt; 界面样式来 &lt;del&gt;压榨&lt;/del&gt; Win2D 的潜力，开发团队引入了字体自定义功能。问题也就出在这里。发版之后的那天晚上，我们用户群里就有人反馈，部分字体会导致高亮出错。具体表现就像是&lt;a class="link" href="https://github.com/microsoft/Win2D/issues/781" target="_blank" rel="noopener"
&gt;这篇Issue&lt;/a&gt;；唯一的不同是，这不是字与字之间的重叠问题。后经过确认，是在特定字体下，字体内部存在重叠，导致在上文提到的 Geometry 取交集的时候无法正确进行计算。重叠的部分于是出现了空洞。&lt;/p&gt;
&lt;h2 id="过程"&gt;过程
&lt;/h2&gt;&lt;p&gt;这就比较麻烦了 不是吗？ 主要是因为高亮需要找到对应位置才能进行，不能硬做。但是Geometry肯定是不能作为后续方案了。幸运的是，我在查文档的时候看到了lindexi老师关于 CanvasActiveLayer 的&lt;a class="link" href="https://www.cnblogs.com/lindexi/p/12085807.html" target="_blank" rel="noopener"
&gt;文章&lt;/a&gt;。
在讲解决方案前，我们要先回忆一下刚刚的内容。歌词高亮/渐变的核心原理都是根据字生成一个Rect，作为高亮区域； 然后将生成的Rect和由字生成的 Geometry 取交集，然后再次绘制到画布上。这时候你应该已经想到了对应解决方案了，既然部分字体会导致 Geometry 在取交集的时候略过重叠部分导致字中出现空洞；那么，我们多次绘制字体，然后利用 CanvasActiveLayer 进行裁切，便可以达成同等的效果。（仍然是进行多次绘制，只不过一个绘制的是 Geometry ，另一个是 TextLayout。 Bug也就迎刃而解了。后来请教蓝火火，蓝火火说 Geometry 取交集是使用 CPU 进行计算，而 CanvasActiveLayer 是利用 GPU 的分层渲染；理论上性能会更高，但是我这里没有办法定量进行测试。于是这个只能先打一个问号。&lt;/p&gt;
&lt;h2 id="结果"&gt;结果
&lt;/h2&gt;&lt;p&gt;这次的修复已经发到了 &lt;a class="link" href="https://github.com/HyPlayer/HyPlayer/commit/8879fd875f3aa07a073dc848fc041fb1d1b530a8" target="_blank" rel="noopener"
&gt;GitHub&lt;/a&gt; 上，只是因为最近一段时间没办法更新Blog，所以到现在才汇报。Blog到这里就结束了，Bye!&lt;/p&gt;</description></item><item><title>记一次将SoftEther部署Let's Encrypt的过程</title><link>https://blog.raspberrykan.dev/p/%E8%AE%B0%E4%B8%80%E6%AC%A1%E5%B0%86softether%E9%83%A8%E7%BD%B2lets-encrypt%E7%9A%84%E8%BF%87%E7%A8%8B/</link><pubDate>Wed, 22 May 2024 09:10:41 +0000</pubDate><guid>https://blog.raspberrykan.dev/p/%E8%AE%B0%E4%B8%80%E6%AC%A1%E5%B0%86softether%E9%83%A8%E7%BD%B2lets-encrypt%E7%9A%84%E8%BF%87%E7%A8%8B/</guid><description>&lt;h2 id="部署起因"&gt;部署起因
&lt;/h2&gt;&lt;p&gt;众所周知，我是一个怪人；毕竟 SoftEther 安装之后默认行为是不认证SSL证书，而且就算部署也不一定真的能用上。但是我还是想部署，主要是 &lt;del&gt;(闲得慌)&lt;/del&gt; 想说好不容易买了个域名 &lt;del&gt;(就不说我域名跑到帮我付钱的群友身上的事情了)&lt;/del&gt; 不上证书怎么行。但是Porkbun自带的Wildcard Certificates需要每三个月手动部署一次。&lt;del&gt;(太麻烦了)&lt;/del&gt; 实质上也是Let&amp;rsquo;s Encrypt 签发的证书。所以干脆用Certbot DNS Challenge直接签发，还能用systemd的Timer自动触发证书更新。何乐而不为？&lt;/p&gt;
&lt;h2 id="部署过程"&gt;部署过程
&lt;/h2&gt;&lt;p&gt;我走的路线是用 Certbot 的 &lt;a class="link" href="https://certbot.eff.org/instructions?ws=other&amp;amp;os=snap" target="_blank" rel="noopener"
&gt;snap&lt;/a&gt; 包以及 infinityofspace 开发的 &lt;a class="link" href="https://github.com/infinityofspace/certbot_dns_porkbun" target="_blank" rel="noopener"
&gt;snap Plugin&lt;/a&gt; &lt;del&gt;(口口声声说不喜欢 snap 实际上真到想偷懒的时候还是直接上了 snap 我感觉我有点傲娇了)&lt;/del&gt;。然后就是不停试错，记得证书文件夹给个755权限，免得vpncmd读不到（我也不知道为啥最开始死活读不到，给了755就行了。但是执行者是root啊，怪欸。）&lt;/p&gt;
&lt;h2 id="部署结果和工具"&gt;部署结果和工具
&lt;/h2&gt;&lt;p&gt;还是很顺利地部署上了&lt;del&gt;要是没有就不会有这篇 blog 了&lt;/del&gt; 这次所用到的脚本已经使用Unlicense license开源在&lt;a class="link" href="https://github.com/Raspberry-Monster/SoftEther-LetsEncrypt-Script" target="_blank" rel="noopener"
&gt;GitHub&lt;/a&gt;，欢迎各位大佬前来参观和批评指正&lt;/p&gt;</description></item><item><title>孩子们，我回来了</title><link>https://blog.raspberrykan.dev/p/%E5%AD%A9%E5%AD%90%E4%BB%AC%E6%88%91%E5%9B%9E%E6%9D%A5%E4%BA%86/</link><pubDate>Wed, 22 May 2024 08:49:05 +0000</pubDate><guid>https://blog.raspberrykan.dev/p/%E5%AD%A9%E5%AD%90%E4%BB%AC%E6%88%91%E5%9B%9E%E6%9D%A5%E4%BA%86/</guid><description>&lt;p&gt;久违的开始更新个人站了。上一次更新的时候还是小学的事情 &lt;del&gt;买了个叫做raspberrymonster.top的域名刺客&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;最开始也不知道用啥库，原本想着用 Gridea 但是发现它似乎没有活跃更新了？&lt;/p&gt;
&lt;p&gt;然后看了看 WordPress 再看看我不充裕的钱包 &lt;del&gt;不好意思打扰了&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;再看了下 Hexo &lt;del&gt;node_modules的质量比黑洞还大&lt;/del&gt; 遂放弃&lt;/p&gt;
&lt;p&gt;于是选择了 Hugo 另一方面是之前仰慕的一个技术大佬 &lt;a class="link" href="https://tinko.moe/" target="_blank" rel="noopener"
&gt;Tinko Liu&lt;/a&gt; 从WordPress迁移之后也用着这个 Hugo 主题，我觉得很好看于是直接 &lt;del&gt;拿来主义&lt;/del&gt; 了。 顺便也能提升我的Markdown能力，不是吗？&lt;/p&gt;
&lt;p&gt;于是乎，我的个人站回归了。&lt;/p&gt;
&lt;p&gt;芜湖！&lt;/p&gt;</description></item><item><title>归档</title><link>https://blog.raspberrykan.dev/archives/</link><pubDate>Sun, 06 Mar 2022 00:00:00 +0000</pubDate><guid>https://blog.raspberrykan.dev/archives/</guid><description/></item><item><title>朋友们</title><link>https://blog.raspberrykan.dev/links/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.raspberrykan.dev/links/</guid><description/></item><item><title>搜索</title><link>https://blog.raspberrykan.dev/search/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.raspberrykan.dev/search/</guid><description/></item></channel></rss>