Skip to content

About

Android 改名助手: 接收分享的文件 → 改名(扩展名默认锁定)→ 再分享。无网络、无系统权限、无悬浮窗。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

hellorename

分享 → 改名 → 再分享。 一个极简的 Android 小工具:接收其他 App 分享过来的文件,改好名字后,再把文件分享给别的 App。

中文系统下,应用名显示为 改名助手(英文/其他语系显示 HelloRename)。

QA Release APK License: MIT minSdk


它解决什么问题

Android 系统分享面板本身没有“改名后再分享”的能力:接收方拿到的 content:// URI 是临时且不可转发的授权,而且没有任何 API 能改别人的文件名(改名本质上是换个名字重新提供同一份数据)。

HelloRename 就是干这件事的:

  1. 出现在系统分享面板里,接收任意类型的文件(ACTION_SEND / ACTION_SEND_MULTIPLE)。
  2. 上半部分显示旧文件名.扩展名(可长按复制)。
  3. 下半部分是一个自动换行的大输入框,直接改新文件名。
  4. 扩展名默认锁定,且不显示在输入框里(连灰色后缀也不放,由输入框下方的提示行写明保留的扩展名);勾选最底部的“允许修改扩展名”后,扩展名才拼进输入框一起编辑。
  5. 输入框下方并排两个按钮:发送(丢进系统分享面板再分享)和保存到下载(Android 10+ 走 MediaStore 直写、Android 7–9 走 SAF 选择器,同样零权限)。
  6. 发送或保存成功后 Toast 提示并自动关闭主界面,避免用户在分享面板里又把文件选回本应用。
  7. 底部固定一条红色警示语,提醒发送后在分享面板选择其它应用接收、勿再选本应用,防止无限循环。
  8. 跟随系统的深色 / 浅色模式:只保留白底黑字、黑底白字两套默认配色;顶部 56dp 应用栏与页面同色,不提供自定义颜色入口。
  9. 切换深浅色/字体/语言不丢状态:Activity 重建后恢复已选文件、输入框内容、扩展名锁定和复制进度;从桌面重进不会清空当前文件。
  10. 分享面板不再列出本应用:除了底部警示语,chooser 还通过 EXTRA_EXCLUDE_COMPONENTS 排除 HelloRename,进一步避免自循环。
  11. 文件校验值:复制完成后对缓存文件做一次只读回读,显示 MD5 / SHA-1 / SHA-256,支持逐行选择或一键复制全部,方便和源文件对比。

设计取舍(为什么这么实现)

决策 原因
标准前台 Activity,不用悬浮窗、不占通知、不申请任何系统权限 悬浮窗在国产 ROM 上要额外授权、易被拦截;SYSTEM_ALERT_WINDOW 权限一旦被回收功能就废了。前台 Activity 走系统分享入口,行为最可预期。
收到文件后立刻复制到 cacheDir/shared/<会话>/_incoming.<ext> 源 URI 的读授权是一次性的、不可传递的,且源 App 随时可能被系统杀掉。越早复制越可靠,之后改名、分享都不再依赖源 App。
改名时只是同目录 rename,不再复制第二份 一份数据从头到尾只存在一份:_incoming.png → 我的新名字.png 是同一文件系统内的瞬时操作,零字节拷贝。
用自己的 FileProvider 对外提供文件 cacheDir/shared/<会话>/<新文件名> 的真实磁盘文件名就是对外报出的 DISPLAY_NAME,不需要写自定义 ContentProvider 去伪造名字。
锁定时完全隐藏扩展名,勾选后才把扩展名拼进输入框(全程不用 TextWatcher 硬顶) 未勾选时输入框里只有“基础名”,改名焦点完全放在名字上;保留的扩展名由下方提示行写明(锁定为 .png)。用户仍可以在名字里随便用点号(my.file.v2 不会被误切),勾选后扩展名变成普通可编辑文本。
保存到下载:10+ 走 MediaStore、7–9 走 SAF,全程零权限 写自己新建进 Downloads 的文件在 scoped storage 下不需要任何权限,保住“零权限”卖点;老系统退回系统选择器,只多一次确认,也不用申请 WRITE_EXTERNAL_STORAGE(那会让 QA 体检的权限门禁失败)。
只有能读到文件大小时才走 FileChannel.transferTo,并按字节数校验;拿不到大小(pipe 型 Provider)或校验不符就退回 64 KiB 流复制 transferTo 允许“少传”甚至返回 0,只有在 statSize 可校验时才敢用零拷贝;pipe 型 Provider 走流复制,两条路都不会把不完整的文件当成成功。
启动分享面板之前先写交付标记,写失败就不分享 旧实现先弹面板再写标记:进程在中间被杀(或磁盘已满)时,目标 App 拿到的 URI 没有跨进程保护,下一次清理就会把它变成死链。写不进去时宁可不发,也不把死链交给对方。代价是用户在分享面板里取消时,这份缓存仍会按已交付保温 24 小时。
未知大小的流最多复制 2GB,并且不超过“可写空间 - 动态预留” 导出的 Activity 可以被任意 Provider 触发;没有上限的 pipe 流可以把设备存储填满。已知大小仍按实际大小预检,未知大小才用这个兜底上限。

