☰
按文件名自动归类蓝牙接收文件:安卓FileObserver工具实现
2026/10/14 18:20:19 网站建设 项目流程

你有没有遇到过这种情况:手机蓝牙接收文件,不管是什么类型,通通塞进内部存储的Bluetooth文件夹。图片、PDF、压缩包、安装包混在一起,时间一长,那个目录乱到我不想打开。我一直在想,能不能根据文件名,自动把蓝牙接收的文件放到不同地址?比如看到合同两个字就进 Documents 的合同目录,看到IMG或.jpg就进相册目录,看到.apk就进安装包目录。

这篇文章要说的,就是我自己做的一个自用安卓工具:监听蓝牙接收目录里新增的文件,读取文件名,按规则自动归类到对应文件夹。从需求拆解、方案选型,到具体代码和踩坑记录,都摊开讲。解决的核心问题,就是"根据文件名称修改安卓默认的蓝牙接收文件地址"——不是直接改系统设置,而是在文件落地的瞬间,让它在实际存储位置上按你的规则走。适合有安卓开发基础、或者玩过 Tasker / 自动化工具的朋友参考,零基础也能看懂思路。

1. 需求拆解:为什么要按文件名自动分配蓝牙接收目录

1.1 蓝牙接收的默认路径与"地址"到底指什么

安卓系统自带的蓝牙文件接收功能,默认把文件放在/storage/emulated/0/Bluetooth这个目录,也就是手机内部存储根目录下的Bluetooth文件夹。不同厂商可能会小改,比如在内部存储里建一个蓝牙或者bluetooth目录,但大差不差。

"默认的蓝牙接收文件地址"如果从系统层面去改,基本只能靠 root 后替换系统文件,或者修改系统蓝牙协议栈的上层逻辑。这对绝大多数人来说不现实,风险也高。所以我换了一个思路:系统默认仍然往Bluetooth目录写,但我做一个后台监听服务,文件写完的瞬间把它移动到目标目录。从使用者的角度看,文件最终保存在哪里,完全由文件名规则决定,等于"曲线修改"了默认接收地址。

1.2 按文件名归类的典型场景

我自己的实际场景大概有这几类:

  • 图片与视频:蓝牙从别人手机传过来的照片,命名可能是IMG_20241001.jpg或VID_20241001.mp4,希望自动进入Pictures/蓝牙相册,省得再打开相册扫描、手动移。
  • 工作文档:对方发来xx合同_终版.docx、周报.pdf,希望进入Documents/工作资料,方便后续按项目找。
  • 安装包:朋友发来一个.apk,直接进Download/应用安装包,因为有些 APK 我只是临时存一下,不打算装。
  • 压缩包:项目素材.zip进入Download/压缩包,然后在电脑上统一解压整理。
  • 特殊格式:.epub想直接进书架目录,.gpx轨迹文件想进运动设备目录。

这些场景的共同点:只看文件名就能确定该去哪。不需要打开文件内容,不需要人工干预。规则可以用后缀匹配,也可以包含关键词,甚至用正则表达式。我的工具核心就是做这件事。

2. 可行方案选型:应用层监听 vs 系统级修改

2.1 系统级修改的局限

先说说为什么不做系统级修改。网上有一些教程,通过替换/system/etc/bluetooth/下的配置文件,或修改蓝牙共享进程的源码来改默认路径。这需要 root、需要关闭系统签名校验,还要适配不同安卓版本。一旦系统更新,改动可能被覆盖。另一个问题是风险:改坏了蓝牙文件接收功能,连普通传输都会受影响。所以不是我没考虑过,而是对于"根据文件名分类"这个需求,系统级方案太重,而且系统默认路径是一个全局设置,做不到"按文件名区分目标文件夹"。

2.2 应用层监听:FileObserver 方案

最自然的选择是 FileObserver。这是安卓自带的一个文件系统监听类,可以监听某个目录的增删改事件。我把监听目标指向蓝牙接收目录,当新文件出现时,立刻触发回调,拿到文件名,然后移动到规则指定的目录。

