---
name: android-gallery-bugfix-skill
description: Diagnose and fix Android gallery, photo-picker, MediaStore, and image-loading issues — including permissions (Android 9–14+), Scoped Storage, URI access, EXIF orientation, Glide/Coil/Picasso errors, OOM, and list/preview/save/delete failures.
license: MIT
compatibility: Android (Gradle) projects. Used for reference during openspec-android-bug investigation when the triage category is Gallery / Media.
metadata:
author: openspec
version: "1.0"
---
# Android Gallery Bugfix Skill
## 适用范围
用于 Android 项目中 Gallery / 相册 / 图片选择 / 图片浏览 / 图片保存相关问题的排查、修复和验证,包括但不限于:
- 相册打不开
- 图片列表为空
- 图片加载慢
- 缩略图显示异常
- 图片方向错误
- 图片保存失败
- 图片删除失败
- 图片扫描不到
- 图片选择失败
- Android 10+ 分区存储问题
- MediaStore 查询异常
- Glide / Coil / Picasso 加载异常
- OOM / Bitmap 内存问题
- 大图预览卡顿
- EXIF 信息异常
- 视频和图片混合列表异常
- 权限问题
---
## Bugfix 总体原则
相册类问题通常和以下因素相关:
1. 存储权限
2. Android 版本差异
3. Scoped Storage / 分区存储
4. MediaStore 查询
5. URI 权限
6. 图片解码
7. 缩略图缓存
8. EXIF 方向
9. 文件路径和 content uri 混用
10. 大图内存优化
11. 异步加载和 RecyclerView 复用
处理时必须区分:
- 文件不存在
- 没有权限
- MediaStore 未扫描
- URI 不可访问
- 解码失败
- 缩略图加载失败
- UI 绑定错误
---
## 常见问题分类
### 1. 相册列表为空
重点检查:
- 是否有 READ 权限
- Android 13+ 是否申请 READ_MEDIA_IMAGES / READ_MEDIA_VIDEO
- Android 10+ 是否适配 Scoped Storage
- MediaStore 查询条件是否过窄
- selection / sortOrder 是否错误
- 查询的是 Images 还是 Files
- 是否只查外部存储
- 是否被 MIME type 过滤掉
- 文件是否已被 MediaScanner 扫描
权限差异:
```text
Android 12 及以下:
- READ_EXTERNAL_STORAGE
Android 13+:
- READ_MEDIA_IMAGES
- READ_MEDIA_VIDEO
- READ_MEDIA_AUDIO
Android 14+:
- READ_MEDIA_VISUAL_USER_SELECTED
```
---
### 2. 图片加载失败
重点检查:
- URI 是否有效
- 文件是否存在
- 是否有 URI 权限
- 是否在后台线程解码
- 图片是否损坏
- 是否为 HEIC / WEBP / RAW 等特殊格式
- Glide / Coil / Picasso 是否配置正确
- RecyclerView 复用导致错图
常见日志关键字:
```text
FileNotFoundException
SecurityException
BitmapFactory
ImageDecoder
GlideException
Coil
Picasso
openInputStream
decodeStream
```
---
### 3. 图片方向错误
重点检查:
- 是否读取 EXIF orientation
- 是否对 content uri 支持 EXIF 读取
- 是否只处理 file path,未处理 input stream
- 前置摄像头图片是否镜像
- 压缩后是否丢失 EXIF
常用类:
```kotlin
ExifInterface
Matrix
BitmapFactory
ImageDecoder
```
修复建议:
- 从 InputStream 读取 EXIF
- 保存压缩图时保留必要 EXIF
- 显示层优先让图片加载库处理 orientation
- 避免重复旋转
---
### 4. 图片保存失败
重点检查:
- Android 版本
- 是否直接写公共目录
- 是否使用 MediaStore
- 是否设置 RELATIVE_PATH
- 是否设置 IS_PENDING
- OutputStream 是否 close
- 文件名是否非法
- 存储空间是否不足
- 是否需要触发媒体扫描
Android 10+ 推荐:
```kotlin
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
ContentValues
RELATIVE_PATH
DISPLAY_NAME
MIME_TYPE
IS_PENDING
contentResolver.openOutputStream(uri)
```
保存后:
```kotlin
IS_PENDING = 0
```
---
### 5. 删除失败
重点检查:
- Android 10+ 是否需要用户授权删除
- 是否使用 ContentResolver.delete
- 是否捕获 RecoverableSecurityException
- 是否处理系统确认弹窗
- 是否有写入权限
- 是否删除后刷新列表
Android 11+ 常见方式:
```kotlin
MediaStore.createDeleteRequest(...)
```
---
### 6. OOM / 加载卡顿
重点检查:
- 是否直接加载原图
- 是否根据 View 尺寸采样
- 是否在主线程解码
- RecyclerView 是否复用
- 是否开启过大的缓存
- 是否一次性查询或加载全部图片
- 是否大图预览没有 tile / subsampling
修复建议:
- 使用 Glide / Coil 缩放加载
- RecyclerView 分页
- 使用 Paging
- 大图使用 subsampling
- 避免 Bitmap 常驻内存
- onViewRecycled 中取消加载
- 使用缩略图优先
---
## 推荐排查流程
### Step 1:确认复现条件
记录:
- 设备型号
- Android 版本
- 是否 Android 10+
- 是否 Android 13+
- 是否首次安装
- 权限是否授权
- 是图片、视频还是混合媒体
- 来源:相机拍摄 / 下载 / 微信 / 浏览器 / 系统截图
- 文件格式:JPG / PNG / HEIC / WEBP / GIF / RAW
- 是列表页、选择页、预览页还是保存页
- 复现概率
---
### Step 2:收集日志
推荐命令:
```bash
adb logcat -v time > gallery_bug.log
adb shell dumpsys package your.package.name > package_info.txt
adb shell dumpsys media.provider > media_provider.txt
```
检查文件:
```bash
adb shell ls -l /sdcard/DCIM/
adb shell ls -l /sdcard/Pictures/
adb shell ls -l /sdcard/Download/
```
检查权限:
```bash
adb shell appops get your.package.name
adb shell dumpsys package your.package.name | grep permission
```
---
### Step 3:定位层级
按照以下顺序判断:
1. App 是否有读取权限
2. MediaStore 是否能查询到数据
3. 查询条件是否正确
4. URI 是否可访问
5. 图片是否可解码
6. 缩略图是否正确加载
7. RecyclerView 绑定是否正确
8. 图片预览是否处理方向和大图
9. 保存 / 删除是否符合 Android 版本要求
---
## MediaStore 查询检查项
重点检查:
```kotlin
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
MediaStore.Files.getContentUri("external")
ContentResolver.query(...)
```
必须确认:
- projection 不为空或字段存在
- selection 正确
- sortOrder 正确
- cursor 正确 close
- `_ID` 正确拼接 content uri
- DATA 字段在 Android 10+ 不应强依赖
- 不要直接依赖绝对路径
推荐使用:
```kotlin
ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id
)
```
---
## Android 版本适配重点
### Android 9 及以下
常见权限:
```xml
```
---
### Android 10
重点:
- Scoped Storage
- MediaStore 写入
- 不建议直接使用 file path 访问公共目录
- 可临时使用 requestLegacyExternalStorage,但不推荐长期依赖
---
### Android 11 / 12
重点:
- 更严格的公共目录访问
- 删除 / 修改他人媒体需要用户授权
- MANAGE_EXTERNAL_STORAGE 不应滥用
---
### Android 13+
重点权限:
```xml
```
---
### Android 14+
重点:
```xml
```
需要处理用户只授权部分照片的情况。
---
## 图片加载库检查
### Glide
重点检查:
```kotlin
Glide.with(context)
.load(uri)
.thumbnail(...)
.override(width, height)
.into(imageView)
```
注意:
- Fragment / View 生命周期
- RecyclerView 复用
- 清理请求
- 加载 content uri
- placeholder / error 图
---
### Coil
重点检查:
```kotlin
imageView.load(uri) {
crossfade(true)
size(width, height)
}
```
注意:
- lifecycle
- memory cache
- disk cache
- SVG / GIF / HEIC 支持
---
## 常见修复策略
- 修复 Android 13+ 媒体权限
- Android 14 处理部分照片授权
- 不再依赖 MediaStore DATA 字段
- 使用 content uri 替代 file path
- 查询 Cursor 后及时 close
- 图片保存改用 MediaStore
- 删除媒体时使用系统授权流程
- 图片解码增加采样
- RecyclerView 中取消旧请求
- 修复 EXIF 方向处理
- 增加空列表、权限拒绝、加载失败 UI
- 大图预览使用压缩或分块加载
- 异步加载避免阻塞主线程
---
## 代码审查重点
修复 gallery bug 时,必须检查:
- 是否兼容 Android 10+
- 是否兼容 Android 13+
- 是否兼容 Android 14 部分照片权限
- 是否处理权限拒绝
- 是否处理 URI 失效
- 是否关闭 Cursor / InputStream / OutputStream
- 是否避免主线程解码
- 是否避免 OOM
- 是否处理 RecyclerView 错图
- 是否处理图片方向
- 是否处理保存失败
- 是否处理删除授权
- 是否处理空数据状态
---
## 验证清单
至少验证:
- 首次安装进入相册
- 权限拒绝
- 权限允许
- Android 13 图片权限
- Android 13 视频权限
- Android 14 部分照片授权
- 图片列表加载
- 视频列表加载
- 混合媒体列表
- 大图预览
- HEIC 图片
- GIF 图片
- 图片方向
- 拍照后刷新相册
- 保存图片
- 删除图片
- 存储空间不足
- 快速滑动列表
- 前后台切换
- 横竖屏切换
---
## 输出 Bugfix 结论模板
```text
Root Cause:
- 问题原因:
Fix:
- 修改内容:
Risk:
- 影响范围:
Verification:
- 已验证场景:
- 未覆盖场景:
Logs:
- 关键日志:
```
## Heuristics
- For "empty gallery" on Android 13+, always check the per-media-type granular permissions (`READ_MEDIA_IMAGES` / `READ_MEDIA_VIDEO`) before blaming MediaStore.
- For "photo not visible after save" — suspect `IS_PENDING = 1` still set, or MediaStore entry created but OutputStream not flushed/closed, or the app bypassed the media scanner.
- For "wrong orientation" — check EXIF read path: `ExifInterface` works on InputStream/Uri path but some libraries silently drop EXIF when compressing/resizing.
- For "blurry / wrong image in RecyclerView" — classic missing `.into()` after cancel, or Glide/Coil request not cancelled in `onViewRecycled`.
- For "image load fails on Android 13+ but works on older" — suspect `READ_EXTERNAL_STORAGE` still used instead of `READ_MEDIA_IMAGES`, or `content://` Uri permissions not persisted across process restart.
- For "image visible in file manager but not in query results" — check MediaScanner has run, and that projection/sortOrder/selection aren't excluding it by MIME type or `IS_PENDING` flag.
- For "OOM in gallery list" — always suspect: no sampling on decode, loading full-res for a thumbnail slot, no Paging, no per-item Glide/Coil override to view size.
- For "Android 14 partial selection shows nothing" — the app did not handle `READ_MEDIA_VISUAL_USER_SELECTED` and didn't check Uri availability per item.
## Anti-Patterns
- Catching `SecurityException` around a storage call without first checking permission state — hides the real bug.
- Using `MediaStore.Images.ImageColumns.DATA` (absolute path) on Android 10+ — the column is deprecated/unreliable under Scoped Storage.
- Opening `FileInputStream(uri.path)` on a `content://` Uri — must use `contentResolver.openInputStream(uri)`.
- Loading full-resolution bitmaps into RecyclerView thumbnails instead of sampling to view dimensions.
- Holding Bitmap references in static/cache without bounds or eviction.
- Calling `ContentResolver.delete` then refreshing the list without handling `RecoverableSecurityException` on Android 10+ / `MediaStore.createDeleteRequest` on Android 11+.
- Setting `IS_PENDING = 1` on MediaStore insert and forgetting to flip it to `0` after write — leaves entries invisible to other apps/scanners.
- Depending on `requestLegacyExternalStorage` as a fix rather than a migration window on Android 10.
- Logging user photo `Uri`s verbatim — leaks private picture paths; redact to `id` + MIME type.
- Treating gallery issues as "UI-only" and skipping permission + MediaStore + storage-layer checks.
## CodeGraph Integration
CodeGraph helps ground the gallery diagnosis in the actual codebase structure. Run it before proposing a fix.
**When to run CodeGraph**:
- After the triage narrows the symptom to a specific gallery sub-area (list load / save / delete / orientation / OOM / picker)
- When tracing the data flow from MediaStore query → URI handling → image loading → UI binding
- When the fix may change how multiple screens consume a shared image repository
**How**:
```bash
codegraph explore ""
codegraph explore ""
codegraph explore ""
```
**What to look for from CodeGraph results (gallery-focused)**:
- **Callers of the MediaStore query method** — do all callers pass the same projection/selection, or are some paths using deprecated columns or wrong MIME filters?
- **Uri consumer chain** — who receives the Uri from the query result, and does anyone convert it back to a file path (a Scoped Storage anti-pattern)?
- **Image-loading entry points** — which `Glide`/`Coil`/`Picasso` call sites load the suspect Uri, and which lifecycle scopes them (Fragment vs Application)?
- **Write/save call sites** — which callers set `IS_PENDING`, open the OutputStream, and flip `IS_PENDING` back to `0`. A missing flip is a common root cause.
- **Delete paths** — which callers use `ContentResolver.delete` vs `MediaStore.createDeleteRequest`, and whether `RecoverableSecurityException` is handled.
- **RecyclerView adapter binding** — the on-bind handler that calls into the image loader; paired with `onViewRecycled` for cancellation.
- **Downstream impact** — screens that share the same repository; a fix that changes Uri access semantics may break the preview or share flow.
**Scope note**: CodeGraph does not reliably index XML layouts, `res/values`, `AndroidManifest.xml`, or Gradle build scripts. For gallery issues rooted in manifest permissions, `requestLegacyExternalStorage`, or `network_security_config`, supplement with `rg`/`find`. Also scan `settings.gradle` for multi-module dependencies on the image loading library.
**Fallback**: If `codegraph explore` returns no meaningful results, proceed without it — CodeGraph is an enhancement, not a blocker. Fall back to `rg`/grep for MediaStore / image-loader / save-path call-site search.
## Guardrails
- Run the full triage classification before jumping to a code fix — gallery bugs are disproportionately driven by Android version + permission state.
- Always check Android 10 / 13 / 14 permission differences for media access before editing code.
- Always verify the fix with real devices on at least two API levels (e.g., API 29 and API 34+) — gallery behavior is highly version-sensitive.
- Always confirm the MediaStore query projection is version-appropriate — `DATA` is deprecated under Scoped Storage.
- Always confirm `IS_PENDING` toggling on MediaStore insert/update is correct.
- Always close `Cursor`, `InputStream`, `OutputStream` — resource leaks show up as OOM / stale scans hours later.
- Always use `contentResolver.openInputStream(uri)` on `content://` Uris, never `FileInputStream(uri.path)`.
- Never log photo Uris or absolute paths verbatim — redact to `id` + MIME type to avoid PII leakage.
- Do not mask storage errors with a broad try/catch — find and fix the missing permission / missing scan / deprecated column first.
- If `codegraph explore` returns no meaningful results, proceed without it — CodeGraph is an enhancement, not a blocker. Fall back to `rg`/grep.