rokkonet

PC・Androidソフトウェア・アプリの開発・使い方に関するメモ

android開発 Spinner ドロップダウンリストの位置番号(インデックス番号)を取得する

2022 May 14.

端末

android 11 ( APIレベル 30 )

概要
  • ドロップダウン表示する文字列の配列を作る
  • Spinnerを置くアクティビティにAdapterView.OnItemSelectedListenerインターフェースを組み込み、onItemSelected()とonNothingSelected()を実装する
    onItemSelected()
    Spinner#selectedItemPosition()でドロップダウンリストの位置番号(インデックス番号)を取得する
    onNothingSelected()

    利用しないので、何も行わない実装とする

  • ArrayAdapterにドロップダウン表示する文字列の配列を組み込む
  • SpinnerにArrayAdapterをセットする

    サンプルコード

    app/res/values/arrays.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <array name="myItems">
        <item>@string/item1</item>
        <item>@string/item2</item>
        <item>@string/item3</item>
    </array>
</resources>


MainActivity.kt

package net.sytes.rokkosan.myspinnerselectbyid

/*
2022 May 14.
Ryuichi Hashimoto.

referrence
    http://pg-box.jp/blog/20140328/pulldown#getSelectedItemId
    https://developer.android.com/guide/topics/ui/controls/spinner?hl=ja
*/

import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity


class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener {

    val spinner1: Spinner by lazy { findViewById<Spinner>(R.id.spinner1) }
    val textViewInfo1: TextView by lazy { findViewById<TextView>(R.id.textVewInfo1) }

    override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
        when (spinner1.selectedItemPosition) {
            0 -> { textViewInfo1.text = getString(R.string.item1) }
            1 -> { textViewInfo1.text = getString(R.string.item2) }
            2 -> { textViewInfo1.text = getString(R.string.item3) }
            else -> { textViewInfo1.text = getString(R.string.error) }
        }
    }

    override fun onNothingSelected(parent: AdapterView<*>) {
        // Another interface callback
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        setAdapterSpinner()
        spinner1.onItemSelectedListener = this
    }

    private fun setAdapterSpinner() {
        val myAdapter = ArrayAdapter.createFromResource(
                this, R.array.myItems, android.R.layout.simple_spinner_item)
            // simple_spinner_item
            //     プラットフォームによってデフォルトで提供されているスピナーの外観レイアウト
        myAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
            // simple_spinner_dropdown_item
            //     プラットフォームで定義された標準のドロップダウンレイアウト
        spinner1.setAdapter(myAdapter)
    }
}


strings.xml

<resources>
    <string name="app_name">MySpinnerSelectById</string>
    <string name="item1">アイテム1</string>
    <string name="item2">アイテム2</string>
    <string name="item3">アイテム3</string>
    <string name="error">エラー</string>
</resources>


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Spinner
        android:id="@+id/spinner1"
        android:layout_width="match_parent"
        app:layout_constraintBottom_toTopOf="@id/textVewInfo1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/textVewInfo1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/spinner1"
        />

</androidx.constraintlayout.widget.ConstraintLayout>


android開発 ContentResolver 音声メディア・動画メディアへのクエリによるメディア情報取得

2022 May 08.
2022 May 05.

端末

android 11 ( APIレベル 30 )

ContentResolverでの音声メディア・動画メディアへのクエリで得られるメディア情報


クエリできたメディア

システムにデフォルト内蔵の音声ファイル
システムメモリ(プライマリストレージ)のMoviesディレクトリ・Musicディレクトリに保存したテレビ録画ファイル・ラジオ録音ファイル
SDカード内に作成した(Movies、Music以外のディレクトリ名の)ディレクトリに保存したテレビ録画ファイル・ラジオ録音ファイル

クエリのUriコレクションに指定した値

MediaStore.Files.getContentUri("external")もしくはMediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)。どちらも同じ結果だった

クエリのプロジェクションに指定する値とその内容
MediaStore.MediaColumns._ID

 URI取得に利用できるID

MediaStore.Files.FileColumns.MEDIA_TYPE

 メディアのタイプ
  0:none 1:image 2:audio 3:video 4:playlist 5:subtitle 6:document

