rokkonet

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

android開発 端末内の音声ファイルをランダムに再生する

2022 Jul. 18.
2022 Feb. 27.
2022 Jan. 30.

最新ソース保管場所 https://bitbucket.org/arsmus/playrandomexternalstoragesound/src/master/

概要

MediaStoreを利用して取得した、共有領域(アプリ固有領域外)の音声ファイルを、インテントによってオーディオプレーヤーで再生する

インストールしたandroid端末

android バージョン 7
android バージョン 11

コンパイル環境

compileSdkVersion 31
minSdkVersion 24
targetSdkVersion 31

ソース

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

android {
    compileSdk 31

    defaultConfig {
        applicationId "net.sytes.rokkosan.playrandomexternalstoragesound"
        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

ストレージ読み込み許可を記述する
uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"

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

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


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">

    <Button
        android:id="@+id/btnPlaySound"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/button_play_sound"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/btnReplay"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/tvFilePath"
        />

    <Button
        android:id="@+id/btnReplay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/button_replay"
        app:layout_constraintBottom_toTopOf="@id/tvFilePath"
        app:layout_constraintLeft_toRightOf="@id/btnPlaySound"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tvFilePath"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        app:layout_constraintBottom_toTopOf="@id/tvFileSize"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btnPlaySound"
        />

    <TextView
        android:id="@+id/tvFileSize"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tvFilePath"
        app:layout_constraintBottom_toBottomOf="parent"
        />
</androidx.constraintlayout.widget.ConstraintLayout>


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>


string.xml
<resources>
    <string name="app_name">PlayRandomExternalStorageSound</string>
    <string name="button_play_sound">Play</string>
    <string name="button_replay">Replay</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="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="PlayHistoryTitle">Played Files</string>
    <string name="noPlayedFile">No played sound file</string>
</resources>


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>


MainActivity.kt
package net.sytes.rokkosan.playrandomexternalstoragesound

// 2022 Feb. 27.
// 2022 Jan. 30.
// Ryuichi Hashimoto.

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.MediaStore
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import net.sytes.rokkosan.playrandomexternalstoragesound.databinding.ActivityMainBinding
import java.io.*
import java.util.*
import java.util.concurrent.TimeUnit

class MainActivity : AppCompatActivity() {
    companion object {
        private const val PlayPermissionReadExStor: Int = 100
        private const val ReplayPermissionReadExStor: Int = 110
    }

    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)

        // アクションバーに表示する文字列を設定
        supportActionBar?.title = "Play Sound in device"

        binding.btnPlaySound.setOnClickListener {
            playStorageAudioFile()
        }
        binding.btnReplay.setOnClickListener {
            replayStorageAudioFile()
        }
    }

    private fun playStorageAudioFile() {
        // パーミッションがあれば処理を進める。無ければ取得する
        if ( isGrantedReadStorage()) {
            getPlayStorageMedia()
        } else {
            requestPermissions(
                arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
                PlayPermissionReadExStor
            )
            // requestPermissions()の結果に応じた処理がonRequestPermissionsResult()で行われる
        }
    }

    private fun replayStorageAudioFile() {
        // パーミッションがあれば処理を進める。無ければ取得する
        if ( isGrantedReadStorage()) {
            replayStorageMedia()
        } else {
            requestPermissions(
                arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
                ReplayPermissionReadExStor
            )
            // requestPermissions()の結果に応じた処理がonRequestPermissionsResult()で行われる
        }

    }

    // アプリにパーミッションが付与されているかどうかを確認するメソッド
    private fun isGrantedReadStorage(): Boolean {
        return (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
                == PackageManager.PERMISSION_GRANTED)
    }

    // requestPermissions()の結果に対する処理を行うメソッド
    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when (requestCode) {
            PlayPermissionReadExStor -> {
                if (grantResults.isEmpty()) {
                    // STOP WORK
                    throw RuntimeException("Empty permission result")
                }
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    // permission is got.
                    // YOU CAN CONTINUE YOUR WORK WITH EXTERNAL_STORAGE HERE
                    getPlayStorageMedia()

                } else {  // no permission is got
                    if (shouldShowRequestPermissionRationale(
                            Manifest.permission.READ_EXTERNAL_STORAGE)) {
                        // permission request was denied.
                        // User declined, but system can still ask for more
                        // request permission again
                        requestPermissions( arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PlayPermissionReadExStor)
                    } else {
                        // permission request was denied and requested "no not ask".
                        // User declined and i can't ask.
                        // CODES in case of rejection to get permission here
                        Toast.makeText(applicationContext, R.string.noPermission, Toast.LENGTH_LONG).show()
                    }
                }
            }
            ReplayPermissionReadExStor -> {
                if (grantResults.isEmpty()) {
                    // STOP WORK
                    throw RuntimeException("Empty permission result")
                }
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    // permission is got.
                    // YOU CAN CONTINUE YOUR WORK WITH EXTERNAL_STORAGE HERE
                    replayStorageMedia()
                } else {  // no permission is got
                    if (shouldShowRequestPermissionRationale(
                            Manifest.permission.READ_EXTERNAL_STORAGE)) {
                        // permission request was denied.
                        // User declined, but system can still ask for more
                        // request permission again
                        requestPermissions( arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), ReplayPermissionReadExStor)
                    } else {
                        // permission request was denied and requested "no not ask".
                        // User declined and i can't ask.
                        // CODES in case of rejection to get permission here
                        Toast.makeText(applicationContext, R.string.noPermission, Toast.LENGTH_LONG).show()
                    }
                }
            }
        }
    }

    private fun getPlayStorageMedia() {
        var pathOfUri = ""

        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 columns = arrayOf(
            MediaStore.Audio.Media._ID,
            MediaStore.Audio.Media.SIZE,
            "_data"
        )

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

        val resolver = applicationContext.contentResolver
        val query = resolver.query(
            collection,  //データの種類
            columns, //取得する項目 nullは全部
            selection, //フィルター条件 nullはフィルタリング無し
            selectionArgs, //フィルター用のパラメータ
            null   //並べ替え
        )
        // Log.d( "MyApp" , Arrays.toString( query?.getColumnNames() ) )  //項目名の一覧を出力
        val numCount = query?.count
        // Log.d("MyApp", "Num raws : $numCount")

        query?.use { cursor ->
            val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
            val columnIndex = cursor.getColumnIndexOrThrow("_data")

            cursor.moveToPosition(getRandomNum(numCount!!))
            val mediaSize = cursor.getInt(sizeColumn)
            pathOfUri = cursor.getString(columnIndex)
            binding.tvFilePath.text = pathOfUri
            val sizeKb = (mediaSize / 1000 ).toString() + "KB"
            binding.tvFileSize.text = sizeKb

            // play audio
            val audioIntent = Intent()
            audioIntent.action = Intent.ACTION_VIEW
            audioIntent.setDataAndType(Uri.parse(pathOfUri), "audio/*")
            // audioIntent.setDataAndType(contentUri, "audio/*")  <- NG
            // audioIntent.setPackage("org.videolan.vlc")  <- If you want
            startActivity(audioIntent)

            // save audio-file-path to storage
            lateinit var fw: FileWriter
            val savePathFile =
                File(applicationContext.filesDir, getString(R.string.pathHistoryFile))

            fw = if (savePathFile.exists()) {
                FileWriter(savePathFile, true)
            } else {
                FileWriter(savePathFile)
            }
            try {
                fw.write("${pathOfUri},_,_")
            } catch(e: IOException) {
                binding.tvFilePath.text = e.toString()
            } finally {
                try {
                    fw.close()
                } catch(e: IOException) {
                    binding.tvFilePath.text = e.toString()
                }
            }
        }
        query?.close()
        return
    }

    private fun replayStorageMedia() {
        // read play-history from saved file
        var playedFiles = ""
        lateinit var br: BufferedReader
        try {
            val savePathFile = File(this.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"
                    }
                }
            } else {
                binding.tvFilePath.setText(R.string.noPlayedFile)
            }
        } catch (e: Exception){
            playedFiles = "$playedFiles $e"
        }
        playedFiles = playedFiles.removePrefix(",_,_")
        playedFiles = playedFiles.removeSuffix(",_,_")
        if ( "" == playedFiles ) {
            binding.tvFilePath.setText(R.string.noPlayedFile)
        } else {
            val playedFileList = playedFiles.split(",_,_").asReversed()
            binding.tvFilePath.text = playedFileList[0]

            // play audio
            val audioIntent = Intent()
            audioIntent.action = Intent.ACTION_VIEW
            audioIntent.setDataAndType(Uri.parse(playedFileList[0]), "audio/*")
            // audioIntent.setDataAndType(contentUri, "audio/*")  <- NG
            // audioIntent.setPackage("org.videolan.vlc")  <- If you want
            startActivity(audioIntent)
        }
    }

    // 0以上、maxNum未満の範囲でランダムな数を1つ返す
    private fun getRandomNum(maxNum: Int): Int {
        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)
    }

    // 画面回転対応
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        val tvFilePath = binding.tvFilePath.text.toString()
        val tvFileSize = binding.tvFileSize.text.toString()
        outState.putString("tvFilePath", tvFilePath)
        outState.putString("tvFileSize", tvFileSize)
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        binding.tvFilePath.text = savedInstanceState.getString("tvFilePath", "")
        binding.tvFileSize.text = savedInstanceState.getString("tvFileSize", "")
    }
}


