Sibainu Relax Room

柴犬と過ごす

Android MediaStore.MediaColumnsを調べる

散歩でカモを見つけ観察している柴犬です。

概要

「Android CameraX を始めてみて3」の takePhoto() 関数の入れ子 onImageSaved() 関数の中でもう少し突き詰めてみたいところがありましたので、記録します。

次に紹介する本はちょっと過激な表紙ですが、本の内容はまじめてよく理解できる書き方をしています。先の「Androidアプリ開発」で飛ばしているところを丁寧に説明しているので、これで理解が早まりました。お勧めです。

しかもこれが100円で買え、ボリュームがすごい量です。なので著者に感謝です。

onImageSaved() 関数の中2つの疑問

調べてなんとなくできたという感じでしたが次の 2 点について疑問に思っていましたので、もう少し突っ込んで試行してみました。

1. projection はどういう働きをするのか

・・・・私の判定 SQL でいうならば Field の指定のようです。

2. カラムのインデックスがなぜ 0 なのか

・・・・私の判定 一つの Field だけなので 0 でよい。

copy

                override fun
                        onImageSaved(output: ImageCapture.OutputFileResults) {
                    val msg = "Photo capture succeeded: ${output.savedUri}"
                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                    Log.d(TAG, msg)

                    //*****疑問1
                    val projection = arrayOf(MediaStore.MediaColumns.DATA)
                    //アプリケーションのデータベースに接続して URI を検索します
                    val cursor =
                        applicationContext.contentResolver.query(
                            Uri.parse(
                                output.savedUri.toString()
                            ), projection, null, null, null
                        )

                    if (cursor != null) {
                        var path: String? = null
                        //最初のレコードにカーソルを移します
                        if (cursor.moveToFirst()) {
                            //*****疑問2
                            //path はカラムのインデックスが 0 のようです
                            path = cursor.getString(0)
                        }
                        cursor.close()
                        if (path != null) {
                            //パスが取得できたので File を作成します
                            val file = File(path)
                            //ftpDirectory は "ftp://192.168.□□.□□/" 不要
                            val filelist = FtpAccess().Await(
                                "□□□□",
                                "□□□□□□□□□□",
                                "192.168.□□.□□",
                                "□□□□□□/□□□□□□/",
                                file
                            )
                            findViewById<TextView>(R.id.tvinfo).text = filelist
                        }
                    }
                }

疑問点を推測

上のコードを実行して最終行で表示させないように次のとおりコメントアウト

findViewById<TextView>(R.id.tvinfo).text = filelist

↓

//findViewById<TextView>(R.id.tvinfo).text = filelist

if (moveToFirst()) {} のブロックの中に次のコード(cursorのスコープ内なので)

val columnnames: Array<String> = getColumnNames()
findViewById<TextView>(R.id.tvinfo).text = columnnames.joinToString("\n")

または

val infolist = mutableListOf<String>()
for (i in 0..(getCount() - 1)) {
    infolist.add(getColumnName(i))
}
findViewById<TextView>(R.id.tvinfo).text = infolist.joinToString("\n")

を挿入して実行します。

実行の結果

いくつかの名前があると予想していましたが、なんと次の一つのみでした。

_data

原因の推測

この原因を推測してみました。

cursor を取得するときに projection を次のようにして、データの項目のみとしているからと結論しました。

val projection = arrayOf(MediaStore.MediaColumns.DATA)

であるなら、projection を次のように指定しなければすべてのカラム名が見えるはずです。

val cursor = applicationContext.contentResolver.query(
               Uri.parse(output.savedUri.toString()), projection, null, null, null            )

↓↓

val cursor = applicationContext.contentResolver.query(
               Uri.parse(output.savedUri.toString()), null, null, null, null            )

推測の検証結果

早々に実行してみました。予想通りすべて表示されました。

全部で次の56項目が表示されました。

instance_id, 
compilation,
disc_number,
duration,
album_artist,
description,
picasa_id,
resolution,
atitude,
orientation,
artist,
author,
height,
is_drm,
bucket_display_name,
owner_package_name,
f_number,
volume_name,
date_modified,
writer,
date_expires,
composer,
_display_name,
scene_capture_type,
datetaken,
mime_type,
bitrate,
cd_track_number,
_id,
iso,
xmp,
year,
_data,
_size,
album,
genre,
title,
width,
longitude,
is_favorite,
is_trashed,
exposure_time,
group_id,
document_id,
generation_added,
is_download,
generation_modified,
is_pending,
date_added,
mini_thumb_magic,
capture_framerate,
num_tracks,
isprivate,
original_document_id,
bucket_id,
ralative_path

_data は上から32番目にありました。