MediaStore.Files.FileColumns.DURATION

 再生時間(ミリ秒)

MediaStore.Files.FileColumns.TITLE

 ファイル名から拡張子を除いた文字列(パスは無い)。ファイル名の先頭から最後の"-"(ハイフン)までの文字列は削除されていた。ファイル名に含まれる"_"(アンダースコア)は半角空白に変換されていた

MediaStore.Files.FileColumns.DISPLAY_NAME

 拡張子が付いたファイル名(パスは無い)

MediaStore.Files.FileColumns.RELATIVE_PATH

 フルパス文字列から、先頭のパーティション部分("/srorage/external_primary/"、"/srorage/emulated/0/"、"/srorage/xxxx-xxxx/")と末尾のファイル名を取り除いた文字列。先頭に"/"(スラッシュ)は付かない。末尾に"/"(スラッシュ)が付く 。
 "/srorage/external_primary"と"/srorage/emulated/0"は同じもの。"/srorage/external_primary"はMediaStore上の表現。"/srorage/emulated/0"はファイルシステム上の表現。

MediaStore.Files.FileColumns.VOLUME_NAME

 パーティション文字列("/srorage/external_primary"、"/srorage/xxxx-xxxx")から"/srorage/"を取り除いた文字列(external_primary、xxxx-xxxx)。前後に"/"(スラッシュ)は付かない。プライマリストレージは、"emulated/0"ではなく、"external_primary"となる

MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME

 直上のディレクトリ名文字列(親ディレクトリ)。前後に"/"(スラッシュ)は付かない

"_data"

 メディアファイルの、ファイルシステム上のフルパス文字列。先頭に"/"(スラッシュ)が付く

(_dataを使わない)フルパス取得は次の文字列の連結。プライマリストレージのボリューム名は、"emulated/0"ではなく、"external_primary"となる

"/srorage/"
MediaStore.Files.FileColumns.VOLUME_NAME
"/"
MediaStore.Files.FileColumns.RELATIVE_PATH
MediaStore.Files.FileColumns.DISPLAY_NAME


"/srorage/" + MediaStore.Files.FileColumns.VOLUME_NAME + "/" + MediaStore.Files.FileColumns.RELATIVE_PATH + MediaStore.Files.FileColumns.DISPLAY_NAME

("/srorage/" + MediaStore.Files.FileColumns.VOLUME_NAME + "/" + MediaStore.Files.FileColumns.RELATIVE_PATH + MediaStore.Files.FileColumns.DISPLAY_NAME).replace("external_primary", "emulated/0")

確認実験したコード

MainActivity.kt

ファイル読み込み権限取得コードを省いているので、端末のメニューのアプリ設定で許可しておく必要あり

package net.sytes.rokkosan.randomplayer

/*
2022 May 05.
Ryuichi Hashimoto.
*/

