rokkonet

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

android開発 SMBサーバーもしくは端末内からランダムに1つの音声ファイルをストリーミング再生する

2021 Sep. 24.
2021 Sep. 16.

概要

SMBサーバーからランダムに1つの音声ファイルをストリーミング再生する。
SMBサーバーにつながらなかったら、android端末の外部共有ストレージからランダムに1つの音声ファイルをストリーミング再生する。
再生にはサードパーティ再生アプリをIntent起動する。

インストールしたandroid端末

android バージョン 10

コンパイル環境

compileSdkVersion 30
minSdkVersion 24
targetSdkVersion 30


android studioのプロジェクトのapp/libsにjcifs-ngライブラリを配置

jcifs-ng-2.1.6.jar

 https://mvnrepository.com/artifact/eu.agno3.jcifs/jcifs-ng/2.1.6 のFilesのbundleをクリックしてダウンロード

bcprov-jdk15to18-1.69.jar

 https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15to18/1.69 のFilesのjarをクリックしてダウンロード

build.gradle(module:app)

次の依存関係設定を記述する

    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-alpha03"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0-alpha03"
    implementation files('libs/jcifs-ng-2.1.6.jar')
    implementation group: 'org.bouncycastle', name: 'bcprov-jdk15to18', version: '1.69'
    implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32'
    implementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.32'


build.gradle(module:app) 全体

plugins {
    id 'com.android.application'
    id 'kotlin-android'
}

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        applicationId "PACKAGE.randomsmbsoundplay"
        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'
    }
    buildFeatures {
        viewBinding = true
    }
}

dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.6.0'
    implementation 'androidx.appcompat:appcompat:1.3.1'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-alpha03"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0-alpha03"
    implementation files('libs/jcifs-ng-2.1.6.jar')
    implementation group: 'org.bouncycastle', name: 'bcprov-jdk15to18', version: '1.69'
    implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32'
    implementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.32'
}


許可権限

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


AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="PACKAGE.randomsmbsoundplay">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <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.AppCompat.DayNight.NoActionBar"
        >
        <activity android:name=".MainActivity">
            <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">

    <TextView
        android:id="@+id/tvInfo01"
        android:layout_width="231dp"
        android:layout_height="64dp"
        app:layout_constraintBottom_toTopOf="@+id/tvInfo02"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.15"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Space
        android:id="@+id/space01"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/tvInfo02"
        app:layout_constraintTop_toBottomOf="@+id/tvInfo01"
        app:layout_constraintBottom_toTopOf="@id/tvFilePath"
        />

    <TextView
        android:id="@+id/tvInfo02"
        android:layout_width="192dp"
        android:layout_height="80dp"
        android:layout_marginBottom="4dp"
        app:layout_constraintBottom_toTopOf="@+id/tvFilePath"
        app:layout_constraintEnd_toStartOf="@id/space02"
        app:layout_constraintHorizontal_bias="0.526"
        app:layout_constraintStart_toEndOf="@+id/space01"
        app:layout_constraintTop_toBottomOf="@+id/tvInfo01" />
    <Space
        android:id="@+id/space02"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toEndOf="@+id/tvInfo02"
        app:layout_constraintEnd_toStartOf="@+id/btnQuit"
        app:layout_constraintTop_toBottomOf="@+id/tvInfo01"
        app:layout_constraintBottom_toTopOf="@+id/tvFilePath"
        />

    <Button
        android:id="@+id/btnQuit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/quit"
        app:layout_constraintStart_toEndOf="@+id/space02"
        app:layout_constraintEnd_toStartOf="@+id/space03"
        app:layout_constraintTop_toBottomOf="@+id/tvInfo01"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintVertical_bias="0.04"
        />

    <Space
        android:id="@+id/space03"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toEndOf="@+id/btnQuit"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvInfo01"
        app:layout_constraintBottom_toBottomOf="parent"
        />

    <TextView
        android:id="@+id/tvFilePath"
        android:layout_width="295dp"
        android:layout_height="129dp"
        android:layout_marginBottom="20dp"
        android:hint="@string/hintPath"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvInfo02"
        app:layout_constraintBottom_toTopOf="@+id/tvFileSize"
        app:layout_constraintHorizontal_bias="0.521"
        />

    <TextView
        android:id="@+id/tvFileSize"
        android:layout_width="290dp"
        android:layout_height="68dp"
        android:layout_marginBottom="20dp"
        android:hint="@string/hintSize"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvFilePath"
        app:layout_constraintBottom_toTopOf="@+id/btnPlay"
        />

    <Button
        android:id="@+id/btnPlay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/play"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/btnReplay"
        app:layout_constraintTop_toBottomOf="@+id/tvFileSize"
        app:layout_constraintBottom_toBottomOf="parent"
        />


    <Button
        android:id="@+id/btnReplay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/replay"
        app:layout_constraintStart_toEndOf="@+id/btnPlay"
        app:layout_constraintEnd_toStartOf="@+id/btnReset"
        app:layout_constraintTop_toBottomOf="@+id/tvFileSize"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintBaseline_toBaselineOf="@+id/btnPlay"
        />

    <Button
        android:id="@+id/btnReset"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/reset"
        app:layout_constraintStart_toEndOf="@+id/btnReplay"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvFileSize"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintBaseline_toBaselineOf="@id/btnReplay"
        />