ListPlayedFilesDialogFragment
package net.sytes.rokkosan.playrandomexternalstoragesound

import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import java.io.*

// 2022 Feb. 26.
// 2022 Feb. 14.

// Ryuichi Hashimoto

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 filePath data which is 1 line data, having no NewLine character.
            // Each datum in the line 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.removePrefix(",_,_")
        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(getString(R.string.PlayHistoryTitle))
            .setPositiveButton("Clear") { dialog, which ->
                // Clear play-history in file.
                //   Only ',_,_' will be saved in file.
                try {
                    File( context?.filesDir, getString(R.string.pathHistoryFile)).writeText(",_,_")
                } catch (e: Exception) {
                    Toast.makeText(context,e.toString(), Toast.LENGTH_LONG ).show()
                }
            }
            .setNeutralButton("OK") { _, _ ->
                // nothing is done
            }

        return builder.create()
    }
}




以下は、旧記事

2021 Jul. 14.
2021 Jul. 06.

最も単純な音楽再生アプリ

アプリを起動すると自動的に外部ストレージの共有領域からランダムに1ファイルを再生する。
起動後、フォアグラウンドにある間だけ動作すると思ったが、他のアプリを起動しても音楽再生が継続されているし、画面オフになっても再生継続している。
アプリがフォアグラウンドにない時はデバイスを回転しても再生が継続している。
バイスを回転してアクティビティを破壊し再起動すると別のファイルが再生される。
アプリの終了(再生中の終了)は、「Androidシステムの設定メニュー→アプリ」から終了を行う。

確認端末

android API 29 (android version 10)

コンパイル環境

compileSdkVersion 30
minSdkVersion 24
targetSdkVersion 30


AndroidManifest.xml

READ_EXTERNAL_STORAGEパーミッションの利用を記述する

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



activity_main.xml

何も配置しない

MainActivity.kt

コード保管ページ
https://bitbucket.org/arsmus/playsimpleaudio/src/master/app/src/main/java/net/sytes/rokkosan/playsimpleaudio/MainActivity.kt