会不会中途多留一份副本?

会,只留这一份:源文件在对方 App 里(或对方进程的内存/管道里),你不可能原地改名,也不可能把临时授权转给下一个 App,所以复制到自己的缓存是唯一稳妥的做法。

理论上还有“不复制”的方案——写一个自定义 ContentProvider,query() 报新名字、openFile() 现场把源 URI 的数据流转发出去。但它依赖源 App 的进程一直活着、授权一直有效,在国产 ROM 的后台清理下非常容易失败;而且分享面板弹出后你的 Activity 可能已经退栈,失败就发生在用户看不到的地方。所以本项目不做这个方案。

缓存文件放在 cacheDir/shared/<会话>/,系统可以回收;App 自己也会清理,规则按“有没有交出去”分成两种:

会话状态 什么时候删 为什么
未交付(取消、直接杀进程、复制失败) 用户离开界面 30 分钟后才开始按 10 分钟 TTL 计算 没有任何第三方握有它的 URI,留着只会白占空间;30 分钟宽限让用户切出去再回来还能继续用,超时后 10 分钟 TTL 负责收尾
已交付(发送过 / 保存过) 从交付时刻起 24 小时 对方的 content:// URI 在它自己的任务存活期间一直有效,可能还在异步上传,用户也可能稍后再次转发

清理全程在 IO 线程(老版本在主线程同步删,攒几个大文件就会卡冷启动),并且绝不会碰当前界面正在用的会话:进程内的活会话登记 + cacheDir/shared-state/ 里的交付标记(跨进程持久化)一起把这件事钉死。交付标记用“临时文件 + fsync + rename”原子写入;清理只删除本次真正列到过、且确实已经删掉的会话标记,所以清理期间新完成的分享不会被误删。触发点是冷启动(强制)+ 复用界面时收到新分享 + 新建复制会话,后两者按 10 分钟节流。

从后台回来时,如果发现缓存文件已经被 30 分钟宽限后的清理收走,会自动用原 URI 重试复制;源 URI 授权已经过期时会明确提示“请重新分享”,不会静默变成空界面。

下载

最新的 APK 在 Releases 页面(release 变体,已用项目自己的密钥签名,可直接安装)。

版本号约定:versionName 来自 tag(去掉 v),versionCode 用 CI 的 run number(单调递增,可直接覆盖安装升级)。

使用

任意 App → 分享 → HelloRename → 改名字 → 发送 → 系统分享面板 → 目标 App
  • 从启动器直接打开时,可以点“选择一个文件”(走 SAF),方便自测。
  • 支持多文件分享,但当前版本只处理第 1 个(界面会提示收到几个)。
  • 只收到纯文本(EXTRA_TEXT)而不是文件时会明确提示,不会假装成功。

构建

云端(推荐,本仓库就是这么发的包)

  • 推到 main / 开 PR → QA workflow:单元测试 → Lint 门禁 → 构建 release APK → APK 出厂体检;同时并行跑云端模拟器 UI 验证。QA 产物带 .debug 包名后缀(moe.hellorename.debug),可以和正式包同时装在手机上。
  • 打 v* tag → Release APK workflow:先跑云端模拟器 UI 验证 → 要求签名 Secrets → 构建 → 体检 → 创建 Release(附 APK 与 SHA256SUMS.txt,说明自动取自 CHANGELOG.md)。

云端模拟器验证(发布前硬门槛)

.github/workflows/emulator-ui.yml 在 GitHub Actions 的 API 36 模拟器上执行 connectedDebugAndroidTest:

  • 主题门禁:系统 ActionBar 必须关闭,全屏只有一个应用名标题(防止出现 v1.3.0 那样上下两个标题);顶栏高度恰好 56dp,Android 15+ 强制 edge-to-edge 时由根布局让出状态栏/导航栏/IME inset;浅色模式白底黑字、深色模式黑底白字。
  • 主流程门禁(v1.3.2 起,v1.5.0 增加校验值):分享文件 → 等待复制和校验值计算 → 输入新名字 → ActivityScenario.recreate() 后输入/旧文件名/复制状态/MD5/SHA-1/SHA-256 仍在;ACTION_MAIN 重进不清空;异常 EXTRA_STREAM 不崩溃;Parcelable[] 形式仍能识别。

Release workflow 的 release job 通过 needs: emulator-verify 依赖这项验证;模拟器不通过就不会构建发布包,更不会创建 Release。测试结束后会上传测试报告、浅色 / 深色截图与 UI 层级作为 Actions artifact,方便人工复核。