这个方案的优点是轻量、不用 root、代码量少。缺点是安卓 10 以后强化了分区存储,对公共目录的访问限制变严。为了直接监听/storage/emulated/0/Bluetooth并移动文件,应用需要申请"所有文件访问权限"(MANAGE_EXTERNAL_STORAGE)。好在对于工具类应用,Google Play 和国内商店都有对应的权限申请渠道,个人自用更是没问题。

2.3 辅助功能方案与兼容性

还有一个方案是监听通知栏。蓝牙文件接收完成后,系统通常会弹一条通知,应用通过无障碍服务读取通知内容,获取文件名和路径,再执行移动。这个方案的好处是不需要申请"所有文件访问权限",但对通知权限和无障碍服务权限要求更敏感,而且各家 ROM 对无障碍服务的后台存活限制很严。实测下来,权限更容易被用户收回。

我的最终选择是 FileObserver 为主。原因很简单:直接监听目录,事件可靠,不依赖弹通知,移动逻辑也好写。对分区存储的权限问题,用 MANAGE_EXTERNAL_STORAGE 解决。再配合一个简单的服务保证后台运行。

3. 核心实现:监听蓝牙接收目录并按文件名移动

3.1 准备工作与权限声明

项目基于 Kotlin,最低版本可以放到 Android 8.0,目标版本建议 Android 13 或 14。需要声明这些权限:

  • RECEIVE_BOOT_COMPLETED:开机自启服务。
  • FOREGROUND_SERVICE:Android 9 以后前台服务权限。
  • FOREGROUND_SERVICE_DATA_SYNC:Android 14 对前台服务类型的要求。
  • POST_NOTIFICATIONS:Android 13 以后要弹通知才能显示前台服务。
  • READ_EXTERNAL_STORAGE/WRITE_EXTERNAL_STORAGE:Android 12 以下兼容旧支持库时可能需要。
  • MANAGE_EXTERNAL_STORAGE:核心权限,拥有所有文件访问能力。

Manifest 里大致这么写:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

MANAGE_EXTERNAL_STORAGE需要在代码里跳转到系统设置页让用户授权,不能只在 Manifest 里声明。Android 11 以上的授权路径是Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,我需要传入应用包名。如果跳转失败(某些 ROM 改了路径),可以先打开应用详情页再找"所有文件访问"。这一步要处理异常,不然用户会以为按钮没反应。

3.2 使用 FileObserver 监听目录

FileObserver 的构造函数可以传路径和事件掩码。我监听的事件包括CREATE、MOVED_TO、CLOSE_WRITE。蓝牙文件传输完成后,通常会先创建临时文件,再重命名为最终文件名,或者直接在目录里写入。只监听CREATE可能拿到半成品;监听CLOSE_WRITE才能确保文件写入完成。这里有个细节:文件很大的时候,从CREATE到CLOSE_WRITE可能间隔几十秒甚至几分钟,不能一看到CREATE就去移动,否则文件内容不完整。

我写了一个BluetoothDirObserver类,继承 FileObserver:

class BluetoothDirObserver( private val path: String, private val onFileCreated: (String) -> Unit ) : FileObserver(path, CREATE or MOVED_TO or CLOSE_WRITE) { override fun onEvent(event: Int, filePath: String?) { if (filePath == null) return val targetEvent = (event and FileObserver.CLOSE_WRITE) != 0 if (targetEvent) { onFileCreated(filePath) } } }

这里需要注意,event是一个位掩码,不能直接== FileObserver.CLOSE_WRITE,而是用与运算判断对应位是否置一。我一开始就是用直接比较,结果发现很多时候收不到事件,后来翻了源码才明白。

另一个坑:如果只监听目录,收到的是相对目录的文件名,不是完整路径。组合时要自己补全:

val fullPath = File(observerPath, filePath).absolutePath

我设计的回调传递的是相对文件名,然后在服务层拼完整路径,这样路径逻辑都集中在一个地方。

3.3 文件名规则匹配与目标路径计算

规则是整个工具的灵魂。我定义了一个数据类:

data class MoveRule( val id: Int, val pattern: String, val targetSubPath: String, val regex: Boolean = false )