</androidx.constraintlayout.widget.ConstraintLayout>


dialog_smb_server_setting.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"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <EditText
        android:id="@+id/etDomain"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/hintDomain"
        android:autofillHints="@string/hintDomain"
        android:inputType="text"
        app:layout_constraintBottom_toTopOf="@id/etSmbPath"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/etSmbPath"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/hintSmbRootPath"
        android:autofillHints="@string/hintSmbRootPath"
        android:inputType="textUri"
        app:layout_constraintBottom_toTopOf="@+id/etSmbUser"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/etDomain" />

    <EditText
        android:id="@+id/etSmbUser"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:autofillHints="@string/hintUserName"
        android:hint="@string/hintUserName"
        android:inputType="textUri"
        app:layout_constraintBottom_toTopOf="@+id/etSmbPassword"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/etSmbPath" />

    <EditText
        android:id="@+id/etSmbPassword"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:autofillHints="@string/hintPassword"
        android:hint="@string/hintPassword"
        android:inputType="textPassword"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/etSmbUser" />

</androidx.constraintlayout.widget.ConstraintLayout>


MainActivity.kt

package net.sytes.rokkosan.randomsmbsoundplay

// 2021 Sep. 26.
// 2021 Sep. 06.

import android.Manifest
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import net.sytes.rokkosan.randomsmbsoundplay.databinding.ActivityMainBinding
import java.io.File
import java.util.*


class MainActivity : AppCompatActivity(),SmbServerSettingDialogFragment.DialogListener {

    //////////////////////////////////////////////
    // Please replace these values to your data //
    /*
    var smbDomain: String = "192.168.1.1"
    var smbRoot: String = "/SMB/SERVER/FILE/"
    var smbUser: String = ""
    var smbPassword: String = ""
    */
    var smbDomain: String = "192.168.1.1"
    var smbRoot: String = "/audio-video/01/"
    var smbUser: String = ""
    var smbPassword: String = ""
    //////////////////////////////////////////////

    private var smbFilepath: String = ""
    private lateinit var binding: ActivityMainBinding
    private lateinit var audioUri: Uri
    private var isDestroy: Boolean = false
        // flag to check if Activity was destroyed just before creation

    lateinit var smbViewModel: SmbViewModel
    lateinit var smbViewModelFactory: SmbViewModelFactory
    lateinit var smbUrl: String


