rokkonet

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

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)
    }
}