pattern支持两种写法:普通关键词,或者正则表达式。普通关键词规则里,文件名包含该关键词就命中;正则规则,则用正则去匹配文件名。例如:

  • 关键词规则:合同->Documents/合同
  • 后缀规则:.jpg或.jpeg->Pictures/蓝牙相册
  • 正则规则:^.*IMG_[0-9]{8}.*$->Pictures/蓝牙相册
  • 正则规则:^.*\.apk$->Download/应用安装包

匹配顺序很重要。我按规则的优先级从高到低排列,第一条命中后就停止。为什么要这样?因为一个文件名可能同时匹配多个规则。比如公司合同备份_IMG_20241001.zip,如果先匹配图片规则,就会跑错地方。所以我约定:具体关键词规则优先级高于宽泛后缀规则。在代码里,规则列表带priority字段,匹配前先排序。

目录计算方面,移动的目标根目录默认是/storage/emulated/0/,后面拼接targetSubPath。如果目标目录不存在,就自动创建。遇到特殊字符、多级目录,比如Documents/合同/2024年,用File.mkdirs()就能一次创建。

3.4 移动文件与处理冲突

移动文件直接用File.renameTo()是效率最高的。但跨存储目录时可能出现问题,也就是从/storage/emulated/0/Bluetooth移到/storage/emulated/0/Pictures,这两个目录在同一个存储分区上,通常没问题。如果手机有外置 SD 卡,且目标目录在 SD 卡,renameTo就不一定可靠,需要改用流复制加删除。

为了稳妥,我封装了一个FileMover:

fun move(source: File, target: File): Boolean { if (target.exists()) { target.delete() } target.parentFile?.mkdirs() return if (target.isDirectory && !isSameStorage(source, target)) { copyAndDelete(source, target) } else { source.renameTo(target) } }

文件名冲突我直接覆盖。蓝牙传输来的文件,如果目标位置已经存在同名文件,新文件覆盖旧文件比较合理,因为蓝牙传文件的场景,重复传输意味着你要的是最新版。如果不想覆盖,可以在重名前加数字后缀,比如合同(1).pdf。我选择覆盖,把选择权留给用户,在设置里加了一个开关。

移动完成之后,我还记录一条日志,保存到应用私有目录下。内容包括:原文件名、源路径、目标路径、匹配到的规则、移动时间。这样以后想排查"文件去哪了"就翻日志。

4. 实操过程:从搭建项目到实测生效

4.1 前台服务与事件分发

文件监听不能放在普通 Activity 里,否则应用一退到后台,监听就断了。我写了一个MoveService,在onStartCommand里开启监听:

class MoveService : Service() { private lateinit var observer: BluetoothDirObserver override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { startForeground(NOTIFICATION_ID, buildNotification()) if (::observer.isInitialized) { observer.startWatching() } else { observer = BluetoothDirObserver(BLUETOOTH_DIR) { fileName -> handleFile(fileName) } observer.startWatching() } return START_STICKY } }

START_STICKY很重要。如果服务被系统杀死,系统会尝试重启服务,但它不会记住 Intent,可能传null。所以要判空。我在onStartCommand里做了空安全处理,只要服务重启,就重新绑定监听。

前台服务的通知要写得让用户知道"在监听蓝牙目录",但不要频繁刷新。我用了一个静态通知,只有移动文件成功时才更新通知内容,减少耗电。Android 14 上startForeground必须指定服务类型,否则会崩。所以我加了FOREGROUND_SERVICE_TYPE_DATA_SYNC。

4.2 开机自启与快捷开关

开机自启是通过静态广播接收器实现的。清单里声明:

<receiver android:name=".BootReceiver" android:exported="true"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver>

在BootReceiver里再启动MoveService。但国产 ROM 开机自启的权限管理很严格,用户需要在手机管家应用里把"自启动"设为允许,否则广播收不到。这个我在应用引导页里有提示。

我还给 MainActivity 加了一个开关,用SwitchPreference控制服务启停。每次启动时检查是否已经授权"所有文件访问权限",没授权就跳到设置页。授权回来后,重新启动服务。

4.3 实测效果:从同事手机传一个 PDF

我用一台 Android 13 手机做了实测。前置条件:应用已授权所有文件访问权限,已开启前台服务,规则里配置了.*\.pdf$到Documents/PDF。