    val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this /* Activity context */)




    val smbSettingdialog = SmbServerSettingDialogFragment()
    val PERMISSION_READ_EX_STR: Int = 100

    // option menu
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        super.onCreateOptionsMenu(menu)
        val inflater = menuInflater
        //メニューのリソース選択
        inflater.inflate(R.menu.settings, menu)
        return true
    }


    //Option menuのitemがクリックされた時の動作
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item?.itemId) {
            R.id.itemSmbServer -> {

                // ダイアログに渡す値をBundleにセット
                val args = Bundle()
                args.putString("smbDomain", smbDomain )
                args.putString("smbRootPath", smbRoot )
                args.putString("smbUser", smbUser )
                args.putString("smbPassword", smbPassword )
                smbSettingdialog.setArguments(args)

                // ダイアログを表示
                smbSettingdialog.show(supportFragmentManager, "simple")

            }
        }
        return super.onOptionsItemSelected(item)
    }


    override fun onDialogMapReceive(smbSettingdialog: DialogFragment, smbSettings: MutableMap<String, String>) {
        //smbSettingdialogからの値を受け取る

        with ( defaultSharedPreferences.edit()) {
            putString("smbDomain", smbSettings["smbDomain"])
            putString("smbPath", smbSettings["smbPath"])
            putString("smbUser", smbSettings["smbUser"])
            putString("smbPassword", smbSettings["smbPassword"])
            apply()
        }


        smbDomain = smbSettings["smbDomain"]!!
        smbRoot = smbSettings["smbPath"]!!
        smbUser = smbSettings["smbUser"]!!
        smbPassword = smbSettings["smbPassword"]!!

    }

    /*
    override fun onDialogPositive(smbSettingdialog: DialogFragment) {
        //実装なし
    }

    override fun onDialogNegative(smbSettingdialog: DialogFragment) {
        //キャンセル時
    }
    */




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

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // val name = defaultSharedPreferences.getString("signature", "") // sample to get data


        if (savedInstanceState != null) {
            isDestroy = savedInstanceState.getBoolean("IS_DESTROY")
        }

        // リプレイボタン・リセットボタンを無効化しておく
        binding.btnReplay.isEnabled = false
        binding.btnReset.isEnabled = false

        // play-button to connect to SMB server and play audio
        binding.btnPlay.setOnClickListener {
            binding.tvInfo01.setText(R.string.waitMinutes)
            binding.btnPlay.setEnabled(false)

            // set smbUrl to prepare for connecting to SMB server
            smbUrl = "smb://" + smbDomain + smbRoot
            if (smbUrl.takeLast(1) != "/") {
                smbUrl += "/"
                // smbUrl文字列の末尾はスラッシュ(/)とする
            }

            /*
             * get ViewModel for SmbConnection
             */
            smbViewModelFactory = SmbViewModelFactory(smbUser, smbPassword, smbDomain, smbUrl)
            smbViewModel = ViewModelProvider(this, smbViewModelFactory).get(SmbViewModel::class.java)


            /*
             * create Observers
             */
            // ファイルパス取得
            val pathObserver = Observer<String> { sPath ->
                smbFilepath = sPath
                binding.tvFilePath.text = sPath
                binding.btnReplay.isEnabled = true
                binding.btnReset.isEnabled = true
            }

            // ファイルサイズ取得
            val sizeObserver = Observer<Long> { sSize ->
                binding.tvFileSize.setText(getString( R.string.kb, (sSize / 1000L).toString()))
            }

            // SMB接続状況に応じた処理
            val connectStatusObserver = Observer<SmbViewModel.ProceccStatus> { status ->
                if (!isDestroy) {  // 直前にActivityがdestroyされたのではない場合
                    when(status!!) {
                        SmbViewModel.ProceccStatus.notYetGot-> binding.tvInfo01.setText(R.string.notConnectedSmb)
                        SmbViewModel.ProceccStatus.fromNow -> binding.tvInfo01.setText(R.string.fromNowTryConnectSmb)
                        SmbViewModel.ProceccStatus.trying -> binding.tvInfo01.setText(R.string.tryConnectingSmb)
                        SmbViewModel.ProceccStatus.got -> binding.tvInfo01.setText(R.string.connectedSmb)
                        SmbViewModel.ProceccStatus.failed -> {
                            binding.tvInfo01.setText(R.string.failedConnectSmb)

                            // play an audio file in external storage in android device
                            binding.tvInfo02.setText(R.string.tryGetAudioFile)
                            playAndroidExternalAudio()
                            binding.btnReplay.isEnabled = true
                            binding.btnReset.isEnabled = true
                        }
                    }
                }
            }

            // SMBサーバーの音声ファイル取得状況に応じた処理
            val getSmbAudioFileStatusObserver = Observer<SmbViewModel.ProceccStatus> { status ->
                if (!isDestroy) {  // 直前にActivityがdestroyされたのではない場合
                    when (status!!) {
                        SmbViewModel.ProceccStatus.notYetGot -> binding.tvInfo02.setText(R.string.notGetSmbAudioFile)
                        SmbViewModel.ProceccStatus.fromNow -> binding.tvInfo02.setText(R.string.fromNowTryGetSmbAudioFile)
                        SmbViewModel.ProceccStatus.trying -> binding.tvInfo02.setText(R.string.tryGetSmbAudioFile)
                        SmbViewModel.ProceccStatus.got -> {
                            binding.tvInfo02.setText(R.string.gotSmbAudioFile)

                            // create and send Intent of audio-player
                            //   play with 3rd party media play application
                            try {
                                val audioIntent = Intent()
                                audioIntent.action = Intent.ACTION_VIEW
                                audioIntent.setDataAndType(Uri.parse(smbFilepath), "audio/*")
                                // if you decide VLC as the player, uncomment the next line
                                audioIntent.setPackage("org.videolan.vlc")
                                startActivity(audioIntent)
                                binding.tvInfo01.setText(R.string.sentIntent)
                            } catch (e: Exception) {
                                binding.tvInfo01.setText(e.toString())
                            } finally {
                                smbViewModel.smb.close()
                            }
                        }

                        SmbViewModel.ProceccStatus.failed -> { // SMBサーバーの音声ファイル取得に失敗
                            binding.tvInfo02.setText(R.string.failedGetSmbAudioFile)

                            // play an audio file in external storage in android device
                            binding.tvInfo01.setText(R.string.failedConnectSmb)
                            binding.tvInfo02.setText(R.string.tryPlayAndroidFile)
                            playAndroidExternalAudio()
                        }
                    }
                }
            }

            // ViewModelでSMBサーバーに接続し音声ファイルを取得する
            smbViewModel.connectSmbSelectAudioFile()

            // Proceed according to status of SMB-connection in the screen
            smbViewModel.smbConnectionStatus.observe(this, connectStatusObserver)

            // Display filepath, filesize in the screen
            smbViewModel.smbPath.observe(this, pathObserver)
            smbViewModel.smbSize.observe(this, sizeObserver)

            // proceed according to status of getting SMB-audio-file
            smbViewModel.getAudioFileStatus.observe(this, getSmbAudioFileStatusObserver)

        }

        // replay-button
        binding.btnReplay.setOnClickListener {
            if ( "smb" == binding.tvFilePath.text.toString().substring(0, 3)) {
                // in case SMB
                // create and send Intent of audio-player
                //   play with 3rd party media play application
                try {
                    val audioIntent = Intent()
                    audioIntent.action = Intent.ACTION_VIEW
                    audioIntent.setDataAndType(Uri.parse(smbFilepath), "audio/*")
                    // if you decide VLC as the player, uncomment the next line
                    audioIntent.setPackage("org.videolan.vlc")
                    startActivity(audioIntent)
                    binding.tvInfo01.setText(R.string.sentIntent)
                } catch (e: Exception) {
                    binding.tvInfo01.setText(e.toString())
                } finally {
                    smbViewModel.smb.close()
                }
            } else {
                // in case external storage in android device
                // パーミッションを取得しているのでオーディオファイルをIntentに乗せて発出する
                try {
                    val audioIntent = Intent()
                    audioIntent.action = Intent.ACTION_VIEW
                    audioIntent.setDataAndType(audioUri, "audio/*")
                    // if you decide VLC as the player, uncomment the next line
                    audioIntent.setPackage("org.videolan.vlc")
                    startActivity(audioIntent)
                    binding.tvInfo02.setText(R.string.sentIntent)
                } catch (e: Exception) {
                    binding.tvInfo02.setText(e.toString())
                }
            }
        }

        // reset-button
        binding.btnReset.setOnClickListener {
            binding.tvInfo01.setText("")
            binding.tvInfo02.setText("")
            binding.tvFilePath.setText("")
            binding.tvFileSize.setText("")
            smbViewModel._smbConnectionStatus.postValue(SmbViewModel.ProceccStatus.notYetGot)
            binding.btnPlay.isEnabled = true
            binding.btnReplay.isEnabled = false
            binding.btnReset.isEnabled = false
        }

        // quit-button
        binding.btnQuit.setOnClickListener {
            binding.btnReset.performClick()
            finishAndRemoveTask()
        }
    }

    // Activityのdestroy時のインスタンス保存
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)

        isDestroy = true
        outState.putBoolean("IS_DESTROY", true)

        val info01Text: String = binding.tvInfo01.text.toString()
        outState.putString("INFO01", info01Text)

        val info02Text: String = binding.tvInfo02.text.toString()
        outState.putString("INFO02", info02Text)

        val pathText: String = binding.tvFilePath.text.toString()
        outState.putString("PATH", pathText )

        val sizeText: String = binding.tvFileSize.text.toString()
        outState.putString("SIZE", sizeText )
    }

    // 保存されたインスタンスの読み込み
    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)

        val info01Text: String = savedInstanceState.getString("INFO01") ?: ""
        binding.tvInfo01.setText(info01Text)

        val info02Text: String = savedInstanceState.getString("INFO02") ?: ""
        binding.tvInfo02.setText(info02Text)

        val pathText: String = savedInstanceState.getString("PATH") ?: ""
        binding.tvFilePath.setText(pathText)

        val sizeText: String = savedInstanceState.getString("SIZE") ?: ""
        binding.tvFileSize.setText(sizeText)
    }

    /*
     * create and send Intent of audio-player
     *  play with 3rd party media play application
     */
    private fun playAndroidExternalAudio() {
        if (!isPermissionREAD_EXTERNAL_STORAGE()) {
            // READ_EXTERNAL_STORAGEパーミッションがない場合
            requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSION_READ_EX_STR)
        }
        // パーミッションを取得しているのでオーディオファイルをIntentに乗せて発出する
        try {
            audioUri = getExAudioUriRandom()
            val audioPath = getPathFromUri(this, audioUri)
            binding.tvInfo02.setText(R.string.gotLocalAudioFile)
            binding.tvFilePath.setText(audioPath)
            binding.tvFileSize.setText(getString( R.string.kb, (File(audioPath!!).length() / 1000L).toString()))
            val audioIntent = Intent()
            audioIntent.action = Intent.ACTION_VIEW
            audioIntent.setDataAndType(audioUri, "audio/*")
            // if you decide VLC as the player, uncomment the next line
            audioIntent.setPackage("org.videolan.vlc")
            startActivity(audioIntent)
            binding.tvInfo02.setText(R.string.sentIntent)
        } catch (e: Exception) {
            binding.tvInfo02.setText(e.toString())
        }
    }

    /*
    * return boolean
    *   true: permission.READ_EXTERNAL_STORAGE is granted
    *   false: permission.READ_EXTERNAL_STORAGE is not granted
    */
    fun isPermissionREAD_EXTERNAL_STORAGE(): Boolean {
        // アプリに権限が付与されているかどうかを確認する
        return ActivityCompat.checkSelfPermission(this,
            Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
    }

    /*
     * android端末の外部共有ストレージから音声ファイルをランダムに1つ選び、そのURIを返す
     */
    private fun getExAudioUriRandom():Uri {
        lateinit var contentUri: Uri
        val columns = arrayOf(
            MediaStore.Audio.Media._ID,
            MediaStore.Audio.Media.DISPLAY_NAME,
        )
        val resolver = applicationContext.contentResolver
        val cursor = resolver.query(
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,  //データの種類
            columns, //取得する項目 nullは全部
            null, //フィルター条件 nullはフィルタリング無し
            null, //フィルター用のパラメータ
            null   //並べ替え
        )
        val numCount: Int? = cursor?.count
        if ( numCount!! < 1 ) {
            return throw Exception("No Media")
        }
        val MediaNum = getRandomNum(numCount)

        cursor.use {
            val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
            val displayNameColumn =
                cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
            cursor.moveToPosition(MediaNum)
            val id = cursor.getLong(idColumn)
            val displayName = cursor.getString(displayNameColumn)
            contentUri = Uri.withAppendedPath(
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                id.toString()
            )
        }
        return contentUri
    }

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

    /*
     * Thanks for https://stackoverflow.com/questions/61356281/android-files-unable-to-get-file-path-from-content-uri-when-file-is-selected-f
     * URIからファイルパスを取得する
     */
    fun getPathFromUri(context: Context, uri: Uri): String? {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(
                context,
                uri
            )
        ) {
            if (isExternalStorageDocument(uri)) {
                val docId = DocumentsContract.getDocumentId(uri)
                val split = docId.split(":").toTypedArray()
                val type = split[0]
                return Environment.getExternalStorageDirectory().toString() + "/" + split[1]

5            } else if (isDownloadsDocument(uri)) {
                try {
                    val id = DocumentsContract.getDocumentId(uri)
                    //Log.d(TAG, "getPath: id= " + id);
                    val contentUri = ContentUris.withAppendedId(
                        Uri.parse("content://downloads/public_downloads"),
                        java.lang.Long.valueOf(id)
                    )
                    return getDataColumn(context, contentUri, null, null)
                } catch (e: Exception) {
                    e.printStackTrace()
                    val segments = uri.pathSegments
                    if (segments.size > 1) {
                        val rawPath = segments[1]
                        return if (!rawPath.startsWith("/")) {
                            rawPath.substring(rawPath.indexOf("/"))
                        } else {
                            rawPath
                        }
                    }
                }
            } else if (isMediaDocument(uri)) {
                val docId = DocumentsContract.getDocumentId(uri)
                val split = docId.split(":").toTypedArray()
                val type = split[0]
                var contentUri: Uri? = null
                if ("image" == type) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
                } else if ("video" == type) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
                } else if ("audio" == type) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
                }
                val selection = "_id=?"
                val selectionArgs = arrayOf(
                    split[1]
                )
                return getDataColumn(context, contentUri, selection, selectionArgs)
            }
        } else if ("content".equals(uri.scheme, ignoreCase = true)) {

            // Return the remote address
            return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(
                context,
                uri,
                null,
                null
            )
        } else if ("file".equals(uri.scheme, ignoreCase = true)) {
            return uri.path
        }
        return null
    }

    fun getDataColumn(
        context: Context, uri: Uri?, selection: String?,
        selectionArgs: Array<String>?
    ): String? {
        var cursor: Cursor? = null
        val column = "_data"
        val projection = arrayOf(
            column
        )
        try {
            cursor = context.contentResolver.query(
                uri!!, projection, selection, selectionArgs,
                null
            )
            if (cursor != null && cursor.moveToFirst()) {
                val column_index = cursor.getColumnIndexOrThrow(column)
                return cursor.getString(column_index)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            cursor?.close()
        }
        return "nodata"
    }

    fun isExternalStorageDocument(uri: Uri): Boolean {
        return "com.android.externalstorage.documents" == uri.authority
    }

    fun isDownloadsDocument(uri: Uri): Boolean {
        return "com.android.providers.downloads.documents" == uri.authority
    }

    fun isMediaDocument(uri: Uri): Boolean {
        return "com.android.providers.media.documents" == uri.authority
    }

    fun isGooglePhotosUri(uri: Uri): Boolean {
        return "com.google.android.apps.photos.content" == uri.authority
    }
}


