rokkonet

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

android開発 指定ディレクトリの音声ファイル・動画ファイルをランダムに再生するアプリ(ViewModel非対応。縦画面固定)

2022 Jul. 20.

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


AndroidManifest.xml

<?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:screenOrientation="portrait"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>


layout: 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/textViewDir"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

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

    <Button
        android:id="@+id/buttonChangeDir"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/button_change_dir"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textViewDir"
        app:layout_constraintBottom_toTopOf="@id/textViewMediaPath"
        />

    <TextView
        android:id="@+id/textViewMediaPath"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@id/textViewFileSize"
        app:layout_constraintTop_toBottomOf="@id/buttonChangeDir"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        />

    <TextView
        android:id="@+id/textViewFileSize"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@id/spinnerMediaType"
        app:layout_constraintTop_toBottomOf="@id/textViewMediaPath"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        />

    <Spinner
        android:id="@+id/spinnerMediaType"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:entries="@array/spinner_items_media_type"
        android:minHeight="48dp"
        app:layout_constraintBottom_toTopOf="@id/buttonPlay"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textViewFileSize"
        />

    <Button
        android:id="@+id/buttonPlay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/button_play"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/buttonReplay"
        app:layout_constraintTop_toBottomOf="@id/textViewMediaPath" />

    <Button
        android:id="@+id/buttonReplay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/button_replay"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@id/buttonPlay"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textViewMediaPath" />
</androidx.constraintlayout.widget.ConstraintLayout>


layout: played_files_list.xml

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

    <ListView
        android:id="@+id/listViewFiles"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>


menu: option.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/playHistory"
        android:title="@string/playHistoryTitle"
    />
</menu>


values: arrays.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="spinner_items_media_type">
        <item>Video_and_Sound</item>
        <item>Sound</item>
        <item>Video</item>
    </string-array>
</resources>


values: strings.xml

<resources>
    <string name="app_name">RandomPlayer</string>
    <string name="button_play">Play</string>
    <string name="button_replay">Replay</string>
    <string name="button_change_dir">Change_Directory</string>
    <string name="gotLocalAudioFile">Got an audio file from local device</string>
    <string name="kb">%sKB</string>
    <string name="sentIntent">Sent Intent for playing audio</string>
    <string name="havePermission">Allowed to read storage</string>
    <string name="noPermission">No Permission to read storage</string>
    <string name="playHistoryTitle">Play History</string>
    <string name="pathHistoryFile">path_history.txt</string>
    <string name="noHistory">No history</string>
    <string name="no_dirs">No directories</string>
    <string name="volume_dir_key">volumeDir</string>
    <string name="relative_dir_key">relativeDir</string>
    <string name="illegal_typeMedia">Illegal typeMedia</string>
    <string name="storage_top">/srorage/</string>
    <string name="external_primary">external_primary</string>
    <string name="emulated0">emulated/0</string>
    <string name="not_find">File not found</string>
    <string name="not_selected">Media type is not selected</string>
    <string name="reason_for_permission">ストレージファイル読み込みに読込権限の許可が必要です。設定を開きますか?</string>
    <string name="ok">OK</string>
    <string name="dismiss">Dismiss</string>
    <string name="selected_path">/%1$s/%2$s</string>
</resources>


MainActivity.kt

package net.sytes.rokkosan.randomplayer

/*
2022 Jul. 20.
2022 Jun. 25.
2022 May 15.
2022 Apr. 30.
2022 Feb. 23.
Ryuichi Hashimoto.

端末内の、指定ディレクトリ内の、動画もしくは音声ファイルを再生するアプリ

起動時にファイル読み込みパーミッションを取得する。
メイン画面に配置するウィジェット
 「読み込みディレクトリ選択」
 「音声ファイルのみ・ビデオファイルのみ・音声+ビデオファイル選択」
 「再生」
 「リプレイ」
*/