测试过程:

  1. 用另一台手机通过蓝牙发送一个名为项目需求文档.pdf的文件。
  2. 接收方手机会先弹蓝牙接收通知,系统默认存到/storage/emulated/0/Bluetooth。
  3. 几秒钟后,我打开文件管理器,发现Bluetooth目录里已经没有这个 PDF,而/storage/emulated/0/Documents/PDF下出现了项目需求文档.pdf。
  4. 查看应用日志,显示匹配到的规则是"PDF正则规则",耗时 320ms。

整个过程没有手动操作。看起来就像是"蓝牙默认保存地址变成了 Documents/PDF"。

我又测试了没有配置规则的场景。比如传一个.txt文件,没有规则命中,文件会留在Bluetooth目录,应用不做任何处理。这个设计是有意的,避免误移动。

4.4 关键参数与规则配置建议

在配置文件里,我维护了一个 JSON 格式的规则列表:

[ { "priority": 80, "pattern": "合同|协议|报价", "targetSubPath": "Documents/工作资料", "regex": false }, { "priority": 70, "pattern": "^.*\\.(jpg|jpeg|png|gif)$", "targetSubPath": "Pictures/蓝牙相册", "regex": true }, { "priority": 60, "pattern": "^.*\\.(mp4|mov)$", "targetSubPath": "Movies/蓝牙视频", "regex": true }, { "priority": 50, "pattern": "^.*\\.apk$", "targetSubPath": "Download/应用安装包", "regex": true }, { "priority": 40, "pattern": "^\\(.*\\.(zip|rar|7z)$", "targetSubPath": "Download/压缩包", "regex": true } ]

注意正则里的中文括号转义问题,我写规则时一般用英文半角括号。实际配置里,中文关键词规则用contains判断更直观,正则规则捕获.*开头来兼容厂商可能加的前缀。比如有些手机蓝牙接收文件名前面会多一个(已接收)之类的标记,正则开头就匹配不上,所以统一写成^.*开头。

规则优先级的值越大越先匹配。配置完要重启服务,让新规则生效。我在 MainActivity 里提供了"保存并重启服务"按钮,点击后停止服务、清理旧 observer、再启动新 observer。

5. 常见问题与排查技巧实录

5.1 文件事件收不到怎么办

我最早在 Android 11 上测试时,FileObserver 完全不触发。后来定位到两个原因:一是没有申请MANAGE_EXTERNAL_STORAGE,系统限制了应用对公共目录的访问,FileObserver也无能为力;二是监听目录路径写错了,厂商把蓝牙目录改成了/storage/emulated/0/蓝牙而不是Bluetooth。

排查方法很简单:先在应用里写一个"测试目录"按钮,用File.exists检查你监听的路径是否存在。如果路径不存在,遍历根目录下所有文件夹,把包含bluetooth或蓝牙的目录名打印出来。另外,确认权限是否真的授权成功,可以在设置里搜索"所有文件访问",找到你的应用,看开关是否打开。

5.2 文件传输中就被移动,导致文件损坏

这是我最开始踩过的大坑。只监听CREATE事件,收到回调就去移动,结果文件还在写入,移动过去的文件只有几百 KB,实际内容不完整。

解决方式是等CLOSE_WRITE事件。这个事件表示文件已经关闭写入,内容完整。但要注意,有些蓝牙协议在传输完成后不是关闭文件描述符,而是重命名临时文件。比如文件名从xxx.tmp变成xxx.pdf,这时会触发MOVED_TO事件。我是CLOSE_WRITE和MOVED_TO一起监听,但过滤掉.tmp结尾的文件。如果以后遇到.part、.downloading这类临时后缀,也要统一处理。

另一个保险措施是:移动前检查文件大小,如果大小等于 0 就跳过,等下一次事件触发。文件很大的话,还可以加一个延迟:收到事件后等待 500 毫秒再移动,让文件系统做收尾刷新。实测下来,等待 500 毫秒的失败率比立即移动低很多。

5.3 目标目录出现同名文件,新文件消失