SmbServerSettingDialogFragment.kt

出典 【Kotlin】DialogFragmentからActivityへ値を渡す - Qiita

// 2021 Sep. 23.
// 2021 Sep. 20.

// Thanks for https://qiita.com/gino_gino/items/9390e1a0ce5e42d16466

import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.widget.EditText
import androidx.fragment.app.DialogFragment

class SmbServerSettingDialogFragment: DialogFragment()  {

    public interface DialogListener{
        // public fun onDialogPositive(dialog: DialogFragment) //今回は使わない。色んなダイアログで使いまわす際には使います。
        // public fun onDialogNegative(dialog: DialogFragment)
        public fun onDialogMapReceive(dialog: DialogFragment, smbSettings: MutableMap<String, String>) //Activity側へStringを渡します。
    }
    var listener:DialogListener? = null


    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        var smbSettings: MutableMap<String, String> = mutableMapOf(
            "smbDomain" to "",
            "smbPath" to "",
            "smbUser" to "",
            "smbPassword" to "")

        val builder = AlertDialog.Builder(activity)
        val inflater = requireActivity().layoutInflater
        val smbServerSettingView = inflater.inflate(R.layout.dialog_smb_server_setting, null)

        // 呼び出し元でセットした値をViewにセットする
        smbServerSettingView.findViewById<EditText>(R.id.etDomain).setText(requireArguments().getString("smbDomain", ""))
        smbServerSettingView.findViewById<EditText>(R.id.etSmbPath).setText(requireArguments().getString("smbRootPath", ""))
        smbServerSettingView.findViewById<EditText>(R.id.etSmbUser).setText(requireArguments().getString("smbUser", ""))
        smbServerSettingView.findViewById<EditText>(R.id.etSmbPassword).setText(requireArguments().getString("smbPassword", ""))

        builder.setView(smbServerSettingView)
            .setTitle("SMB Server Setting")
            .setPositiveButton("OK") { dialog, id ->
                if (smbServerSettingView.findViewById<EditText>(R.id.etDomain).text.isNotEmpty()) {
                    smbSettings.put(
                        "smbDomain",
                        smbServerSettingView.findViewById<EditText>(R.id.etDomain).text.toString()
                    )
                }
                if (smbServerSettingView.findViewById<EditText>(R.id.etSmbPath).text.isNotEmpty()) {
                    smbSettings.put(
                        "smbPath",
                        smbServerSettingView.findViewById<EditText>(R.id.etSmbPath).text.toString()
                    )
                }
                if (smbServerSettingView.findViewById<EditText>(R.id.etSmbUser).text.isNotEmpty()) {
                    smbSettings.put(
                        "smbUser",
                        smbServerSettingView.findViewById<EditText>(R.id.etSmbUser).text.toString()
                    )
                }
                if (smbServerSettingView.findViewById<EditText>(R.id.etSmbPassword).text.isNotEmpty()) {
                    smbSettings.put(
                        "smbPassword",
                        smbServerSettingView.findViewById<EditText>(R.id.etSmbPassword).text.toString()
                    )
                }

                listener?.onDialogMapReceive(this,smbSettings)
            }
            .setNegativeButton("Cancel") { dialog, id ->
                // nothing is done
            }
        return builder.create()
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        try {
            listener = context as DialogListener
        }catch (e: Exception){
            Log.e("MyApp","CANNOT FIND LISTENER")
        }
    }

    override fun onDetach() {
        super.onDetach()
        listener = null
    }
}


SmbViewModel.kt

package net.sytes.rokkosan.randomsmbsoundplay

// 2021 Sep. 18.
// 2021 Sep. 06.

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import jcifs.config.PropertyConfiguration
import jcifs.context.BaseContext
import jcifs.smb.NtlmPasswordAuthenticator
import jcifs.smb.SmbFile
import kotlinx.coroutines.*
import java.util.*


class SmbViewModel(val user: String, val password: String, val domain: String, val smbUrl: String): ViewModel() {
    val audioExtension: String = "\\.mp3|\\.wav|\\.aac|\\.au|\\.gsm|\\.m4a|\\.ogg|\\.flac"

    lateinit var smb: SmbFile
    data class AudioFileProperty(val smbPath: String, val fileSize: Long)

    enum class ProceccStatus {
        notYetGot,
        fromNow,
        trying,
        got,
        failed
    }

    // LiveDataを設定する
    var _smbConnectionStatus = MutableLiveData(ProceccStatus.notYetGot)
    val smbConnectionStatus: LiveData<ProceccStatus>
        get() = _smbConnectionStatus

    private var _getAudioFileStatus = MutableLiveData(ProceccStatus.notYetGot)
    val getAudioFileStatus: LiveData<ProceccStatus>
        get() = _getAudioFileStatus