出厂前自动体检(tools/qa_check.py)

每次 CI 都会对产出的 APK 做一遍硬性检查,任何一项失败就不发版:

  • 包名 moe.hellorename、版本号与 tag 一致、minSdk 24、targetSdk 36
  • 除了 androidx 自用的 DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION(signature 级、不向外部应用授权)外没有任何权限,一旦多出 INTERNET/存储之类就失败
  • application-debuggable 不存在
  • MainActivity exported=true,注册了 MAIN/LAUNCHER、SEND、SEND_MULTIPLE,launchMode=singleTop
  • FileProvider exported=false + grantUriPermissions=true + authority 与 file_paths.xml 到位
  • APK Signature v2 / v3 校验通过,且证书 SHA-256 等于发布密钥(不是 Android Debug 证书)
  • APK 内不得残留 *.jks / keystore.properties / local.properties

本地也能跑:

python3 tools/qa_check.py app/build/outputs/apk/release/app-release.apk \
  --expect-cert-sha256 3682cce6d8fbf859b9b0a90ef8818acc747c82bb5f9d97418a4e6749cb6ccaca

(没装 build-tools 时会跳过依赖 aapt2/apksigner 的检查并给出 WARN。)

签名

签名密钥不在仓库里,通过 GitHub Actions Secrets 注入:

Secret 内容
SIGNING_KEYSTORE_BASE64 密钥库文件的 base64(单行)
SIGNING_STORE_PASSWORD 密钥库口令
SIGNING_KEY_ALIAS key alias
SIGNING_KEY_PASSWORD key 口令

生成密钥库:

keytool -genkeypair -v -keystore hellorename-release.jks -alias hellorename \
  -keyalg RSA -keysize 4096 -validity 10950 -storetype PKCS12 \
  -storepass '<PASS>' -keypass '<PASS>' \
  -dname "CN=hellorename, OU=Android, O=hellorename, C=CN"
base64 -w0 hellorename-release.jks   # 粘到 SIGNING_KEYSTORE_BASE64

未配置这些 Secrets 时,release 会自动退回 debug 签名,CI 依旧能出包(仅供自测)。

本地

cp keystore.properties.example keystore.properties   # 填自己的路径与口令,已被 gitignore
gradle assembleRelease                               # 或 ./gradlew assembleRelease
# 产物:app/build/outputs/apk/release/app-release.apk

环境:JDK 17、Gradle 9.6.0、AGP 9.4.1、Kotlin 2.4.21、compileSdk 36、targetSdk 36、minSdk 24。单元测试:

gradle test                    # 78 条 JVM 单测(文件名/缓存/流复制/操作代际/校验值)
gradle connectedDebugAndroidTest   # 需要模拟器或真机;API 36 门禁

隐私

  • 没有网络代码,也没有申请任何系统权限(INTERNET、存储、悬浮窗等一概没有)。APK 里只剩 androidx 自带的 moe.hellorename.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION,它是 signature 级的应用内自用权限,不会授予外部应用,也不出现在安装权限列表里。
  • 文件只在本地缓存目录里复制、改名;除了你自己选的目标 App,不会发给任何第三方。

已知限制

  • 改扩展名会同时改变对外声明的 MIME type,部分 App 会因此拒收(界面有提示)。
  • 多文件分享只处理第一个。
  • 文件名中的 / : * ? " < > | 等非法字符、C0/C1 控制字符会被替换成 _,双向控制符(bidi)会被移除,结尾的点会被去掉(避免 ../ 之类的路径穿越和文件名伪装)。
  • 新文件名按 code point + UTF-8 字节数截断(上限 240 字节,中文约 78 字,扩展名始终完整保留),因为 ext4/f2fs 的单个文件名上限是 255 字节而不是 255 个字符;emoji 不会被劈成半个。
  • 文案默认英文,中文放在 values-zh(所有中文语系都会命中)。
  • 校验值是在复制完成后对缓存文件做一次完整回读计算的,因此大文件会额外花一次读盘时间;计算期间发送/保存按钮保持禁用并显示进度。
  • 分享面板弹出后你没法知道对方何时读完文件,所以临时文件已交付的按交付时刻保温 24 小时;未交付的在离开界面 30 分钟后按 10 分钟 TTL 回收。
  • SAF 保存路径只在 Android 7–9 使用。API 36 模拟器门禁走 MediaStore;wt 回退、截断和取消清理目前靠代码审查 + API 28 人工验证,没有把 API 28 放进自动矩阵(这是 v1.4.0 的验证边界)。

License

MIT

About

Android 改名助手: 接收分享的文件 → 改名(扩展名默认锁定)→ 再分享。无网络、无系统权限、无悬浮窗。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages