Media Storeで他アプリが所有するファイルへ書き込み

投稿日:  更新日:

メディアデータはアプリ間で共有されるので、アプリがデータへアクセスするには、アクセス許可が必要です。

この許可の権限の範囲は、プライバシー保護の観点から、徐々に狭められてきました。

そして、対象範囲別ストレージにおいて、他アプリが所有するファイルへの書き込みは、URI(ファイル)毎の許可が必要になっています。

許可の取得は、ユーザと対話をする形式(ダイアログ)で行われます。手順が少し複雑です。

ここに「他アプリが所有するファイルへ書き込み」する方法を、まとめます。

※環境:Android Studio Narwhal Feature Drop | 2025.1.2 Patch 1

スポンサーリンク

アクセス許可

対象範囲別ストレージにおいて、他のアプリが所有するファイルへアクセスする場合の許可は、図のようになっています。

対象範囲別ストレージにおけるパーミッション

読み出し

通常のパーミッションシステムで、メディアデータへアクセスする許可を取得できます。
※詳細は「外部ストレージへアクセスするパーミッションと権限の範囲」を参照

許可を得たアプリは、ファイルを読み出し可能です。

書き込み

後述する方法で、URI(ファイル)へアクセスする許可を取得できます。

許可を得たアプリは、ファイルへ書き込み可能です。

 注意 

「URIに出されるアクセス許可」と「パーミッションシステムの出すアクセス許可」は独立しています。両者の間に関係性は無く、全くの別物です。

スポンサーリンク

許可の取得手順

アクセス許可の取得は、次のような手順で行います。

URIのアクセス許可の取得

 手順1 

URIのアクセス許可をチェック。許可が無い場合はClientアプリを起動します。

 手順2 

Clientアプリはダイアログを表示して、ユーザにアクセス許可・拒否の判定を求めます。

 手順3 

ダイアログによる判定の結果をコールバックで返します。

手順1~3はアプリケーションコンポーネント(Activity)の連携と同じ動作です。
※詳細は「App component:Activity」を参照

スポンサーリンク

書き込みの例

書き込みの具体的な例です。※Activity Result APIで記述しています。

コールバックの定義と登録

registerForActivityResultはコールバックの定義と登録を行います。

ラムダ式がコールバックです。「許可・拒否」はresultCodeで判断できます。

そして、このコールバックを利用するコンポーネントのランチャー(_MediaWriteDialog : ActivityResultLauncher)を返します。

        private lateinit var targetUri: Uri
        private lateinit var targetData: ByteArray
		...
	
        val _MediaWriteDialog = registerForActivityResult(
            ActivityResultContracts.StartIntentSenderForResult()
        ) { result ->
            if(result.resultCode == RESULT_OK) {    // アクセス許可
                writeToUri_Resolver(targetData, targetUri)
            }
            else { }                                // アクセス拒否
        }

registerForActivityResultは、LifecycleOwnerのcurrentStateが「STARTED」より前のタイミングで実行して下さい。Activityのライフサイクルと密接に関係しています。
※LifecycleOwnerについては「ライフサイクル対応コンポーネント作成」を参照

スポンサーリンク

許可チェックとダイアログ起動

URIのアクセス許可をチェックします。許可の無い場合はClientアプリを起動(launch)し、ダイアログを表示させます。

launchの引数は取得したい許可のリクエスト(MediaWriteReq : IntentSenderRequest)です。書き込みを行いたいのでMediaStore.createWriteRequestを使って、リクエストを構築しています。

        @RequiresApi(Build.VERSION_CODES.R)
        fun onWrite(uri: Uri, id: Long, data: ByteArray) {
            try {
                val _uri = ContentUris.withAppendedId(uri, id)
                val _chkUriPerm = checkUriPermission(		// 許可チェック
                    _uri, Process.myPid(), Process.myUid(),
                    FLAG_GRANT_WRITE_URI_PERMISSION
                )
                if (_chkUriPerm == PackageManager.PERMISSION_GRANTED) {
                    writeToUri_Resolver(data, _uri)
                } else {
                    targetUri = _uri
                    targetData = data						// ↓↓ダイアログを起動
                    _MediaWriteDialog.launch(MediaWriteReq(contentResolver, _uri))
                }
            } catch (e: Exception) { Log.i(TAG, "[onWrite] ${e}") }
        }
@RequiresApi(Build.VERSION_CODES.R)
fun MediaWriteReq(resolver: ContentResolver, uri: Uri): IntentSenderRequest {
    val _pendingIntent = MediaStore.createWriteRequest(resolver, listOf(uri))
    val _intentSenderRequest = IntentSenderRequest.Builder(
        _pendingIntent.intentSender
    ).build()
    return _intentSenderRequest
}

MediaStore.createWriteRequestはAPI≧30から利用が可能です。

スポンサーリンク

書き込みの実行

書き込みの実行(onWrite)を行うと、ダイアログが表示されて、ユーザに「許可・拒否」の判定を求めます。

val _uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val _id = 1000000033

// val _uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
// val _id = 1000000034

// val _uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
// val _id = 1000000035

onWrite(_uri, _id, _data)
メディアテーブルの状態
# pwd
/data/data/com.google.android.providers.media.module/databases
# sqlite3 ./external.db
sqlite> .headers on
sqlite> .mode column
sqlite> select _id,_display_name,_data from images;
_id         _display_name  _data
----------  -------------  -------------------------------------
1000000033  leaf.jpg       /storage/emulated/0/Pictures/leaf.jpg
sqlite> select _id,_display_name,_data from video;
_id         _display_name  _data
----------  -------------  --------------------------------------
1000000034  myroom.mpeg    /storage/emulated/0/Movies/myroom.mpeg
sqlite> select _id,_display_name,_data from audio;
_id         _display_name  _data
----------  -------------  --------------------------------------
1000000035  akatonbo.mp3   /storage/emulated/0/Music/akatonbo.mp3

※エミュレータ(API 33)で確認
メディアデータの状態
# pwd
/storage/emulated/0
# ls -l Pictures/* Movies/* Music/* */.thumbnails/*
-rw-rw---- 1 u0_a173 media_rw   5349 2025-08-19 13:23 Movies/.thumbnails/1000000034.jpg
-rwxrwx--- 1 u0_a173 media_rw 195759 2025-08-19 13:23 Movies/myroom.mpeg
-rw-rw---- 1 u0_a173 media_rw  11472 2025-08-19 13:28 Music/.thumbnails/1000000035.jpg
-rwxrwx--- 1 u0_a173 media_rw  45366 2025-08-19 13:27 Music/akatonbo.mp3
-rw-rw---- 1 u0_a173 media_rw  34088 2025-08-19 13:23 Pictures/.thumbnails/1000000033.jpg
-rwxrwx--- 1 u0_a173 media_rw  47891 2025-08-19 13:22 Pictures/leaf.jpg

※エミュレータ(API 33)で確認

なお、ダイアログに表示される画像はサムネイル(.thumbnail/メディア情報のID.jpg)です。

imagesvideoaudio
許可・拒否ダイアログ(images)
許可・拒否ダイアログ(video)
許可・拒否ダイアログ(audio)

「許可」となったURIは、このアプリによる書き込みが可能です。

例えば上書きするのであれば、通常のアクセスと同様に、下記の拡張関数で行えます。
※メディアファイルの書き込みについては「ImagesデータへMedia Storeでアクセス(API≧29)」を参照

fun Context.writeToUri_Resolver(data: ByteArray, uri: Uri) {
    try {
        contentResolver.openOutputStream(uri, "w")?.use { outputStream ->
            outputStream.write(data)
            outputStream.flush()
        }
    } catch (e: Exception) { Log.i(TAG, "[writeToUri] ${e}") }
}
スポンサーリンク

「既存のファイルへ書き込み」と考えるのが妥当

対象範囲別ストレージにおいて、既存のファイルへ書き込み(更新)を行う場合、アクセス許可の必要性は、次のようになります。

 自アプリのファイル ⇒ 許可が不要
 他アプリのファイル ⇒ 許可が必要