import android.content.ContentUris
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import androidx.annotation.RequiresApi
import net.sytes.rokkosan.randomplayer.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // setContentView(R.layout.activity_main)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.buttonPlay.setOnClickListener {
            getMediaInfo()
        }
    }

    @RequiresApi(Build.VERSION_CODES.Q)
    private fun getMediaInfo() {
        val contentResolver = this.contentResolver
        val proj = arrayOf(
            MediaStore.MediaColumns._ID,
            MediaStore.Files.FileColumns.MEDIA_TYPE,
            MediaStore.Files.FileColumns.DURATION,
            MediaStore.Files.FileColumns.TITLE,
            MediaStore.Files.FileColumns.DISPLAY_NAME,
            MediaStore.Files.FileColumns.RELATIVE_PATH,
            MediaStore.Files.FileColumns.VOLUME_NAME,
            MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME,
            "_data"
        )

        val selection = (MediaStore.Files.FileColumns.MEDIA_TYPE + "="
                + MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO
                + " OR "
                + MediaStore.Files.FileColumns.MEDIA_TYPE + "="
                + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO
                )

        val query = contentResolver.query(
            MediaStore.Files.getContentUri("external"),
            //MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
            proj,
            selection,
            null,
            MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
        )
        query?.use { cursor ->
            val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
            val typeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
            val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DURATION)
            val titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.TITLE)
            val displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)
            val relativePathColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.RELATIVE_PATH)
            val volumeNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.VOLUME_NAME)
            val bucketDisplayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME)
            val pathColumn = cursor.getColumnIndexOrThrow("_data")

            while (cursor.moveToNext()) {
                val mediaType = cursor.getInt(typeColumn)
                    // mediaType  0:none 1:image 2:audio 3:video 4:playlist 5:subtitle 6:document
                    // referrence
                    //     https://developer.android.com/reference/android/provider/MediaStore.Files.FileColumns#MEDIA_TYPE

                // audioとvideoのURIを取得
                // referrence
                //     https://developer.android.google.cn/training/data-storage/shared/media?hl=ja
                var mediaId: Long
                var mediaUri: Uri
                var mediaDuration: Long
                var mediaTitle: String
                var mediaDisplayName: String
                var mediaRelativePath: String
                var mediaVolumeName: String
                var mediaBucketDisplayName: String
                var mediaPath: String
                if (mediaType == 2) { // audio
                    mediaId = cursor.getLong(idColumn)
                    mediaUri = ContentUris.withAppendedId(
                        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mediaId
                    )
                    mediaDuration = cursor.getLong(durationColumn)
                    mediaTitle = cursor.getString(titleColumn)
                    mediaDisplayName = cursor.getString(displayNameColumn)
                    mediaRelativePath = cursor.getString(relativePathColumn)
                    mediaVolumeName = cursor.getString(volumeNameColumn)
                    mediaBucketDisplayName = cursor.getString(bucketDisplayNameColumn)
                    mediaPath = cursor.getString(pathColumn)
                }
                else if (mediaType == 3) { // video
                    mediaId = cursor.getLong(idColumn)
                    mediaUri = ContentUris.withAppendedId(
                        MediaStore.Video.Media.EXTERNAL_CONTENT_URI, mediaId
                    )
                    mediaDuration = cursor.getLong(durationColumn)
                    mediaTitle = cursor.getString(titleColumn)
                    mediaDisplayName = cursor.getString(displayNameColumn)
                    mediaRelativePath = cursor.getString(relativePathColumn)
                    mediaVolumeName = cursor.getString(volumeNameColumn)
                    mediaBucketDisplayName = cursor.getString(bucketDisplayNameColumn)
                    mediaPath = cursor.getString(pathColumn)
                } else {
                    throw java.lang.Exception()
                }

                Log.d("MyTag", "mediaUri: $mediaUri")
                Log.d("MyTag","mediaDuration: " + (mediaDuration/1000).toString() + "Sec")
                Log.d("MyTag","mediaTitle: $mediaTitle")
                Log.d("MyTag","mediaDisplayName: $mediaDisplayName")
                Log.d("MyTag","mediaRelativePath: $mediaRelativePath")
                Log.d("MyTag","mediaVolumeName: $mediaVolumeName")
                Log.d("MyTag","mediaBucketDisplayName: $mediaBucketDisplayName")
                Log.d("MyTag","mediaPath: $mediaPath")
            }
        }
        query?.close()
    }
}


AndroidManifest.xml

READ_EXTERNAL_STORAGE権限を許可する

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.sytes.rokkosan.randomplayer">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.PlayRandomExternalStorageSound">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