移动逻辑里,如果目标文件已存在,我最初用的方案是renameTo覆盖,但这个操作并不总是可靠。在部分文件系统上,renameTo遇到已存在目标文件会返回 false,而且不报异常,导致新文件留在原目录或者直接从源目录消失。

后来我改成三步走:先尝试renameTo覆盖;失败则用FileOutputStream写入目标,临时文件命名为目标文件名.tmp,写入完成后重命名;最后删除源文件。这样即使主路径失败,也有后备逻辑。如果希望保留多个版本,而不是覆盖,就在目标文件名后拼时间戳,比如合同_20241001.pdf,但这种操作会导致文件名变化,我在设置里默认关闭。

5.4 规则误判把文件移错地方

关键词规则误判很常见。我有一次配置了"测试"关键词,结果任何包含"测试"的图片都被移到了文档目录。解决方案就是提高规则的精准度:能用后缀就不要用关键词,能用正则就不要用 contains。比如图片规则用^.*\.(jpg|jpeg)$就比用图片关键词可靠得多。

误判之后文件路径变了,怎么恢复?我在设置页加了一个"最近移动记录"列表,每条记录都显示源路径和目标路径。用户可以直接复制目标路径去文件管理器里找。这个功能我建议一定要做,否则规则配错了,用户连文件去哪了都不知道,体验会很差。

5.5 后台服务总被系统杀死

国产 ROM 对后台服务限制非常严格。我的经验是:尽可能提高服务重要性。启动前台服务、在通知栏常驻已经是最基本的要求,但仍然会被 MIUI、ColorOS 杀。要想稳定,可以引导用户把应用拉到"后台无限制"或"允许自启动"的白名单里。

部分高版本 ROM 上,即使用前台服务,超过一定时间也可能被冻结。一个可以接受的替代方案是:在应用内使用JobScheduler周期性扫描Bluetooth目录,发现新文件就补移动。虽然实时性差一点,但胜在稳定。我把这个作为兜底策略,默认每 15 分钟扫描一次,如果 FileObserver 活着就跳过扫描。

6. 更多玩法与实际使用心得

6.1 把规则扩展到通知栏分享

蓝牙只是我整理的入口之一。现在我让这个工具也能监听"分享到本机"的文件,也就是第三方应用通过 SAF 保存文件时,我可以用一个快捷方式接收并执行相同规则。思路是在应用中注册一个文件接收 Activity,接收ACTION_SEND的 intent,读取 Uri,复制到目标目录。这样不仅蓝牙文件,微信、钉钉收到的文件也能按文件名整理。

6.2 按日期自动建子目录

我发现按文件名规则匹配之外,日期也是一个很好的维度。现在我的规则目标地址支持占位符,比如Documents/工作资料/{yyyyMM},移动文件时会自动替换成当前年月。这样每个月的文件都自动分在一个文件夹,整理起来很清爽。代码里用SimpleDateFormat格式化日期即可。

6.3 移动失败的重试队列

文件被占用、权限瞬时失效,偶尔会发生。我增加了一个简单的重试队列:移动失败的文件路径放在内存里的ArrayDeque中,由Handler每 10 秒重试一次,最多重试 3 次。三次都失败,就把错误日志写到应用私有目录,并发送通知提示用户。这个重试机制成本很低,但实际使用中救回了不少文件。

6.4 我的一点实际使用体会

用了快半年,最大的感受是:这种工具的价值不在代码多复杂,而在规则设计是否符合自己的使用习惯。刚开始我把规则写得很复杂,输出结果反而不理想。后来删掉大部分正则,只保留"后缀匹配 + 少数高频关键词",整理效果反而最好。蓝牙接收文件本质上是一个低频动作,不需要追求极致的自动化,稳定不丢文件才是第一位的。

如果你也想做类似的东西,建议先把你手机当前的蓝牙目录翻一遍,统计一下常见文件名,再设计规则。规则不要一上来想覆盖所有场景,先把最频繁的三类文件搞定,后面再慢慢加。这样既容易测试,也不会因为误判而失去信心。我到现在还把"移动到哪个目录"这个选择按钮放在通知栏快捷面板里,偶尔手误也可以手动干预。这个工具的整体思路可以复制到其他"根据文件名自动归类"的场景,值得琢磨。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询