MediaStore.MediaColumns

val projection = arrayOf(MediaStore.MediaColumns.DATA) の配列の中身である

MediaStore.MediaColumns.DATA が何か調べてみることにします。それはただの値でした。

MediaStore.MediaColumns に関するリファレンスは次の URL に載っています。

https://developer.android.com/reference/android/provider/MediaStore.MediaColumns

これををまとめて次の表を作りました。

Media メタデータ集
定数
値と説明
ALBUM
Value: “album”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_ALBUM
ALBUM_ARTIST
Value: “album_artist”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_ALBUMARTIST
ARTIST
Value: “artist”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_ARTIST または ExifInterface#TAG_ARTIST
AUTHOR
Value: “author”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_AUTHOR
BITRATE
Value: “bitrate”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_BITRATE
BUCKET_DISPLAY_NAME
Value: “bucket_display_name”The primary bucket display name of this media item.
BUCKET_ID
Value: “bucket_id”
The primary bucket ID of this media item.
CAPTURE_FRAMERATE
Value: “capture_framerate”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_CAPTURE_FRAMERATE
CD_TRACK_NUMBER
Value: “cd_track_number”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_CD_TRACK_NUMBER
COMPILATION
Value: “compilation”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_COMPILATION
COMPOSER
Value: “composer”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_COMPOSER
Value: “_data”
Absolute filesystem path to the media item on disk.
DATE_ADDED
Value: “date_added”
The time the media item was first added.
DATE_EXPIRES
Value: “date_expires”
The time the media item should be considered expired.
DATE_MODIFIED
Value: “date_modified”
このメディアから導き出される指標値 File#lastModified()
DATE_TAKEN
Value: “datetaken”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_DATE または ExifInterface#TAG_DATETIME_ORIGINAL
DISC_NUMBER
Value: “disc_number”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_DISC_NUMBER
DISPLAY_NAME
Value: “_display_name”
The display name of the media item.
DOCUMENT_ID
Value: “document_id”
The “document ID” GUID as defined by the XMP Media Management standard, extracted from any XMP metadata contained within this media item.
DURATION
Value: “duration”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_DURATION
GENERATION_ADDED
Value: “generation_added”
Generation number at which metadata for this media item was first inserted.
GENERATION_MODIFIED
Value: “generation_modified”
Generation number at which metadata for this media item was last changed.
GENRE
Value: “genre”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_GENRE
HEIGHT
Value: “height”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_VIDEO_HEIGHT, MediaMetadataRetriever#METADATA_KEY_IMAGE_HEIGHT または ExifInterface#TAG_IMAGE_LENGTH
INSTANCE_ID
Value: “instance_id”
The “instance ID” GUID as defined by the XMP Media Management standard, extracted from any XMP metadata contained within this media item.
IS_DOWNLOAD
Value: “is_download”
Flag indicating if the media item has been marked as being part of the Downloads collection.
IS_DRM
Value: “is_drm”
Flag indicating if a media item is DRM protected.
IS_FAVORITE
Value: “is_favorite”
Flag indicating if the media item has been marked as being a “favorite” by the user.
IS_PENDING
Value: “is_pending”
Flag indicating if a media item is pending, and still being inserted by its owner.
IS_TRASHED
Value: “is_trashed”
Flag indicating if a media item is trashed.
MIME_TYPE
Value: “mime_type”
The MIME type of the media item.
NUM_TRACKS
Value: “num_tracks”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_NUM_TRACKS
ORIENTATION
Value: “orientation”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_VIDEO_ROTATION, MediaMetadataRetriever#METADATA_KEY_IMAGE_ROTATION, または ExifInterface#TAG_ORIENTATION
ORIGINAL_DOCUMENT_ID
Value: “original_document_id”
The “original document ID” GUID as defined by the XMP Media Management standard, extracted from any XMP metadata contained within this media item.
OWNER_PACKAGE_NAME
Value: “owner_package_name”
Package name that contributed this media.
RELATIVE_PATH
Value: “relative_path”
Relative path of this media item within the storage device where it is persisted.
RESOLUTION
Value: “resolution”
Calculated value that combines WIDTH and HEIGHT into a user-presentable string.
SIZE
Value: “_size”
このメディアから導き出される指標値 File#length()
TITLE
Value: “title”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_TITLE
VOLUME_NAME
Value: “volume_name”
Volume name of the specific storage device where this media item is persisted.
WIDTH
Value: “width”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_VIDEO_WIDTH, MediaMetadataRetriever#METADATA_KEY_IMAGE_WIDTH または ExifInterface#TAG_IMAGE_WIDTH
WRITER
Value: “writer”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_WRITER
XMP
Value: “xmp”
インデックスされた XMP メタデータ
YEAR
Value: “year”
このメディアから導き出される指標値 MediaMetadataRetriever#METADATA_KEY_YEAR
計 44項目