    private val _smbPath: MutableLiveData<String> by lazy {
        // MutableLiveData<T>は、valでmutableな変数を宣言できる
        MutableLiveData<String>()
    }
    val smbPath: LiveData<String>
        // 下記により、smbPathの読み込み時は _smbPathが読み出される
        get() = _smbPath

    private val _smbSize: MutableLiveData<Long> by lazy {
        MutableLiveData<Long>()
    }
    val smbSize: LiveData<Long>
        get() = _smbSize


    // Main process in this ViewModel
    fun connectSmbSelectAudioFile() {
        viewModelScope.launch(Dispatchers.IO) {
            // connect to SMB server in worker thread
            try {
                _smbConnectionStatus.postValue(ProceccStatus.trying) // tyring to connect to SMB
                connectSmb()
            } catch (e: Exception) {
                _smbConnectionStatus.postValue(ProceccStatus.failed) // Failed to connect to SMB"
                return@launch
            }
            // got connection to SMB
            _smbConnectionStatus.postValue(ProceccStatus.got) // connected to SMB

            // select an audio file in SMB
            _getAudioFileStatus.postValue(ProceccStatus.trying) // trying to get an audio file
            val audioFileProperty = selectAudioSmbFile()
            if ( audioFileProperty.fileSize < 1L ) {
                // no audio file
                _getAudioFileStatus.postValue(ProceccStatus.failed) // failed to get an audio file
                smb.close()
            } else {
                // got an audio file
                _smbPath.postValue(audioFileProperty.smbPath)
                _smbSize.postValue(audioFileProperty.fileSize)
                _getAudioFileStatus.postValue(ProceccStatus.got) // got an audio file

                // continue to play audio in MainActivity
            }
        } // end of launch

        if ( ProceccStatus.failed == smbConnectionStatus.value) {
            // Failed to connect to SMB
            return
        }
        if (ProceccStatus.failed == getAudioFileStatus.value) {
            // Failed to get an audio file
            return
        }
    }

    // SMBサーバーに接続し、接続オブジェクトをsmbに入れる
    private fun connectSmb() {
        try {
            val prop = Properties()  // java.util.Properties
            prop.setProperty("jcifs.smb.client.minVersion", "SMB202")
            prop.setProperty("jcifs.smb.client.maxVersion", "SMB300")
            val baseCxt = BaseContext(PropertyConfiguration(prop))
            val auth =
                baseCxt.withCredentials(NtlmPasswordAuthenticator(domain, user, password))

            // connect to SMB server
            smb = SmbFile(smbUrl, auth)
            smb.list() // check if SMB-connection succeeded or failed
        } catch (e1: Exception) {
            try {
                smb.close()
            }
            catch (e2: Exception) {
                throw e2
            }
            throw e1
        }
    }


