为了舒服地换一张壁纸

Originally published at: 为了舒服地换一张壁纸 – Foo "Raspberry" Barz

从 Wallpaper Changer 到 Tasker, 再到自己 Vibe 了一个 App 出来……

众所周知的,计算机科学有两大终极难题:缓存失效,起名字,以及差一错误。

我以前在用什么,痛在哪里 🌚

Wallpaper Changer

https://play.google.com/store/apps/details?id=de.j4velin.wallpaperChanger

一个老当益壮的软件,能从手机存储的一个或若干个目录中选取照片,然后用动态壁纸的方式切换,能定时能锁屏能双击桌面切换,用起来本来挺舒服的,但问题在哪呢?

问题在它处理壁纸的方式只有两种:

  • 适应主屏幕:什么图都缩放到桌面的两倍,然后照着尺寸裁,结果就是纵向图片惨遭第二张图那般砍头。 🌚
  • 全屏分辨率:居中裁剪,然后不滚动。

于是成了惨遭抛弃的第一个。虽然其实没全抛掉,原因看到后面就懂了 🌝

Tasker

https://tasker.joaoapps.com , 没错就是那个 Tasker。

至于为什么我会想到用这个我忘了,在一度用了自己手写的 Tasker 脚本一段时间之后,壁纸砍头的坑算是解决了,然后有了新的坑。

  • 我到现在也没搞懂比例缩放、裁切和居中有什么效果,因为我怎么试好像都差不多。😅
  • 如果要每次随机壁纸都现场遍历目录的话,SAF 的效率能低到怀疑人生🌚,于是就有了用数组存文件名的奇观 😅

除此这外,这两种方案还有一个共通的问题是,我要在我想换壁纸的每一部手机上准备相同的壁纸文件夹,既不 DRY 又浪费闪存空间 ……(手握 512GB 甚至 TB 级别的大户人家请无视)

开始画饼 🫓

虽然我之前一个 Android App 没写过,甚至没写过 Kotlin,但这不是赶上了 Vibe Coding 的 龙卷风了嘛,于是就抄起之前用过的 Antigravity ,借着优惠订了个 Google AI Pro 开干了。

如果有人问我为什么不用 GPT-6 Astra 或者 Claude Fable-5.1 而用的是被戏称美国大豆包的 Gemini 3.8 Flash 和看起来慢半拍(Claude Code 都打算放弃 Plan 了 Agy 才慢吞吞拿出 Plan Mode)的 Antigravity 😅:
回答:因为买不起(Google AI Pro 都是靠着前三月折扣订的🌚),你送我一份 ChatGPT Pro / Claude Max 或者帮我充几千刀 API 积分我就帮你吹爆好不好 🌝

当跨代兼容的野心遇上 Android Studio

因为手上有还能亮,偶尔还打开用用的 Android 4.4 和 6.0 设备,于是在收到 AI 不要搞现代和古老版本两个分支的提示之后,本来想把最低 SDK 设置成 Android 4.4 的 19 的。 然后就被 Compose 和 Android Studio 两面夹击劝退 🌝

  • AndroidX 和 Compose 从去年就开始要求 API Level 23 以上了。
  • 如图所示,新版 Android Studio 新建项目时已经不给更低的选项了。

所以 2026 年了,为什么还要兼容到 Android 6.0 (API 23)?就是因为手上真有钉子户在发光发热!
虽然为了兼容 Android 6.0 还要用 Conscrypt 强行支持 TLS 1.3,和小心 OOM 都是后面踩过的坑了 🌝

饼里有什么?

摘录自 PLANS.md 。

  • 首先,要能实现 Wallpaper Changer 的功能,随机挑选壁纸和定时更换壁纸。
  • 然后,为了节省闪存空间,要能从网络下载图片作为壁纸。于是有了 HTTP API 和 immich 支持。

最开始的饼真的只有这两种面,谁能想到后来越做越多……🌚

抛开技术问题不谈,先说说这 App 干啥 🌝

一句话总结: 它不弹广告、不推通知、不瞎常驻耗电;你感觉不到它的存在,直到每次点亮屏幕,发现手机里又是另一番赏心悦目的风景。”

以上内容由 Antigravity 通过 Gemini 3.8 Flash 生成

可以从系统相册,本地文件夹(SAF) , 返回图片的 HTTP API 和 Immich 挑选图片。

选择一个本地目录真的要像孔乙己写回字一样用三种方法么 😅