import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.view.Menu
import android.view.MenuItem
import android.widget.ArrayAdapter
import android.widget.Spinner
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import net.sytes.rokkosan.randomplayer.databinding.ActivityMainBinding
import net.sytes.rokkosan.randomsmbsoundplay.ListPlayedFilesDialogFragment
import java.io.File
import java.io.FileWriter
import java.io.IOException
import java.util.*
import kotlin.properties.Delegates

class MainActivity : AppCompatActivity() {
    lateinit var binding: ActivityMainBinding
    private var selectedVolumeDir: String = ""
    private var selectedRelativeDir: String = ""
    private val sharedPref: SharedPreferences by lazy {this.getPreferences(Context.MODE_PRIVATE)}
    private val myPermission = Manifest.permission.READ_EXTERNAL_STORAGE
    private var isCheckedNotAskAgain: Boolean = false
    val spinnerMediaType: Spinner by lazy { binding.spinnerMediaType }
    lateinit var typeOfMedia: MediaMyEnumType
    private lateinit var appContext: Context
    private var medType: Int by Delegates.notNull()
    private var medPath: String = ""
    private val audioInt = 2
    private val videoInt = 3
        // mediaType  0:none 1:image 2:audio 3:video 4:playlist 5:subtitle 6:document

    enum class MediaMyEnumType {
        SoundVideo, Sound, Video
    }