    /*
     * ランダムにSMBサーバーの音楽ファイルを1つ選び、そのパスとファイルサイズをAudioFilePropertyデータクラスで返す
     *  data class AudioFileProperty(val smbPath: String, val fileSize: Long)
     * 失敗した時は AudioFileProperty("",0L) を返す。
     * 変数smbは、関数の外でjcifs-ngによって取得したSmbFileインスタンス
     * audioExtensionは、関数の外で設定された、音声ファイル拡張子集合の文字列
     */
    fun selectAudioSmbFile(): AudioFileProperty {
        try {
            if (smb.isDirectory) {
                // get all files in dir
                var smbNormalFiles: MutableList<SmbFile>
                var tmpSmbNormalFiles: MutableList<SmbFile> = mutableListOf()
                smbNormalFiles = getNormalSmbFiles(smb, tmpSmbNormalFiles)
                if (1 > smbNormalFiles.size) {
                    // No file in the directory
                    return AudioFileProperty("",0L)
                } else if (1 == smbNormalFiles.size) {
                    // only one file in the directory
                    if (!isMatchTail(smbNormalFiles[0].name, audioExtension)) {
                        // No audio file
                        return AudioFileProperty("",0L)
                    } else {
                        // Got one sound file.
                        // Play Audio in main thread
                        return AudioFileProperty(smbNormalFiles[0].path, smbNormalFiles[0].length())
                    }
                } else { // smbNormalFiles.size > 1
                    // in case of plural files.
                    // collect audio files to audioSmbFiles
                    var audioSmbFiles: MutableList<SmbFile> = mutableListOf()
                    for (eachSmbNormalFile in smbNormalFiles) {
                        if (isMatchTail(eachSmbNormalFile.name, audioExtension)) {
                            audioSmbFiles.add(eachSmbNormalFile)
                        }
                    }

                    if (1 > audioSmbFiles.size) {
                        // No audio file
                        return AudioFileProperty("",0L)
                    } else if (1 == audioSmbFiles.size) {
                        // Got one sound file.
                        // PlayAudio in main thread
                        return AudioFileProperty(audioSmbFiles[0].path, audioSmbFiles[0].length())
                    } else { // audioSmbFiles.size > 1
                        // select one audio file randomly
                        val countFiles: Int = audioSmbFiles.size
                        val random = Random()
                        val randomNum = random.nextInt(countFiles)
                        // Got one sound file.
                        // PlayAudio in main thread
                        return AudioFileProperty(audioSmbFiles[randomNum].path, audioSmbFiles[randomNum].length())
                    }
                }
            } else if (smb.isFile) {
                if (!isMatchTail(smb.name, audioExtension)) {
                    // No audio file
                    return AudioFileProperty("",0L)
                } else { // a normal file
                    // Got one sound file.
                    // PlayAudio in main thread
                    return AudioFileProperty(smb.path, smb.length())
                }
            } else { // in case of smb is abnormal
                return AudioFileProperty("",0L)
            }
        } catch (e: Exception) {
            return AudioFileProperty("",0L)
        }
    }

    override fun onCleared() {
        try {
            smb.close()
        } catch (e: Exception) { }
        finally {
            viewModelScope.cancel()
            super.onCleared()
        }
    }

    /*
    * SMBサーバーのすべてのノーマルファイルを取得する再帰関数
    * パラメータ
    *   givenDir  取得対象のSMBディレクトリパス。
    *   tmpSmbNormalFiles  空のMutableList<SmbFile>型変数。
    *       1つ前の再帰関数実行結果を次の再帰関数に渡すための変数。
    *       ユーザーは最初にこの関数を起動するので、空のMutableListを与える。
    */
    private fun getNormalSmbFiles(givenDir: SmbFile, tmpSmbNormalFiles: MutableList<SmbFile>): MutableList<SmbFile> {
        val childSmbs = givenDir.listFiles()
        if (childSmbs.size > 0) {
            for (eachChild in childSmbs) {
                if (eachChild.isDirectory) {
                    getNormalSmbFiles(eachChild, tmpSmbNormalFiles)
                } else if (eachChild.isFile) {
                    tmpSmbNormalFiles.add(eachChild)
                } else {
                    continue
                }
            }
        }
        return tmpSmbNormalFiles
    }

    /*
     * target文字列の一番後ろのピリオド以降にconditionStr正規表現文字列が含まれていればtrueを返す
     */
    private fun isMatchTail(target: String, conditionStr: String): Boolean {
        // target中の一番後ろのピリオド(ファイル拡張子の区切り)の場所を検出する
        val idxStr = target.lastIndexOf(".")
        if (idxStr < 0) {
            return false
        }
        // targetから、ピリオドから末尾までの文字列を切り出す
        val extenStr = target.substring(idxStr)
        // 切り出した文字列がconditionStrに一致するか調べる
        val regex = (conditionStr+"$").toRegex(RegexOption.IGNORE_CASE)
        return regex.containsMatchIn(extenStr)
    }
}


SmbViewModelFactory.kt

// 2021 Sep. 06.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

class SmbViewModelFactory(val user: String, val password: String, val domain: String, val smbUrl: String): ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return SmbViewModel(user, password, domain, smbUrl) as T
    }
}