既然已经有了通过 SAF 选【本地文件夹】的功能,为什么还要多此一举地搞一个【系统相册(MediaStore)】?甚至还要去对接 Google 那个只能勾选几张单图、死活选不了整个文件夹的‘残疾’【Photo Picker】?

SAF(本地文件夹)的隐秘痛点:它很强,但 Google 天天背刺它

用 SAF 的 ACTION_OPEN_DOCUMENT_TREE 选一个文件夹,拿到永久授权,看起来最符合极客直觉,对吧? 然而现实是:

  • Google 的“为了你好”封杀令: 从 Android 11 开始,Google 开始在 SAF 里搞连坐。今天禁止选根目录,明天禁止选 Download,后天某些国产 ROM 甚至禁止你直接选 DCIM 或者 Pictures 根目录。用户一点进去,系统弹个冷冰冰的提示:“为了保护隐私,请选择其它文件夹”,用户转头就给 App 提 Issue 骂作者写了个 Bug。
  • 冷门灾难:性能像乌龟爬: SAF 底层是基于 ContentProvider 的进程间通信(IPC)。如果用户选了一个塞了 3000 张图的文件夹,用官方推荐的遍历方式扫一遍,手机能当场卡住好几秒,电池电量肉眼可见地往下掉。
  • 小白劝退器: SAF 调起的是系统那个像电脑文件管理器一样冷冰冰的“文件浏览”界面。很多普通用户根本不知道自己的照片到底存在 DCIM/Camera 还是微信目录下,点进去一片迷茫。

系统相册(MediaStore):专治小白与“快枪手”

为了拯救上面的痛点,才不得不手搓了 MediaStore 系统相册源:

  • 快,快到离谱: 系统在后台早就建好了一个 SQLite 索引数据库。本应用查图只需发一句极轻量的 SQL 查询,数万张照片 15 毫秒内瞬间挑出一张,连读盘都省了,极其省电!
  • 零学习成本: 根本不用管照片在哪个诡异的路径,只要是你手机相册里看得到的,它都能自动抓出来轮换。

那个难用至极的“照片选择器”:防贼式隐私

“后来 Google 又推出了一个大吹特吹的‘照片选择器’(Photo Picker),声称‘零权限保护隐私’。那为什么不用那个呢?

  • 它可以让你单选一张、或者勾选几十张照片;
  • 但是!它根本不支持选整个相册,也不支持选整个文件夹!

作为一个壁纸轮换工具,我要的是‘从某个相册里随机抽取’,Google 却只允许用户像发朋友圈一样,手动一张一张去勾选……我要是有闲心天天一张张手动挑,我还用得着写自动轮换工具吗?!这简直是拿着绣花针去耕地 🌚。


3. Android 14+ 的终极大招:选择部分照片 🌝

“最戏剧性的冲突出现在我这个 Target SDK 36 的现代 Android 环境下:

从 Android 14(API 34)开始,当你申请读取相册权限(READ_MEDIA_IMAGES)时,系统弹窗大发慈悲地给了用户三个选项:

  • 选择部分照片
  • 允许访问全部照片
  • 不允许

这个‘选择部分照片’的设计,初衷是为了防流氓软件偷窥相册。但放到壁纸切换器身上,场面就变得极具喜感:

如果你戒备心很重,随手勾了 3 张风景照,点了【选择部分照片】—— 那么本 App 的后台 Worker 每次醒来,在几千张相册里能‘合法’看见的,就永远只有这 3 张图。于是你就会看到你的手机壁纸,每天在这 3 张图之间进行永恒的鬼畜循环……

在这里不得不卑微地跟各位读者交代一句:

‘各位义士!本项目代码 100% 开源(GPLv3),零联网上传、连网络请求都全写在明处。如果大家信得过孩子,在弹相册权限时请务必高抬贵手,赏脸点一个【允许访问全部】!否则你几千张私藏的高清大图,这倒霉 App 真的只能看着你施舍的那三张图打冷颤了!🌚’” (或者你清楚你的壁纸放在哪里也可以直接用加了土味缓存的 SAF 🌝

从历史记录中挑选你的壁纸中特别喜欢的那几张加入收藏,它的缓存就不会被自动清理掉。

安排计划表,从间隔时间到定时切换,或是锁屏后切换。

为什么非要精细定时器与关电池优化?—— 以及为什么要把用户踹进系统设置里受罪 🌚

1. 为什么有了 WorkManager,还要精细定时器(Exact Alarm)?

“我们应用默认的周期轮换是用 Google 官方推荐的 WorkManager 跑的。它很省电,但有一个致命毛病:它是个‘佛系大师’。

  • 毛病一:最小间隔 15 分钟。想每 5 分钟切一张?WorkManager 直接表示‘我不干’。
  • 毛病二:它根本不在乎你约定的具体时间。 你设置了‘每天早晨 08:00 准时打卡换一张风景壁纸’。 到了 08:00,系统说:‘这会儿 CPU 正在省电,先睡会儿’。 于是你的壁纸可能在 08:17、08:42,甚至等你上午 10 点开完会亮屏时,才懒洋洋地换出来……

要想做到‘说 8 点换就 8 点整换’、或者‘晚上 22:00 准时切暗黑模式’,就必须搬出 Android 最严格的武器——高精度闹钟(AlarmManager.setExactAndAllowWhileIdle)。 而在现代 Android (12+)上,申请‘闹钟和提醒’权限比查户口还严,一不留神就会被系统或者应用商店当成高危分子盘查。


2. 为什么一定要关电池优化?——Doze 模式与国产 ROM 的绞杀网

“有朋友可能会问:‘现在的手机动不动 12G、16G 内存,电池五千毫安,换个壁纸才几毫秒,至于关电池优化吗?’

答案是:极其至于。因为现代系统的后台机制,完全是把你当窃贼在防。

从 Android 6.0 引入 Doze 模式(打盹模式)开始,一旦你的手机锁屏放在桌上超过半小时,系统就会进入‘深度冬眠’:

  • 拔掉你的网络(私有云 Immich 和 API 根本发不出请求);
  • 没收你的 WakeLock(不准 CPU 唤醒);
  • 把所有的后台定时任务全部冻结,直到数小时后的‘维护窗口’……

更不用说国内各大厂商 ROM 那些‘激进到丧心病狂’的智能省电策略: 只要你退出应用,不出 5 分钟,后台进程连骨灰都被系统扬了。如果不开启电池优化白名单,你的自动轮换工具就会在手机锁屏的第一个夜晚,走得非常安详,且永不瞑目 🌚。


3. 为什么非要把用户‘踹’进系统设置里,不能自己在后台搞定?

“这是被用户吐槽最多、也是开发者最委屈的地方:

‘你弹个开关我自己点一下不行吗?为什么非要打开系统设置那个九曲十八弯的页面,让我自己去找【电池】-【应用管理】-【无限制】?你是不是在故意折磨我?!’

真相是:Google 和厂商根本不给第三方 App 任何‘自己偷偷关掉’的 API!

  • 系统为了防流氓,严格禁止应用静默加入白名单。
  • 官方虽然提供过一个直接弹窗申请忽略电池优化的接口(REQUEST_IGNORE_BATTERY_OPTIMIZATIONS),但 Google Play 和各类商店对这个权限严防死守——非闹钟、通讯或 VPN 应用一旦敢声明,下架警告直接糊脸!
  • 至于各家厂商的‘自启动管理’、‘后台无限制’、‘关联启动’,入口藏得一个比一个深,不仅没有统一 API,每升一个大版本还要把设置路径改个底朝天!

于是,你就会在 App 里看到那个令人哭笑不得的‘传统艺能’:

本 App 像个卑微的旅游团导游一样,小心翼翼地弹个指引:‘亲,接下来的几秒钟,我会把你传送到系统的神秘设置迷宫里,请顺着指示找到那个开关把它打开,然后再按返回键回来……相信我,我真的不是故意绑架你的!’ 🌝”

预览现在设置的壁纸,和调整壁纸滚不滚动,从哪里滚动,锁屏要不要单独指定的设置。本来应该是这样的。

翻车现场:在截宣传图的那一秒,我亲手抓包了自己的 Bug 🌚

“原本这一段的标题应该叫《丝滑的构图微调与所见即所得预览》。

我甚至都已经打开了手机投屏,准备优雅地截几张‘微调构图、完美呈现’的高清动图放进博文当排面。

然后,翻车来得猝不及防——

我盯着手机屏幕和 App 预览,陷入了长久的沉默:

  1. 这预览图怎么对不上?! App 里的预览窗口老老实实地在正中间‘居中裁剪’;但切到桌面一看——主流的 AOSP / 类原生启动器(Launcher3 / Pixel Launcher )默认行为根本不是居中,而是从最左侧第一屏(Page 0)开始贴边排布!
  2. 薛定谔的微调反馈: 有时候你在界面里拖动微调了半天,应用到桌面后却感觉‘它好像变了,又好像什么都没变’……

属于是:宣传片还没剪完,制片人先在首映礼上踩到了自己的鞋带,当场摔了个狗吃屎。🌚


为什么做个‘所见即所得’的桌面预览这么难?

很多用 Pixel 的朋友可能觉得:‘这有什么难的?Pixel 系统自带的【壁纸与样式】,不就能实时预览桌面时钟、App 图标叠在壁纸上的真实效果吗?’

我当初也是这么想的:我也要抄这个!我也要在预览界面把用户的桌面图标和时钟无缝叠上去,做个 1:1 的即时预览!

于是我兴冲冲地去翻 Android 文档,和 AI 讨论许久,然后灰头土脸地退了出来:

  1. Google 的特权阶级(系统级后门): Pixel 自带的“壁纸与样式”之所以能做到即时预览桌面图标和网格,是因为它是系统特权级应用(Priv-App),通过私有 IPC 和 Pixel Launcher 穿同一条裤子,能直接读到启动器的桌面布局数据。
  2. 第三方应用的“二等公民”待遇: 作为一个普通权限的第三方 App,你连用户桌面上装了几个图标、时钟挂在哪都无权访问。想拿到桌面实时渲染图?除非申请录屏权限(用户直接把你当流氓卸载)!不同厂商的 Launcher 是从左往右滚、还是居中视差滚? 在外部第三方 App 看来,这完全是一个黑盒随机盲盒。

既然拿不到系统特权,桌面启动器的滚动逻辑又千奇百怪,我只能老老实实收起‘1:1 像素级复刻 Pixel 桌面’的野心。

至于截宣传图时抓包到的‘预览居中 vs 桌面靠左’的偏差 Bug……

没关系,只要我先在博文里大方认领,这就不是 Bug,这叫‘v1.0 版本的已知设计特性(Known Limitations)’!下个版本修,下个版本一定修!一定修!🌝”

从小部件,快捷方式或者快速设置功能块上更换壁纸。

我一度怀疑自己是不是得了某种‘开关强迫症’ 🌞

下拉状态栏做了一个快捷磁贴(Tile),长按图标做了一个快捷方式(Shortcut),桌面上又塞了一个小部件(Widget)……

‘这不全是同一个【立即更换】吗?我是不是套娃套魔怔了?’

直到某天我在 Lawnchair / Nova Launcher 里翻手势设置时,突然灵光一闪: 很多朋友天天想要‘双击桌面切壁纸’,但如果真在 App 里写个手势监听,那得开无障碍服务,分分钟被当成流氓软件。 但如果我把这个快捷方式丢给启动器呢?

打开你的第三方桌面设置,找到【手势操作】,把【双击空白区域】直接绑定到这个快捷方式上。

接下来,奇迹发生了—— 你不用给我任何流氓权限,也不用担心后台偷跑电量;只要在桌面上像敲门一样‘哒哒’双击两下,一张全新的壁纸应声而换。

这就是 Android 开放生态的美妙之处:你以为我在无脑做快捷方式,其实我早就把双击切图的外包合同,悄悄签给你的启动器了。🌝

通过简单密码或 OpenPGP(如果装了 OpenKeyChain 的话)备份你的配置,在另一部手机上还原。

导出备份需要这么麻烦么 🌝

在写到备份导出功能时,有人曾真诚地问我(没有,其实是我扪心自问): ‘我一个壁纸切换器,做个配置备份,至于把 AES-256-GCM 和 OpenPGP 全给焊上去吗?

“我也以为它只是个壁纸配置,直到我打开导出的数据看了一眼——

这里面躺着:

  1. 你家里私有 NAS 的公网/内网域名与端口;
  2. 你的自建 Immich 实例的 API Key;
  3. 你自定义 HTTP API 里的 Authorization Bearer Token 甚至私有密码!

这哪里是壁纸配置?这分明是你家私有云服务器的‘全套家底钥匙’!

如果我不做加密,直接明文甩一个 .json 到手机的 Download 目录:

  • 任何拿到存储权限的第三方流氓 App 都能顺手读走;
  • 你把它顺手传到百度网盘、Google Drive 或者微信聊天记录里当备份,你的私有云 API Key 就相当于直接在互联网上裸奔!
  • 拿到你 Immich API Key 的人,不仅能换你的壁纸,还能把你全家几万张私密照片、GPS 定位轨迹、人脸相册看个精光!(如果你 API Key 的权限给的太宽的话🌚)

为什么是“简单加密 (AES-256) + OpenPGP”双通道?

给不同人群的最体面选择:

  1. 给普通用户的护城河:密码加密
    • 随手输个常用密码,基于标准 AES-256-GCM 带认证加密,既防篡改又防偷窥。丢去云盘备份高枕无忧,换机时输密码一键恢复。
  2. 给极端极客的浪漫终局:OpenPGP (OpenKeyChain 联动)
    • 既然要折腾,就折腾到极致。
    • 很多 Linux / GPG 老炮和隐私偏执狂甚至连在手机屏幕上敲对称密码都觉得不安全,他们手机里装着 OpenKeychain,甚至随身带着 YubiKey 。🔒
    • 直接调起 OpenPGP 意图,用自己的公钥把备份加密成 .gpg / .asc。
    • 支持 OpenPGP 确实有 50% 是为了安全,剩下的 50%…… 主要是当我掏出 OpenKeychain 用公钥给壁纸配置文件盖戳的那一瞬间,真的很酷,好吗?!🌝🔒

我该在哪里下载呢?

https://github.com/foobarz076/Wallpaper-Picker-Android/releases/tag/v1.0.0

  • 虽然这是首个标记为 Release 的正式构建版本,但它依旧是一个初出茅庐的早期项目。由于 Android 各家厂商定制系统的行为千奇百怪,我们完全无法保证其在所有设备上的绝对稳定性 🌚。
  • 如果遇到切壁纸翻车、后台定时不灵或莫名其妙的玄学 Bug,欢迎提交 Issue 反馈,感谢您的包容与当小白鼠的勇气!
  • 另外,如果闲得蛋疼的你和我一样,想把这个 App 装在一部 TouchWiz 6.0 的三星手机上。来自三星的神秘机制和 Android 6.0 并不支持分开设置壁纸的双重夹击会让壁纸不能反映到锁屏上 🌚。首次使用前需手动操作一次:进入系统壁纸设置,选择任意动态壁纸并选择”同时应用到主屏和锁屏”。完成后,本 App 的壁纸设置即可正常同步到锁屏。

键盘敲到这里,我抬头看了一眼编辑器下方的统计数据—— 好家伙,不知不觉已经逼近 8000 字、预估阅读时间超过 30 分钟了!

摸摸我残存的头发,也为了大家的视力和手机电量着想,正片部分今天就先在此优雅刹车!🌝

至于那些在写代码过程中诞生的暴论、商业挣扎、翻车大饼与离谱巧合……我决定毫无保留地留到下一期杂谈篇。


🎬 下期高能预告看点:

  1. 为什么坚决不支持 Google 相册?请欣赏 Google 是如何自废武功的 🌚
    • 砍死旧版只读 API、强推交互式 Picker(总不能为了半夜静默换个壁纸,把你喊起来点确定弹窗吧?);
    • 7 天授权蒸发 + 60 分钟直链暴毙 + CASA Tier 2 天价审计;
    • 揭秘项目里那篇态度极其决绝的 ADR-0002:“凡是提 Issue 要求接 Google 相册的,一律甩出此文并无情 Close。”
  2. 比画饼更离谱的“种小麦”之一:有没有机会跨平台?🌾
    • 首先无情开除 iOS 籍(苹果爹对壁纸自动化有多防备?);
    • 拯救 Linux 桌面下“横屏写代码 + 竖屏被砍头”的受害者程序员;
    • KMP 与 Compose Multiplatform 桌面端的“非分之想”。
  3. 种小麦之二与商业模式反思:现在做这么多是不是亏了?未来想收两块钱会不会被喷?💸
    • 一口气在免费版送出所有王牌功能后的独立开发者深夜反思;
    • 曾几何时,我也幻想过接云端 AI 生图接口卖积分……直到我被国内算法备案、手机实名制、鉴黄机审二次过路费和算力倒贴账单吓得火速认怂、当场把坑填平 🌚;
    • 基于 GPLv3 的开源项目,怎么体面地“站着把冰美式钱给挣了”?
  4. PLANS.md 里的星辰大海 🥐
    • 屠龙勇士终成恶龙:从当年写 Tasker 脚本眼瞎,到未来做 Tasker 插件联动全屋智能灯光;
    • 致敬 Android 经典神器 Tapet:纯数学公式与几何算法生成壁纸(断网也能无限自愈)。
  5. 关于那个敷衍的名字与包名:我发誓真没有钓 Google 的鱼 🎣
    • 从 ID foobarz076 到域名 barz.foo 的宿命感;
    • 那个长得像 Hello World Demo 的包名 foo.barz.wallpaperpicker,究竟是怎么差点撞衫 Google 传奇招聘系统的黑色幽默?

最后祝你身体健康,再见

1 个赞