build.gradle(Module: app)
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    compileSdk 31

    defaultConfig {
        applicationId "net.sytes.rokkosan.randomplayer"
        minSdk 24
        targetSdk 31
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        viewBinding true
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

android開発 ContentResolver 複数のメディアタイプについてURI・ファイルパスを取得する

2022 May 04.

端末

android 11 ( APIレベル 30 )

audioメディアとvideoメディアのURI・ファイルパスを取得するサンプルアプリ

MainActivity.kt
package net.sytes.rokkosan.mygetaudioandvideouriwithcontentresolver

/*
2022 May 04.
2022 May 03.
Ryuichi Hashimoto.
*/

import android.content.ContentUris
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity


class MainActivity : AppCompatActivity() {
    lateinit var textViewDebugInfo: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        textViewDebugInfo = findViewById(R.id.textViewDebugInfo)

        findViewById<Button>(R.id.buttonGetUris).setOnClickListener {
            getPathsOfUris().forEach { (key, value) ->
                Log.d("MyTag", key + " : " + value.toString())}
        }
    }

    private fun getPathsOfUris(): HashMap<String, Uri> {
        val uriMap = hashMapOf<String, Uri>()

        // referrence
        //     https://qiita.com/TaigaNatto/items/239c87080ac766c97ac9
        val contentResolver = this.contentResolver
        val proj = arrayOf(
            MediaStore.MediaColumns._ID,
            MediaStore.Files.FileColumns.MEDIA_TYPE,
            "_data"
        )

        val selection = (MediaStore.Files.FileColumns.MEDIA_TYPE + "="
                + MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO
                + " OR "
                + MediaStore.Files.FileColumns.MEDIA_TYPE + "="
                + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO
                )

        val query = contentResolver.query(
            MediaStore.Files.getContentUri("external"),
            proj,
            selection,
            null,
            MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
        )
        query?.use { cursor ->
            val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
            val typeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
            val pathColumn = cursor.getColumnIndexOrThrow("_data")

            while (cursor.moveToNext()) {
                val mediaType = cursor.getInt(typeColumn)
                // mediaType  0:none 1:image 2:audio 3:video 4:playlist 5:subtitle 6:document
                // referrence
                //     https://developer.android.com/reference/android/provider/MediaStore.Files.FileColumns#MEDIA_TYPE

                // audioとvideoのURIを取得
                var mediaId: Long
                var mediaUri: Uri
                var pathOfUri: String
                if (mediaType == 2) { // audio
                    mediaId = cursor.getLong(idColumn)
                    mediaUri = ContentUris.withAppendedId(
                        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mediaId
                    )
                    pathOfUri = cursor.getString(pathColumn)
                    uriMap.put(pathOfUri, mediaUri)
                }
                else if (mediaType == 3) { // video
                    mediaId = cursor.getLong(idColumn)
                    mediaUri = ContentUris.withAppendedId(
                        MediaStore.Video.Media.EXTERNAL_CONTENT_URI, mediaId
                    )
                    pathOfUri = cursor.getString(pathColumn)
                    uriMap.put(pathOfUri, mediaUri)
                }
            }
        }
        query?.close()
        return uriMap
    }
}


AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.sytes.rokkosan.mygetaudioandvideouriwithcontentresolver">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyGetAudioAndVideoUriWithContentResolver">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


build.gradle(Module: app)
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    compileSdk 32

    defaultConfig {
        applicationId "net.sytes.rokkosan.mygetaudioandvideouriwithcontentresolver"
        minSdk 30
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}


activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textViewDebugInfo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@id/buttonGetUris"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/buttonGetUris"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/button_label"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textViewDebugInfo" />

</androidx.constraintlayout.widget.ConstraintLayout>


strings.xml
<resources>
    <string name="app_name">MyGetAudioAndVideoUriWithContentResolver</string>
    <string name="button_label">Get URIs</string>
</resources>

android開発 ストレージ内の指定したディレクトリ内の音声ファイルをランダムに再生するアプリ

2022 May 08.
2022 May 02.


ファイル読み込みパーミッション取得ルーチンを省いているので、端末のアプリ設定で権限許可する必要あり

端末

android 11 ( APIレベル 30 )

build.gradle(Module: app)

dependenciesはデフォルトのまま

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    compileSdk 32

    defaultConfig {
        applicationId "net.sytes.rokkosan.mygeturifrompathstring"
        minSdk 30
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}


AndroidManifest.xml

"READ_EXTERNAL_STORAGE"権限を許可する

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.sytes.rokkosan.mygeturifrompathstring">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyGetUriFromPathString">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


strings.xml
<resources>
    <string name="app_name">MyGetUriFromPathString</string>
    <string name="display_app_name">Play Audio Randomly</string>
    <string name="button_open_filer2select_dir">Select Directory</string>
    <string name="button_play">Play</string>
    <string name="no_dirs">No DirectoriesFiler</string>
    <string name="dir_exists">Dir of uri exists.</string>
    <string name="dir_not_exists">Dir of uri does not exist.</string>
    <string name="not_find">Could not find</string>
</resources>


activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textViewDir"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@id/textViewFoundFile"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        />

    <TextView
        android:id="@+id/textViewFoundFile"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@id/textViewDebugInfo"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textViewDir"
        />

    <TextView
        android:id="@+id/textViewDebugInfo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@id/buttonOpenFiler"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textViewFoundFile" />

    <Button
        android:id="@+id/buttonOpenFiler"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/buttonPlay"
        app:layout_constraintTop_toBottomOf="@id/textViewDebugInfo"
        android:text="@string/button_open_filer2select_dir" />

    <Button
        android:id="@+id/buttonPlay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@id/buttonOpenFiler"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textViewDebugInfo"
        android:text="@string/button_play" />

</androidx.constraintlayout.widget.ConstraintLayout>


MainActivity.kt
package net.sytes.rokkosan.mygeturifrompathstring

/*
2022 May 03.
2022 May 01.
Ryuichi Hashimoto.
*/

import android.content.ContentUris
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Button
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import java.util.*
import java.util.concurrent.TimeUnit


class MainActivity : AppCompatActivity() {
    private lateinit var textViewDir: TextView
    private lateinit var textViewFoundFile: TextView
    private lateinit var textViewDebugInfo: TextView
    private var selectedDirUri: Uri? = null
    private var selectedDirUriReadable: String = ""

    private val fileLauncherGetDir = registerForActivityResult(
            ActivityResultContracts.StartActivityForResult() )
        { result -> // 結果を受け取るルーチン
            if (result.resultCode == RESULT_OK) {
                // succeeded.
                // get uri.
                selectedDirUri = result.data?.data

                // uri冒頭の"content://com.android.externalstorage.documents/tree"を削除し、
                // エンコードされた /(スラッシュ)を戻す。
                // プライマリ・ストレージの時に付く"/primary/"をファイルシステムに合わせて
                // "/emulated/0/"に置き換える。
                selectedDirUriReadable = selectedDirUri.toString().replace(
                    "content://com.android.externalstorage.documents/tree/", "/"
                    ).
                    replace("%3A", "/" ).replace("%2F", "/").
                    replace(Regex("^/primary/"),"/emulated/0/")

                textViewDir.text = selectedDirUriReadable
                    // uri冒頭の"content://com.android.externalstorage.documents/tree"は削除されている。
                    //   プライマリ・ストレージの時は"/emulated/0/..."となる。
                    //   SDカードの時は"/xxxx-xxxx/...."となる。

            } else {
                // failed
                textViewDir.text = getString(R.string.no_dirs)
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        textViewDir = findViewById<TextView>(R.id.textViewDir)
        textViewFoundFile = findViewById<TextView>(R.id.textViewFoundFile)
        textViewDebugInfo = findViewById<TextView>(R.id.textViewDebugInfo)

        // アクションバーに表示する文字列を設定
        supportActionBar?.title = getString(R.string.display_app_name)

        findViewById<Button>(R.id.buttonOpenFiler).setOnClickListener {
            openFilePickerGetUri()
        }

        findViewById<Button>(R.id.buttonPlay).setOnClickListener {
            // collect media files
            val collection =
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    MediaStore.Audio.Media.getContentUri(
                        MediaStore.VOLUME_EXTERNAL
                    )
                } else {
                    MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
                }

            val projection = arrayOf(
                MediaStore.Audio.Media._ID,
                "_data"
            )

            // 1分間以上のAudioを指定する
            val selection = "${MediaStore.Audio.Media.DURATION} >= ?"
            val selectionArgs = arrayOf(
                TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES).toString()
            )

            val uris: MutableList<Uri> = mutableListOf<Uri>()

            val resolver = applicationContext.contentResolver
            val query = resolver.query(
                collection,  //uriの種類
                projection, //取得する項目 nullは全部
                selection, //フィルター条件 nullはフィルタリング無し
                selectionArgs, //フィルター用のパラメータ
                null   //並べ替え。nullは並べ替えしない
            )
            query?.use { cursor ->
                val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
                val pathColumn = cursor.getColumnIndexOrThrow("_data")

                while (cursor.moveToNext()) {
                    val id = cursor.getLong(idColumn)
                    val queriedUri = ContentUris.withAppendedId(
                        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id
                    )
                    val pathOfUri = cursor.getString(pathColumn)

                    // pathOfUriの文字列を調べてディレクトリ内のメディアファイルを選択する
                    //   プライマリ・ストレージのファイルパスは"/storage/emulated/0/..."となっている
                    //   SDカードのファイルパスは"/storage/xxxx-xxxx/..."となっている
                    val keyStr: String = selectedDirUriReadable
                    if (pathOfUri.contains(keyStr)) {
                        uris.add(queriedUri)
                    }
                }
            }
            query?.close()

            if (uris.size > 0) {
                val playUri = uris[getRandomNum(uris.size)]

                // play audio
                val audioIntent = Intent()
                audioIntent.action = Intent.ACTION_VIEW
                audioIntent.setDataAndType(playUri, "audio/*")
                startActivity(audioIntent)

                // get filepath of playUri
                val proj = arrayOf("_data")
                val cursor = this.getContentResolver().
                    query(playUri, proj, null, null, null)
                if (cursor == null) {
                    textViewFoundFile.text = getString(R.string.not_find)
                } else {
                    val pathColumn = cursor.getColumnIndexOrThrow("_data")
                    cursor.moveToFirst()
                    textViewFoundFile.text =
                        cursor.getString(pathColumn).replace(
                            Regex("^/storage/"), "/"
                        )
                    cursor.close()
                }
            }
        }
    }

    // 0以上、maxNum未満の範囲でランダムな数を1つ返す
    private fun getRandomNum(maxNum: Int): Int {
        val random = Random()
        return random.nextInt(maxNum)
    }

    fun openFilePickerGetUri() {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
            addFlags(
                Intent.FLAG_GRANT_READ_URI_PERMISSION or
                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
                        Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
                        Intent.FLAG_GRANT_PREFIX_URI_PERMISSION )
        }
        fileLauncherGetDir.launch(intent)
    }
}