ファイルの所有者により、必要性の有無が変わります。

ファイルの所有者を判別し、必要な場合のみに許可の取得を行いたいところです。しかし、API内に所有者を判別する手段が準備されていません。

ですので、所有者に関係なく、全ての既存ファイルにおいて、許可の取得を行うしかありません。

既存のファイルへ書き込むと、古いデータは消失します。

不本意な書き込み(間違った書き込み、悪意のあるアプリによる改ざん)は大問題なので、書き込みを行う前に、所有者に関係なく確認を行う行為は理に適ています。

アクセス許可を取得する場面

そう考えると、アクセス許可を取得する場面は、「他アプリが所有するファイルへ書き込み」よりも「既存ファイルへ書き込み」する時と考える方が、妥当かも知れません。

スポンサーリンク

Appendix:その他のリクエスト

createWriteRequest以外のリクエストも準備されています。

リクエスト
(MediaStore.***)
概要許可後の動作
createWriteRequest書き込み許可を要求URIへ書き込み(更新)可能
createDeleteRequest削除する許可を要求即時に削除
createTrashRequest廃棄する許可を要求ゴミ箱へ廃棄したと見なされる
(is_trashedカラムを設定)
一定時間経過後にシステムが削除
createFavoriteRequestお気に入り登録する許可を要求お気に入り登録したと見なされる
(is_favoriteカラムを設定)
※許可はアプリに対して与えられる

アプリが既存ファイル(他アプリが所有するファイル)へ「〇〇する」許可を取得するためのリクエストです。

スポンサーリンク

関連記事:

ストレージのリソースは、内部メモリー、外部メモリー、クラウドの3つがあります。 外部メモリーは、主にSDカードです。 このSDカードはAndroid端末の世代が進むにつれて、扱いを変えてきました。 リソースとSDカードを主眼に置いたストレージの変遷について、まとめます。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
Adoptable Storage(API≧23、Android 6.0)が導入されて、SDカードの扱い方をユーザ側で指定できるようになりました。 「Adoptable Storage」について、まとめます。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
ストレージはデータの用途別に記憶領域が分けられます。 保存先を守らないと、セキュリティリスクが発生したり、他のアプリと協調した動作が出来なくなったり、します。 ですので、適切な場所へデータを保存しましょう。 今回は「用途別記憶領域とボリューム」について、まとめます。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
ストレージへアクセスする方法は、「扱うデータの種類」「アクセス先」「セキュリティの確保」などの要件により、最適なアクセス方法が存在するので、使い分けが必要です。 今回は「アクセスする方法」について、まとめます。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
「内部ストレージ」にアクセスする方法について、まとめます。 「内部ストレージ」は、セキュリティが高いうえに、端末の内部メモリーに構築されるので、必ず存在します。 使いたい時に安心して使え、もっとも使い勝手のよいストレージと言えます。 アプリのデータを保存したいならば、保存先として真っ先に考えるストレージです。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
外部ストレージへアクセスするアプリは、パーミッション(許可)をユーザーから取得する必要があります。 パーミッションを得たアプリは、許可された権限の範囲内で、ストレージへアクセスが可能です。 これらは、ユーザーデータのプライバシー保護と密接に関係しています。 プライバシー保護をより強固にするために、パーミッションと権限の範囲の仕様は改変されてきました。 今回は、改変の歴史を辿りつつ、「パーミッションと権限の範囲」について、まとめます。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
「外部ストレージ_プライマリ」にアクセスする方法について、まとめます。 「外部ストレージ_プライマリ」は、アプリ間で共有するデータを保存するストレージです。 主にカメラ、音楽プレーヤー、動画プレーヤーで扱うデータを保存します。 「外部ストレージ_プライマリ」に保存したデータは、他のアプリに公開することになるので、秘匿性の高いデータの保存に適しません。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
「外部ストレージ_セカンダリ」にアクセスする方法について、まとめます。 「外部ストレージ_セカンダリ」は、デバイス間(または、アプリ間)で共有するデータを保存するストレージです。 デバイスとは、他の携帯端末やパソコンなどを指します。 ストレージがSDカードのような取り外し可能なリソース上に構築されているので、データの共有は取り外したストレージを他のデバイスへ取り付けて行います。 「外部ストレージ_セカンダリ」に保存したデータは、他のデバイスに公開することになるので、秘匿性の高いデータの保存に適しません。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
「外部ストレージ_プライマリ」に保存されるメディアデータは、Media Storeと呼ばれる特別な方法でアクセスします。 他の方法も可能ですが、Media Storeが最も適した方法です。 この「メディアデータへMedia Storeでアクセス」する方法を、まとめます。 この記事は、Imagesデータ編です。また、基本的なアクセス(読み書き)に的を絞って説明しています。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
メディアデータはデータベースによって管理されています。 Media Store APIを使ってメディアデータへアクセスしているのであれば、データベースの所在や構成などを意識する必要はありません。 ただ、「より複雑な制御をしたい場合」や「デバックを効率化したい場合」などに、データベースの知識が役立ちます。 今回は「メディアデータのデータベースによる管理」について、まとめます。 この記事は、Imagesデータ編です。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
「外部ストレージ_プライマリ」に保存されるメディアデータは、Media Storeと呼ばれる特別な方法でアクセスします。 他の方法も可能ですが、Media Storeが最も適した方法です。 この「メディアデータへMedia Storeでアクセス」する方法を、まとめます。 この記事は、Imagesデータ編です。また、基本的なアクセス(読み書き)に的を絞って説明しています。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
メディアデータはデータベースによって管理されています。 Media Store APIを使ってメディアデータへアクセスしているのであれば、データベースの所在や構成などを意識する必要はありません。 ただ、「より複雑な制御をしたい場合」や「デバックを効率化したい場合」などに、データベースの知識が役立ちます。 今回は「メディアデータのデータベースによる管理」について、まとめます。 この記事は、Imagesデータ編です。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
「外部ストレージ_プライマリ」に保存されるメディアデータは、Media Storeと呼ばれる特別な方法でアクセスします。 他の方法も可能ですが、Media Storeが最も適した方法です。 別の記事で「メディアデータへMedia Storeでアクセス」する方法を、まとめました。 その記事は、基本的なアクセスとして「追加(insert)&読み書き」のみを説明しています。 この記事は、上記に加えて「参照(query)・更新(update)・削除(delete)」を説明します。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
「外部ストレージ_プライマリ」に保存されるメディアデータは、データベースで管理されます。 Media Storeを使うと、このデータベースを通してメディアデータへアクセスできます。 データベース(SQLite)が持つ検索機能を使ってアクセス対象を絞り込めるので、とても便利です。 この記事は、「絞り込みアクセス」について、まとめます。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
メディアデータはMedia Storeを使ったアクセスが最適です。 その理由は、メディア特有の付加情報を使って、データを管理できるからです。 管理されたデータはブログラムから扱い易いです。また、ユーザの使い勝手(エクスペリエンス)も向上します。 バケットはメディアデータを管理する付加情報の一つです。 無くてもアクセスは可能ですが、積極的に取り入れた方が良いと思います。 今回は「ファイル名とバケットを指定したアクセス」について、まとめます。 「API≦28」と「API≧29」で動作が異なります。注意して下さい。 ※環境:Android Studio Narwhal Feature Drop | 2025.1.2 ...
メディアデータはMedia Storeを使ったアクセスが最適です。 その理由は、メディア特有の付加情報を使って、データを管理できるからです。 管理されたデータはブログラムから扱い易いです。また、ユーザの使い勝手(エクスペリエンス)も向上します。 バケットはメディアデータを管理する付加情報の一つです。 無くてもアクセスは可能ですが、積極的に取り入れた方が良いと思います。 今回は「ファイル名とバケットを指定したアクセス」について、まとめます。 「API≦28」と「API≧29」で動作が異なります。注意して下さい。 ※環境:Android Studio Narwhal Feature Drop | 2025.1.2 ...
スポンサーリンク