Android‑端末にPDFを保存したい――
「どこに入れたらいいのか」「保存先を管理する方法は?」と戸惑う方は多いです。
特にAndroidは「外部ストレージ(SDカード)」と「内部ストレージ」に分かれ、バージョンが上がるごとに仕様も変わります。
この投稿では、初心者でも安心してPDFを保存し、後から簡単に管理できるベストプラクティスと手順を詳しく解説します。まずは概要を抑えておきましょう。
1. 何を保存するかで選ぶ保存先
| 目的 | 推奨保存先 | 理由 |
|---|---|---|
| ユーザーが自分で閲覧・共有したい PDF(例:レポート、チュートリアル) | Downloads (外部ストレージ) | すぐに他のアプリやPCへ転送しやすい |
| アプリ固有のPDF(例:設定・ライセンス文書) | Internal/Cache (アプリ専用の内部ストレージ) | 他のユーザーに見せたくない、OS が自動削除してくれる |
| 低圧縮のPDFを多く保存し、他のアプリと共有したい | MediaStore (Documents) | 共有やバックアップに最適 |
| SDカードに大量書き込みしたいケース | External SD (マルチパスで取得) | 省メモリ、ユーザー指定で容量増加 |
ポイント
- Android 10 (API 29) 以降は Scoped Storage が有効になり、外部ストレージへの直接アクセスが制限されます。
- 12 以上では更なる制限が入り、MediaStore を使うか Storage Access Framework(SAF) でユーザーにフォルダ選択を任せる方法が主流です。
2. 保存プログラムを作る前に確認すべき設定
| ステップ | 内容 |
|---|---|
| 権限宣言 | <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />(ただし API 30 以降は必要ない) |
| Legacy Storage | Android 10 では android:requestLegacyExternalStorage="true" を application タグに追加すると、従来の一括アクセスが可能。ただし Android 11 以降は無視されます。 |
| ターゲット SDK | targetSdkVersion = 30 以上で Scoped Storage が必須。最新を目安に。 |
注意
- Android 11 以降は WRITE_EXTERNAL_STORAGE でなくても MediaStore に書ける。
- 外部 SD カードは別パスで取得する必要がある(
context.getExternalFilesDirs()にnull以外)。
3. 保存方法 ① 内部ストレージ(アプリ専用)
内部ストレージは 外部ユーザーから隠蔽 されているので、アプリでのみアクセスが可能です。
ファイルパスは Context.getFilesDir() や getCacheDir() が返すディレクトリから相対パスで扱えます。
fun savePdfToInternal(context: Context, pdfBytes: ByteArray, fileName: String) {
val file = File(context.filesDir, fileName) // 例: /data/data/com.example/files/report.pdf
FileOutputStream(file).use { it.write(pdfBytes) }
}
- メリット:外部アプリからはアクセス出来ないので、セキュリティが高い。
- デメリット:別デバイスへ持ち出せない、バックアップに注意が必要。
4. 保存方法 ② 外部ストレージ(Downloads フォルダ)
外部ストレージの Downloads はユーザーが一目で見れ、他デバイスへも簡単に送れます。
4‑1. API 29 以前(Legacy モード)
fun savePdfToDownloads(context: Context, pdfBytes: ByteArray, fileName: String) {
val downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(downloads, fileName)
FileOutputStream(file).use { it.write(pdfBytes) }
}
- 必要な権限:
WRITE_EXTERNAL_STORAGE
4‑2. API 29+(Scoped Storage) – MediaStore を使う
fun savePdfToDownloadsScoped(context: Context, pdfBytes: ByteArray, fileName: String) {
val contentValues = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, fileName)
put(MediaStore.Downloads.MIME_TYPE, "application/pdf")
put(MediaStore.Downloads.IS_PENDING, 1)
}
val resolver = context.contentResolver
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
?: throw IOException("Failed to create new MediaStore record.")
resolver.openOutputStream(uri)?.use { out ->
out.write(pdfBytes)
}
// Finalizar
contentValues.clear()
contentValues.put(MediaStore.Downloads.IS_PENDING, 0)
resolver.update(uri, contentValues, null, null)
}
重要ポイント
-
IS_PENDINGを 1 にして書き込み中は「準備中」と表示。完了後に 0 に戻します。 -
MediaStoreを利用すると、権限なしで保存でき、ファイルサイズ 10 GB 以下 であれば問題ありません。 - ユーザーが
Downloadsフォルダにアクセスできるように、通知欄 でも「ファイルが保存されました」の通知を表示すると安心です。
5. 保存方法 ③ ユーザーが選んだフォルダに保存(SAF)
ユーザーに フォルダを選ばせる 方法は、アプリの設計によっては「ユーザー中心」に保存したい場合に最適です。例:PDF を「仕事用」「プライベート」などに分けたい時。
// 1. フォルダ選択用のIntent
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
startActivityForResult(intent, REQUEST_CODE_OPEN_TREE)
// 2. onActivityResult で選択されたフォルダを取得
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_OPEN_TREE && resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
// 永続化権限を取得
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
// ここからフォルダ内へファイルを作成
val fileName = "custom_report.pdf"
val docUri = DocumentsContract.createDocument(
contentResolver, uri, "application/pdf", fileName
)
contentResolver.openOutputStream(docUri!!)!!.use { stream ->
stream.write(pdfBytes)
}
}
}
}
ポイント
Intent.ACTION_OPEN_DOCUMENT_TREEはユーザーに「フォルダを選択」させる。- パーミッショントークンを 永続化 することで次回以降も同じフォルダへ書ける。
- この方法は Scoped Storage の中で最も安全に「ユーザーの任意フォルダ」に書ける手段です。
6. PDF の管理・検索を簡単にするテクニック
6‑1. ファイル命名のルール
| 用途 | 名前の付け方 | 例 |
|---|---|---|
| 期間別 | YYYYMMDD_report.pdf |
20240623_report.pdf |
| タグ付き | projectX_sprint3.pdf |
kotlin-setup.pdf |
| バージョン管理 | v1.0_manual.pdf |
v2.1_userguide.pdf |
メリット:名前だけで内容やバージョンを判断でき、検索時の混乱を減らす。
6‑2. メタ情報を埋め込む
PDF のアドビ仕様で XMP というメタデータを埋め込むことが可能です。
Java で PDDocument (POI など) を使う例:
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
// 既存 PDF を開く
PDDocument doc = PDDocument.load(file);
PDDocumentInformation info = doc.getDocumentInformation();
info.setAuthor("Your Name");
info.setTitle("Monthly Report");
info.setSubject("Finance");
info.setKeywords("budget, finance, report");
doc.save(file);
doc.close();
メリット
- Android の
MediaStoreでTITLE,AUTHORなどの検索フィールドに自動反映されます。- PDF 閲覧アプリ側でもメタデータが表示されるため、ユーザーが内容を把握しやすいです。
6‑3. アプリ内で一覧表示
一旦保存した PDF を データベース (Room 等) で管理すると、次回起動時にすぐ一覧を作れます。
-
filePath、displayName、lastModified、sizeなどをスキーマに。 -
Flow/LiveDataでUIを即座に更新。
7. Android 11+ での注意点
| 項目 | 変更点 | 具体的な対策 |
|---|---|---|
| 外部ストレージアクセス | 直接ファイルパスを取得できない (PATH は null) |
MediaStore / SAF(Intent.ACTION_OPEN_DOCUMENT)を使う |
| マルチパス SD | context.getExternalFilesDirs() で複数取得 |
アプリが書き込み可能な SD パスを取得し、File で書き込む |
| ストレージの権限 | WRITE_EXTERNAL_STORAGE が自動で削除 |
MANAGE_EXTERNAL_STORAGE 要求は 1 つだけで許可されるので回避 |
ヒント:
公式ガイドの “Scoped storage” に沿った実装は「将来的に互換性が確保」できる最良の方法です。
8. まとめ
| 目的 | よく使う保存先 | 推奨設定 |
|---|---|---|
| ユーザーが閲覧・共有したい | Downloads |
MediaStore で保存、IS_PENDING で安定化 |
| アプリ専用に隠したい | files (内部) |
Context.getFilesDir() |
| ユーザーが好きな場所に保存したい | 任意フォルダ | SAF (ACTION_OPEN_DOCUMENT_TREE) |
| 大量かつ共有可能にしたい | MediaStore/Documents |
ContentValues を駆使 |
- 権限は最小限に。「外部 SD」なら
READ/WRITE_EXTERNAL_STORAGE、Android 11 では除外。- フォルダ名/ファイル名に 一貫したルールを設けて管理しやすく。
- メタデータを埋め込むと検索が楽になる。
これらを守ることで、Android アプリで PDF を保存・管理する作業が シンプルかつ 安全になります。ぜひ、実装に落とし込み、ユーザー体験を向上させてください。 Happy coding! 🚀


コメント