    // ファイル読み込みパーミッション取得
    //   registerForActivityResult()を定義してlaunch()を呼び出すと、
    //   パーミッションが許可されていない時はユーザーにパーミッション許可を促すダイアログが表示される。
    //   パーミッションが「許可された時」「許可されなかった時」のコールバック関数を記述する
    private val permisLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->
        if (granted) {
            // パーミッションが取得されている時
            // パーミッション取得後にすべき処理に移る
            isCheckedNotAskAgain = false
            sharedPref.edit().putBoolean("isCheckedNotAskAgain", isCheckedNotAskAgain).apply()
            workWithPermission()

        } else {
            // パーミッションを取得していない
            if (shouldShowRequestPermissionRationale(myPermission)) {
                // パーミッション取得を拒否され、
                // 「今後表示しない」は選択されていない
                isCheckedNotAskAgain = false
                sharedPref.edit().putBoolean("isCheckedNotAskAgain", isCheckedNotAskAgain).apply()
            } else {
                // パーミッション取得を拒否され、
                // 「今後表示しない」が選択されて再リクエストも拒否されている
                isCheckedNotAskAgain = true
                sharedPref.edit().putBoolean("isCheckedNotAskAgain", isCheckedNotAskAgain).apply()
            }
            workWithoutPermission()
        }
    }

    /*
     * ファイル・ピッカーによるディレクトリ選択
     *    ContentResolver.queryでの利用に合わせ、
     *    MediaStore.Files.FileColumns.VOLUME_NAME に当たる文字列を selectedVolumeDir に
     *    MediaStore.Files.FileColumns.RELATIVE_PATH に当たる文字列を selectedRelativeDir に
     *    格納する
     *    取得した文字列の画面表示も行う
     */
    private val fileLauncherGetDir = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult() )
    { result -> // 結果を受け取るルーチン
        if (result.resultCode == RESULT_OK) {
            // succeeded.
            // get uri of directory
            val selectedDirUri = result.data?.data ?: throw  Exception()

            /*
                uri冒頭の"content://com.android.externalstorage.documents/tree"を削除し、
                エンコードされた /(スラッシュ)を戻す。
                ContentPreviderに合わせ、VolumeとRelativeの文字列を取得する。
                プライマリ・ストレージ文字列は"/external_primary/"に置き換える。
             */
            val selectedDirString = selectedDirUri.toString().
            replaceFirst("content://com.android.externalstorage.documents/tree", "")

            selectedVolumeDir = selectedDirString.
            substring(0, selectedDirString.indexOf("%3A")+3 )
            if (selectedVolumeDir.indexOf("/primary%3A") > -1) {
                // primary volume
                selectedVolumeDir = selectedVolumeDir.
                    replaceFirst("/primary%3A", "external_primary")

            } else if (selectedVolumeDir.indexOf("%3A") > -1) {
                // SD card
                selectedVolumeDir = selectedVolumeDir.
                    replaceFirst("/", "").replaceFirst("%3A", "")
            }

            selectedRelativeDir = selectedDirString.
                substring( selectedDirString.indexOf("%3A")).
                replace("%3A", "" )
            if (selectedRelativeDir.indexOf("%2F") > -1) {
                selectedRelativeDir = selectedRelativeDir.replace("%2F", "/")
            }
            // Relativeの末尾を"/"にしておく
            if (selectedRelativeDir.takeLast(1) != "/" ) {
                selectedRelativeDir = selectedRelativeDir + "/"
            }

            // 取得した文字列を保存
            sharedPref.edit().
                putString(getString(R.string.volume_dir_key), selectedVolumeDir).
                putString(getString(R.string.relative_dir_key), selectedRelativeDir).
                apply()
        } else {
            // failed
            selectedVolumeDir = ""
            selectedRelativeDir = ""
        }

        binding.textViewDir.text =
            getString(R.string.selected_path, selectedVolumeDir, selectedRelativeDir)
    }

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

        appContext = applicationContext
        getFileReadPermission()
    }

    /*
     * パーミッションを取得する
     *   パーミッションが取得されれば workWithPermission() を実行する
     *   パーミッションが取得されなければ workWithoutPermission() を実行する
    */
    private fun getFileReadPermission() {
        if ( ContextCompat.checkSelfPermission(this, myPermission) ==
            PackageManager.PERMISSION_GRANTED) {
            // 既にパーミッションが取得されている時
            // パーミッション取得後にすべき処理に移る
            isCheckedNotAskAgain = false
            sharedPref.edit().putBoolean("isCheckedNotAskAgain", isCheckedNotAskAgain).apply()
            workWithPermission()
        } else {
            // パーミッション未取得
            if (isCheckedNotAskAgain) {
                // 過去に権限取得を拒否され、「今後表示しない」が選択されて再リクエストも拒否されている
                // ダイアログでユーザーに説明し、ユーザーが望めば設定画面を表示する
                val openConfigDialogFragment = OpenConfigDialogFragment()
                openConfigDialogFragment.show(supportFragmentManager, "simple")

                // ダイアログ後の処理
                if ( ContextCompat.checkSelfPermission(this, myPermission) ==
                    PackageManager.PERMISSION_GRANTED) {
                    // (設定画面で)パーミッションを取得している
                    isCheckedNotAskAgain = false
                    sharedPref.edit().putBoolean("isCheckedNotAskAgain", isCheckedNotAskAgain).
                        apply()
                    workWithPermission()
                } else {
                    // パーミッション未取得。
                    // shouldShowRequestPermissionRationale()に応じて
                    // isCheckedNotAskAgainを設定し保存する
                    if (shouldShowRequestPermissionRationale(myPermission)) {
                        // 「今後表示しない」は選択されていない
                        isCheckedNotAskAgain = false
                        sharedPref.edit().putBoolean("isCheckedNotAskAgain", isCheckedNotAskAgain).
                            apply()
                    } else {
                        // 「今後表示しない」が選択されている
                        isCheckedNotAskAgain = true
                        sharedPref.edit().putBoolean("isCheckedNotAskAgain", isCheckedNotAskAgain).
                            apply()
                    }
                    workWithoutPermission()
                }
            } else {
                // パーミッションを取得しておらず、
                // 過去に権限取得で再リクエストの拒否はしていない。
                permisLauncher.launch(myPermission)
            }
        }
    }

    // ファイル読み込みパーミッション取得後の処理
    private fun workWithPermission() {
        binding.textViewMediaPath.text = getString(R.string.havePermission)

        /* get directory in external storage
         *    ContentResolver.queryでの利用に合わせ、
         *    MediaStore.Files.FileColumns.VOLUME_NAME に当たる文字列を selectedVolumeDir に
         *    MediaStore.Files.FileColumns.RELATIVE_PATH に当たる文字列を selectedRelativeDir に
         *    格納する
         */

        // 保存されたディレクトリ情報を取り出す
        selectedVolumeDir = sharedPref.getString(getString(R.string.volume_dir_key), "") ?: throw Exception()
        selectedRelativeDir = sharedPref.getString(getString(R.string.relative_dir_key), "") ?: throw Exception()

        // ディレクトリ情報が保存されていなければファイル・ピッカーでディレクトリを選択する
        if (selectedVolumeDir.equals("") || selectedRelativeDir.equals("")) {
            selectDirWithFilePicker()
        }

        binding.textViewDir.text =
            getString(R.string.selected_path, selectedVolumeDir, selectedRelativeDir)

        // ディレクトリ情報が初期取得できた後の処理
        if (!selectedVolumeDir.equals("") && !selectedRelativeDir.equals("")) {

            // ユーザーによるディレクトリ変更
            binding.buttonChangeDir.setOnClickListener {
                selectDirWithFilePicker()
            }

            // select media type to be played with spinner in layout
            //     音声のみ、動画のみ、音声+動画 から選択する
            setAdapterSpinner()
            spinnerMediaType.onItemSelectedListener = CustomItemSelectedListener(this)

            // 再生
            binding.buttonPlay.setOnClickListener {
                binding.textViewMediaPath.text = ""
                binding.textViewFileSize.text = ""
                playMediaInDir(typeOfMedia)
            }

            // Replay
            binding.buttonReplay.setOnClickListener {
                val mediaIntent = Intent()
                mediaIntent.action = Intent.ACTION_VIEW
                if (medType == audioInt) {
                    mediaIntent.setDataAndType(Uri.parse(medPath), "audio/*")
                } else if (medType == videoInt) {
                    mediaIntent.setDataAndType(Uri.parse(medPath), "video/*")
                } else {
                    binding.textViewMediaPath.text = getString(R.string.illegal_typeMedia)
                }
                if (medType == audioInt || medType == videoInt) {
                    startActivity(mediaIntent)
                }
            }
        }
    }

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

    private fun workWithoutPermission() {
        binding.textViewMediaPath.text = getString(R.string.noPermission)
    }

    // ファイル・ピッカーでディレクトリを選択し、パス文字列情報を取得する
    // パス文字列情報の画面への表示も行う
    private fun selectDirWithFilePicker() {
        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)
    }

    /*
     * Play media.
     * Int typeMedia: Type of media to play
     */
    @RequiresApi(Build.VERSION_CODES.Q)
    private fun playMediaInDir(typeMedia: MediaMyEnumType) {
        lateinit var selectionClause: String

        /*
         * get uri of audio media and video media in the directory
         */
        val contentResolver = this.contentResolver
        val proj = arrayOf(
            MediaStore.Files.FileColumns.MEDIA_TYPE,
            MediaStore.Files.FileColumns.SIZE,
            "_data"
        )

        when(typeMedia) {
            MediaMyEnumType.SoundVideo -> { // audio & video
                selectionClause = "(" +
                        MediaStore.Files.FileColumns.MEDIA_TYPE + " = " +
                        MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO +
                        " OR " +
                        MediaStore.Files.FileColumns.MEDIA_TYPE + " = " +
                        MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO +
                        ")" +
                        " AND " +
                        "(" +
                        MediaStore.Files.FileColumns.VOLUME_NAME + " LIKE ? " +
                        " AND " +
                        MediaStore.Files.FileColumns.RELATIVE_PATH + " LIKE ? " +
                        ")"
            }
            MediaMyEnumType.Sound -> { // audio
                selectionClause = "(" +
                        MediaStore.Files.FileColumns.MEDIA_TYPE + " = " +
                        MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO +
                        ")" +
                        " AND " +
                        "(" +
                        MediaStore.Files.FileColumns.VOLUME_NAME + " LIKE ? " +
                        " AND " +
                        MediaStore.Files.FileColumns.RELATIVE_PATH + " LIKE ? " +
                        ")"
            }
            MediaMyEnumType.Video -> { // video
                selectionClause = "(" +
                        MediaStore.Files.FileColumns.MEDIA_TYPE + " = " +
                        MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO +
                        ")" +
                        " AND " +
                        "(" +
                        MediaStore.Files.FileColumns.VOLUME_NAME + " LIKE ? " +
                        " AND " +
                        MediaStore.Files.FileColumns.RELATIVE_PATH + " LIKE ? " +
                        ")"
            }
            else -> {throw java.lang.Exception(getString(R.string.illegal_typeMedia))}
        }

        val selectionArgs = arrayOf(selectedVolumeDir, "$selectedRelativeDir%")

        val query = contentResolver.query(
            MediaStore.Files.getContentUri("external"),
            //MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
            proj,
            selectionClause,
            selectionArgs,
            null
        )
        val numCount = query?.count
        query?.use { cursor ->
            if (cursor.count < 1) {
                binding.textViewMediaPath.text = getString(R.string.not_find)
            } else {
                cursor.moveToPosition(getRandomNum(numCount!!))
                medType =
                    cursor.
                        getInt(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE))
                val medSize =
                    cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE))
                medPath = cursor.getString(cursor.getColumnIndexOrThrow("_data"))

                // play media
                val mediaIntent = Intent()
                mediaIntent.action = Intent.ACTION_VIEW
                if (medType == audioInt) {
                    mediaIntent.setDataAndType(Uri.parse(medPath), "audio/*")
                } else if (medType == videoInt) {
                    mediaIntent.setDataAndType(Uri.parse(medPath), "video/*")
                } else {
                    binding.textViewMediaPath.text = getString(R.string.illegal_typeMedia)
                }
                if (medType == audioInt || medType == videoInt) {
                    binding.textViewMediaPath.text = medPath
                    binding.textViewFileSize.text =
                        getString(R.string.kb, (medSize / 1000L).toString())
                    startActivity(mediaIntent)

                    // save audio-file-path to storage
                    lateinit var fw: FileWriter
                    val savePathFile =
                        File(appContext.filesDir, getString(R.string.pathHistoryFile))
                    if (savePathFile.exists()) {
                        fw = FileWriter(savePathFile, true)
                    } else {
                        fw = FileWriter(savePathFile)
                    }
                    try {
                        fw.write("$medPath,")
                    } catch(e: IOException) {
                        binding.textViewMediaPath.text = e.toString()
                    } finally {
                        try {
                            fw.close()
                        } catch(e: IOException) {
                            binding.textViewMediaPath.text = e.toString()
                        }
                    }
                }
            }
        }
        query?.close()
    }

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

    // option menu
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        super.onCreateOptionsMenu(menu)
        val inflater = menuInflater
        inflater.inflate(R.menu.option, menu)
        return true
    }

    //Option menuのitemがクリックされた時の動作
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId) {
            R.id.playHistory -> {
                val playedFilesDialog = ListPlayedFilesDialogFragment()
                playedFilesDialog.show(supportFragmentManager, "simple")
            }
        }
        return super.onOptionsItemSelected(item)
    }
}


OpenConfigDialogFragment.kt

package net.sytes.rokkosan.randomplayer

import android.app.AlertDialog
import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import androidx.fragment.app.DialogFragment

class OpenConfigDialogFragment: DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return activity?.let {
            val builder = AlertDialog.Builder(it)
            builder.setMessage(R.string.reason_for_permission)
                .setPositiveButton(R.string.ok,
                    DialogInterface.OnClickListener { dialog, id ->
                        // このアプリの設定画面への暗黙的インテントを用意する
                        val packageName = context?.applicationContext?.packageName
                        val intent = Intent(
                            Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
                            Uri.parse("package:${packageName}")
                        )
                        // 新しいタスクで設定画面を開く
                        //   https://developer.android.google.cn/guide/components/activities/tasks-and-back-stack?hl=ja
                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        startActivity(intent)
                    })
                .setNegativeButton(R.string.dismiss,
                    DialogInterface.OnClickListener { dialog, id ->
                        // User cancelled the dialog
                    })
            // Create the AlertDialog object and return it
            builder.create()
        } ?: throw IllegalStateException("Activity cannot be null")
    }
}


CustomItemSelectedListener.kt

package net.sytes.rokkosan.randomplayer

import android.view.View
import android.widget.AdapterView

class CustomItemSelectedListener(private val mainActivity: MainActivity) :
    AdapterView.OnItemSelectedListener {
    override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
        when (mainActivity.spinnerMediaType.selectedItemPosition) {
            0 -> { mainActivity.typeOfMedia = MainActivity.MediaMyEnumType.SoundVideo }
            1 -> { mainActivity.typeOfMedia = MainActivity.MediaMyEnumType.Sound }
            2 -> { mainActivity.typeOfMedia = MainActivity.MediaMyEnumType.Video }
            else -> { mainActivity.binding.textViewMediaPath.text =
                mainActivity.getString(R.string.illegal_typeMedia) }
        }
    }
    override fun onNothingSelected(parent: AdapterView<*>?) {
        mainActivity.binding.textViewMediaPath.text = mainActivity.getString(R.string.not_selected)
    }
}


ListPlayedFilesDialogFragment.kt

package net.sytes.rokkosan.randomsmbsoundplay

// 2022 Feb. 13.
// 2022 Feb. 05.

// Ryuichi Hashimoto

import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import android.app.AlertDialog
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.Toast
import net.sytes.rokkosan.randomplayer.R
import java.io.*

class ListPlayedFilesDialogFragment: DialogFragment(){
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        // get Dialog
        val builder = AlertDialog.Builder(activity)
        val inflater = requireActivity().layoutInflater
        val myDialogView = inflater.inflate(R.layout.played_files_list, null)

        // get array of played files from saved file
        var playedFiles = ""
        lateinit var br: BufferedReader
        try {
            val savePathFile = File(this.context?.filesDir, getString(R.string.pathHistoryFile))
            // savePathFile has only one line.
            // the line is filePath data in which each datum is separated by ','.
            if (savePathFile.exists()) {
                br = BufferedReader(FileReader(savePathFile))
                try {
                    playedFiles = br.readLine()
                } catch(e: IOException) {
                    playedFiles = e.toString()
                } finally {
                    try {
                        br.close()
                    } catch(e: IOException) {
                        playedFiles = playedFiles + " " + e.toString()
                    }
                }
            }
        } catch (e: Exception){
            playedFiles = playedFiles + " " + e.toString()
        }
        if (playedFiles == "") {
            playedFiles = getString(R.string.noHistory)
        }
        playedFiles = playedFiles.removeSuffix(",")
        val playedFileList = playedFiles.split(",").asReversed()

        // システムに組み込まれた"android.R.layout.simple_list_item_1"を介して、
        // playedFileListをArrayAdapterに関連づける
        val myAdapter =
            ArrayAdapter(this.requireContext(), android.R.layout.simple_list_item_1, playedFileList)

        // ArrayAdapterをダイアログ上のレイアウト内のListViewにセットする
        val listViewFiles = myDialogView.findViewById<ListView>(R.id.listViewFiles)
        listViewFiles.adapter = myAdapter

        // display dialog
        builder.setView(myDialogView)
            .setTitle("Played Files")
            .setPositiveButton("Clear") { dialog, which ->
                lateinit var fos: FileOutputStream
                try {
                    val savePathFile =
                        File( context?.filesDir, getString(R.string.pathHistoryFile))
                    fos = FileOutputStream(savePathFile)
                    fos.write(','.code.toInt())
                } catch (e: Exception) {
                    Toast.makeText(context,e.toString(), Toast.LENGTH_LONG ).show()
                } finally {
                    fos.close()
                }
            }
            .setNeutralButton("OK") { _, _ ->
                // nothing is done
            }
        return builder.create()
    }
}