android開発 ContentResolver URIそのものの取得

2022 May 02.
2022 May 01.

端末

android 11 ( APIレベル 30 )

概要

ContentResolverにてIDを取得し、ContentUris.withAppendedId()でIDからURIを取得する

手順
  • ContentResolverのqueryのprojectionに"MediaStore.Audio.Media._ID"をセットする
  • query結果集合にて getColumnIndexOrThrow(MediaStore.Audio.Media._ID) でID列を取得する(取得列をidColumnとする)
  • 対象URIに対し、getLong(idColumn) でidを取得する
  • ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id) でURIを取得する

    URIファイルシステム上のパスを取得するサンプルkotlinコード
            val collection =
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    MediaStore.Audio.Media.getContentUri(
                        MediaStore.VOLUME_EXTERNAL
                    )
                } else {
                    MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
                }

            val projection = arrayOf(
                MediaStore.Audio.Media._ID,
                "_data"
            )

            val resolver = applicationContext.contentResolver
            val query = resolver.query(
                collection,  //データの種類
                projection, //取得する項目 nullは全部
                selection, //フィルター条件 nullはフィルタリング無し
                selectionArgs, //フィルター用のパラメータ
                null   //並べ替え。nullは並べ替えしない
            )

            query?.use { cursor ->
                val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
                val pathColumn = cursor.getColumnIndexOrThrow("_data")

                while (cursor.moveToNext()) {
                    val id = cursor.getLong(idColumn)
                    var queriedUri = ContentUris.withAppendedId(
                        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id
                    )
                    val pathOfUri = cursor.getString(pathColumn)
                }
            }
            query?.close()

android開発 ContentResolverでメディアファイルのファイルシステム上のパスを取得

2022 May 02.
2022 May 01.

端末 Android 11 (API レベル 30)

* ContentResolverで取得したURIのpathプロパティ(getPath()メソッド)で得られる文字列はファイルシステム上のパスとはまったく異なる
* ContentResolverで取得したURIのtoString()メソッドで得れれる文字列もファイルシステム上のパスとはまったく異なる
* ファイルシステム上のパスの取得は、ContentResolverで、projectionに"_data"を指定してqueryした結果集合にgetColumnIndexOrThrow("_data") として得たカラムの値を当該URIから取得する

サンプルkotlinコード
android開発 外部ストレージ共有領域の特定ディレクトリ内のメディアファイル取得 - rokkonet