2022 Jul. 18.
2022 Feb. 27.
2022 Jan. 30.
最新ソース保管場所 https://bitbucket.org/arsmus/playrandomexternalstoragesound/src/master/
概要
MediaStoreを利用して取得した、共有領域(アプリ固有領域外)の音声ファイルを、インテントによってオーディオプレーヤーで再生する
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 xmlnsandroid="http://schemas.android.com/apk/res/android"
package="net.sytes.rokkosan.playrandomexternalstoragesound">
<uses-permission androidname="android.permission.READ_EXTERNAL_STORAGE" />
<application
androidallowBackup="true"
androidicon="@mipmap/ic_launcher"
androidlabel="@string/app_name"
androidroundIcon="@mipmap/ic_launcher_round"
androidsupportsRtl="true"
androidtheme="@style/Theme.PlayRandomExternalStorageSound">
<activity
androidname=".MainActivity"
androidexported="true">
<intent-filter>
<action androidname="android.intent.action.MAIN" />
<category androidname="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
activity_main.xml
xml version="1.0" encoding="utf-8"
<androidxconstraintlayoutwidgetConstraintLayout xmlnsandroid="http://schemas.android.com/apk/res/android"
xmlnsapp="http://schemas.android.com/apk/res-auto"
xmlnstools="http://schemas.android.com/tools"
androidlayout_width="match_parent"
androidlayout_height="match_parent"
toolscontext=".MainActivity">
<Button
androidid="@+id/btnPlaySound"
androidlayout_width="wrap_content"
androidlayout_height="wrap_content"
androidtext="@string/button_play_sound"
applayout_constraintLeft_toLeftOf="parent"
applayout_constraintRight_toLeftOf="@id/btnReplay"
applayout_constraintTop_toTopOf="parent"
applayout_constraintBottom_toTopOf="@id/tvFilePath"
/>
<Button
androidid="@+id/btnReplay"
androidlayout_width="wrap_content"
androidlayout_height="wrap_content"
androidtext="@string/button_replay"
applayout_constraintBottom_toTopOf="@id/tvFilePath"
applayout_constraintLeft_toRightOf="@id/btnPlaySound"
applayout_constraintRight_toRightOf="parent"
applayout_constraintTop_toTopOf="parent" />
<TextView
androidid="@+id/tvFilePath"
androidlayout_width="match_parent"
androidlayout_height="80dp"
applayout_constraintBottom_toTopOf="@id/tvFileSize"
applayout_constraintLeft_toLeftOf="parent"
applayout_constraintRight_toRightOf="parent"
applayout_constraintTop_toBottomOf="@id/btnPlaySound"
/>
<TextView
androidid="@+id/tvFileSize"
androidlayout_width="match_parent"
androidlayout_height="wrap_content"
applayout_constraintLeft_toLeftOf="parent"
applayout_constraintRight_toRightOf="parent"
applayout_constraintTop_toBottomOf="@id/tvFilePath"
applayout_constraintBottom_toBottomOf="parent"
/>
</androidxconstraintlayoutwidgetConstraintLayout>
played_files_list.xml
xml version="1.0" encoding="utf-8"
<androidxconstraintlayoutwidgetConstraintLayout xmlnsandroid="http://schemas.android.com/apk/res/android"
androidlayout_width="match_parent"
androidlayout_height="match_parent">
<ListView
androidid="@+id/listViewFiles"
androidlayout_width="match_parent"
androidlayout_height="match_parent" />
</androidxconstraintlayoutwidgetConstraintLayout>
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 xmlnsandroid="http://schemas.android.com/apk/res/android">
<item
androidid="@+id/playHistory"
androidtitle="@string/playHistoryTitle"
/>
</menu>
MainActivity.kt
package net.sytes.rokkosan.playrandomexternalstoragesound
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)
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
)
}
}
private fun replayStorageAudioFile() {
if ( isGrantedReadStorage()) {
replayStorageMedia()
} else {
requestPermissions(
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
ReplayPermissionReadExStor
)
}
}
private fun isGrantedReadStorage(): Boolean {
return (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED)
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
PlayPermissionReadExStor -> {
if (grantResults.isEmpty()) {
throw RuntimeException("Empty permission result")
}
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getPlayStorageMedia()
} else {
if (shouldShowRequestPermissionRationale(
Manifest.permission.READ_EXTERNAL_STORAGE)) {
requestPermissions( arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PlayPermissionReadExStor)
} else {
Toast.makeText(applicationContext, R.string.noPermission, Toast.LENGTH_LONG).show()
}
}
}
ReplayPermissionReadExStor -> {
if (grantResults.isEmpty()) {
throw RuntimeException("Empty permission result")
}
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
replayStorageMedia()
} else {
if (shouldShowRequestPermissionRationale(
Manifest.permission.READ_EXTERNAL_STORAGE)) {
requestPermissions( arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), ReplayPermissionReadExStor)
} else {
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"
)
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,
selection,
selectionArgs,
null
)
val numCount = query?.count
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
val audioIntent = Intent()
audioIntent.action = Intent.ACTION_VIEW
audioIntent.setDataAndType(Uri.parse(pathOfUri), "audio/*")
startActivity(audioIntent)
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() {
var playedFiles = ""
lateinit var br: BufferedReader
try {
val savePathFile = File(this.filesDir, getString(R.string.pathHistoryFile))
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]
val audioIntent = Intent()
audioIntent.action = Intent.ACTION_VIEW
audioIntent.setDataAndType(Uri.parse(playedFileList[0]), "audio/*")
startActivity(audioIntent)
}
}
private fun getRandomNum(maxNum: Int): Int {
val random = Random()
return random.nextInt(maxNum)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
val inflater = menuInflater
inflater.inflate(R.menu.option, menu)
return true
}
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.*
class ListPlayedFilesDialogFragment: DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(activity)
val inflater = requireActivity().layoutInflater
val myDialogView = inflater.inflate(R.layout.played_files_list, null)
var playedFiles = ""
lateinit var br: BufferedReader
try {
val savePathFile = File(this.context?.filesDir, getString(R.string.pathHistoryFile))
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()
val myAdapter =
ArrayAdapter(this.requireContext(), android.R.layout.simple_list_item_1, playedFileList)
val listViewFiles = myDialogView.findViewById<ListView>(R.id.listViewFiles)
listViewFiles.adapter = myAdapter
builder.setView(myDialogView)
.setTitle(getString(R.string.PlayHistoryTitle))
.setPositiveButton("Clear") { dialog, which ->
try {
File( context?.filesDir, getString(R.string.pathHistoryFile)).writeText(",_,_")
} catch (e: Exception) {
Toast.makeText(context,e.toString(), Toast.LENGTH_LONG ).show()
}
}
.setNeutralButton("OK") { _, _ ->
}
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 androidname="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