DATAの説明

ディスク上のメディアへの絶対ファイルパスを格納しているとあります。

このパスを使ってファイル操作を行うことができるが、ファイルが常に利用可能であるとしてはならないとあります。

Android 11以降は読み取り専用になり、ファイルの場所を更新するには、DISPLAY_NAME列とRELATIVE_PATH列の値を使用しますとあります。

44項目にないもの

description は44項目の中にありませんが、次の MediaStore.Images.ImageColumns に含まれています。

今回はカメラで撮影した画像ファイルが対象ですので MediaStore.Images.ImageColumns の項目も含むようです。

44項目 + 10項目 = 54項目で2項目まだ不足していますが、それは

instance_id と _id の2項目で Columns データ構造上のものだから省かれていると推測されます。

MediaStore.Images.ImageColumns

Image メタデータ集
定数
値と説明
DESCRIPTION
Constant Value: “description”
このメディアから導き出される指標値 ExifInterface#TAG_IMAGE_DESCRIPTION
EXPOSURE_TIME
Constant Value: “exposure_time”
このメディアから導き出される指標値 ExifInterface#TAG_EXPOSURE_TIME
F_NUMBER
Constant Value: “f_number”
このメディアから導き出される指標値 ExifInterface#TAG_F_NUMBER
ISO
Constant Value: “iso”
このメディアから導き出される指標値 ExifInterface#TAG_ISO_SPEED_RATINGS
IS_PRIVATE
Constant Value: “isprivate”
Whether the image should be published as public or private.This constant represents a column name that can be used with a ContentProvider through a ContentValues or Cursor object.
LATITUDE
Constant Value: “latitude”
This constant was deprecated in API level 29. location details are no longer indexed for privacy reasons, and this value is now always null. You can still manually obtain location metadata using ExifInterface#getLatLong(float[]).
LONGITUDE
Constant Value: “longitude”
This constant was deprecated in API level 29. location details are no longer indexed for privacy reasons, and this value is now always null. You can still manually obtain location metadata using ExifInterface#getLatLong(float[]).
MINI_THUMB_MAGIC
Constant Value: “mini_thumb_magic”
This constant was deprecated in API level 29. all thumbnails should be obtained via MediaStore.Images.Thumbnails#getThumbnail, as this value is no longer supported.
PICASA_ID
Constant Value: “picasa_id”
This constant was deprecated in API level 29. this value was only relevant for images hosted on Picasa, which are no longer supported.
SCENE_CAPTURE_TYPE
Constant Value: “scene_capture_type”
このメディアから導き出される指標値 ExifInterface#TAG_SCENE_CAPTURE_TYPE
計 10項目

見直し後のコード

これまでの結果を踏まえて次のようにコードを見直しました。

また、Kotlin イディオム風に

if (cursor != null)

↓↓

cursor?.
if (path != null) 

↓↓

path?.

としてみました。

copy

                override fun
                        onImageSaved(output: ImageCapture.OutputFileResults) {
                    val msg = "Photo capture succeeded: ${output.savedUri}"
                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                    Log.d(TAG, msg)

                    //val projection = null    // 読み込む列の指定
                    // パフォーマンスとしては指定した方がよい
                    val projection = arrayOf(MediaStore.MediaColumns.DATA)
                    val selection = null     // 行の絞り込みの指定
                    val selectionArgs = null // selectionの?を置き換える引数
                    val sortOrder = null     // 並び順

                    //アプリケーションのデータベースに接続して URI を検索します
                    val cursor =
                        applicationContext.contentResolver.query(
                            Uri.parse(
                                output.savedUri.toString()
                            ), projection, selection, selectionArgs, sortOrder
                        )

                    cursor?.apply {
                        var path: String? = null
                        //最初のレコードにカーソルを移します
                        if (moveToFirst()) {
                            //データがあるカラム名は _data です。
                            val index: Int = getColumnIndex(MediaStore.MediaColumns.DATA)
                            path = getString(index)
                        }
                        close()

                        path?.let {
                            //パスが取得できたので File を作成します
                            val file = File(it)
                            //ftpDirectory は "ftp://192.168.□□.□□/" 不要
                            val filelist = FtpAccess().Await(
                                "□□□□",
                                "□□□□□□□□□□",
                                "192.168.□□.□□",
                                "□□□□□□/□□□□□□/",
                                file
                            )
                            findViewById<TextView>(R.id.tvinfo).text = filelist
                        }
                    }
                }

実行の結果は問題ありませんでした。

今回はここまでとします。