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
何も配置しない