【Android】MediaPlayerで音楽プレイヤ―を作ってみた。（Kotlin版）

２版　2024年11月22日　[再生経過時間を表示する機能の追加](https://qiita.com/guijiu/items/7776c74168b5df4db2e9)
１版　2024年10月06日　registerForActivityResultにリファクタリング、ソースコードに不足があり追記など
初版　2021年07月09日　

<iframe width="560" height="315" src="https://www.youtube.com/embed/82FzBA_toOE?si=i-VzPIEGt_AWgqaG" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# 1.概要
## 1.1 ソースコードについて
最新のソースコードを[Github](https://github.com/taka-guijiu/MediaPlayer003)にて公開しています。
第2項以降の情報は、第１版当時の情報です。
## 1.2 第１版改訂にあたり
　MediaPlayerで音楽プレイヤ―を作ってみました。正確には斎藤著「Androidアプリ開発の教科書Kotlin対応(2019年2月初版第2刷)の第12章の記事を元に、ネット上で音楽プレイヤのソースコードを見つけて、内部ストレージのDownLoadフォルダやGoogleドライブから音源を選び再生できるようにコピペし、改造したことがありました。最近少しKotlinが分かるようになったので、改めてMediaPlayerクラスについて勉強し、また併せてstartActivityForResultが非推奨になったのでregisterForActivityResultにリファクタリングしました。
 (2024年10月、ソースコードを[Github](https://github.com/taka-guijiu/MediaPlayer003)上に公開しました。なお、build.gradle#Projectファイルとbulid.gradle#appファイルは、プロジェクトを作り直したため、ktsの形式になっています。）
 ## 1.3 第２版改訂にあたり
 　再生経過時間を表すcurrentPositionを表示できるように改修を行った。概要は「[MediaPlayer音楽プレイヤ―にtimerを使ってcurrentPoisitionを表示してみた。](https://qiita.com/guijiu/items/7776c74168b5df4db2e9)」を参照してください。
 

![TestMediaPlayer03_002_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501068/23457a28-bcd5-f805-5c98-1b27c9279536.png)　　![Picker.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501068/70a546be-106a-efaf-d919-aed3e01ed14f.png)





# 2.開発環境（第１版当時）
Android Studio 4.2.2
Build #AI-202.7660.26.42.7486908, built on June 24, 2021
Runtime version: 11.0.8+10-b944.6842174 amd64
VM: OpenJDK 64-Bit Server VM by N/A
Windows 10 10.0

# 3.動作環境（第１版当時）
 Androidの動作環境を示す代わりにBuildgradleを示します。
 minSdkVersionを24としました。あまり古いSDKバージョンだとストレージの取り扱いが異なるようなので24未満は未対応としました。

```build.gradle#Project
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ext.kotlin_version = "1.5.20"
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.2.2"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
        jcenter() // Warning: this repository is going to shut down soon
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
```

```bulid.gradle#app
plugins {
    id 'com.android.application'
    id 'kotlin-android'
}

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        applicationId "com.example.testmediaplayer"
        minSdkVersion 24
        targetSdkVersion 30
        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 "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.5.0'
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
```

# 4.ソースコード（第１版当時）
## 4.1 レイアウトファイル
```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/btnPlay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="116dp"
        android:enabled="false"
        android:text="@string/play"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnStop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:enabled="false"
        android:text="@string/STOP"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.161"
        app:layout_constraintStart_toEndOf="@+id/btnPlay"
        app:layout_constraintTop_toTopOf="@+id/btnPlay" />

    <Switch
        android:id="@+id/swLoop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:text="@string/loopOn"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

## 4.2 文字列ファイル
```Strings.xml
<resources>
    <string name="app_name">TestMediaPlayer</string>
    <string name="play">再生</string>
    <string name="pause">一時停止</string>
    <string name="STOP">停止</string>
    <string name="loopOn">LoopOn</string>
</resources>
```
## 4.3 オプションメニューファイル
```option_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/openSoundFolder"
        app:showAsAction="never"
        android:title="開く"/>
</menu>
```

## 4.4 プログラムコード
```MainActivity.kt
package com.example.testmediaplayer

import android.app.Activity
import android.content.Intent
import android.media.MediaPlayer
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Button
import android.widget.CompoundButton
import android.widget.Switch
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.app.AppCompatActivity
import java.io.IOException
import java.util.*

class MainActivity : AppCompatActivity() {

    val mediaPlayer = MediaPlayer()
    lateinit var btnPlay: Button
    lateinit var btnStop: Button
    lateinit var swLoopOn: Switch

    val getContentSound =
        registerForActivityResult(StartActivityForResult()) {
            if (it.resultCode == Activity.RESULT_OK) {
                val resultIntent = it.data
                val uri: Uri? = resultIntent?.data

                mediaPlayer.apply {
                    stop()
                    reset()
                }

                if (uri != null) {
                    mediaPlayer.apply {
                        setDataSource(this@MainActivity, uri)   // 音源を設定
                        //メディアソースの再生準備が整ったときに呼び出されるコールバックの登録する
                        setOnPreparedListener {
                            // 各ボタンをタップ可能に設定
                            btnPlay.setEnabled(true)
                            btnPlay.text = getString(R.string.play)
                            btnStop.setEnabled(true)
                        }

                        // 再生中にメディアソースの終端に到達したときに呼び出されるコールバックを登録
                        setOnCompletionListener {
                            // ループ設定がされていなければ
                            if (!isLooping()) {
                                // 再生ボタンのラベルを「再生」に設定
                                btnPlay.text = getString(R.string.play)
                            }
                        }
                        prepareAsync()
                    }
                }
            }
        }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btnPlay = findViewById<Button>(R.id.btnPlay)
        btnStop = findViewById<Button>(R.id.btnStop)
        swLoopOn = findViewById<Switch>(R.id.swLoop)

        btnPlay.setOnClickListener {
            if (!mediaPlayer.isPlaying) {
                try {
                    mediaPlayer.start()
                    btnPlay.text = getString(R.string.pause)
                } catch (e: IllegalStateException) {
                    e.printStackTrace()
                }
            } else {
                try {
                    // 再生を一時停止
                    mediaPlayer.pause()
                } catch (e: IllegalStateException) {
                    e.printStackTrace()
                }
                btnPlay.text = getString(R.string.play)
            }
        }

        btnStop.setOnClickListener {
            try {
                mediaPlayer.stop()
                mediaPlayer.prepare()
            } catch (e: IllegalStateException) {
                e.printStackTrace()

            } catch (e: IOException) {
                e.printStackTrace()
            }
            btnPlay.text = getString(R.string.play)
        }

        // ループスイッチの状態が変化したときに、MediaPlayerのインスタンスに反映
        swLoopOn.setOnCheckedChangeListener(
            object : CompoundButton.OnCheckedChangeListener {
                override fun onCheckedChanged(
                    buttonView: CompoundButton?,
                    isChecked: Boolean,
                ) {
                    mediaPlayer.isLooping = isChecked
                }
            }
        )
    }

    // アプリを一時的に隠した時の処理
    override fun onPause() {
        super.onPause()
        try {
            mediaPlayer.pause()
        } catch (e: IllegalStateException) {
            e.printStackTrace()
        }
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        // オプションメニュー用xmlファイルをインフレートする
        menuInflater.inflate(R.menu.option_menu, menu)
        return super.onCreateOptionsMenu(menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // 選択されたメニューのIDのR値による処理分岐
        when (item.itemId) {
            R.id.openSoundFolder -> {
                // ファイルピッカーを呼び出す
                val iSound = Intent(Intent.ACTION_OPEN_DOCUMENT)
                iSound.type = "audio/mpeg"
                iSound.putExtra(Intent.EXTRA_TITLE, "memo.mp3")
                getContentSound.launch(iSound)
            }
        }
        return super.onOptionsItemSelected(item)
    }
}

```

以上、