rokkonet

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

android開発 画面回転後のActivityにおいてLiveDataによるデータ更新を防ぐ

2021 Sep. 18.

状況

ViewModel内のLiveDataの値に従ってActivityで音楽再生するようにしていると、画面回転によって意図しない音楽再生となった時の対処

概要

Activityのcreateが直前にdestroyされたことによるものなのかどうかを示すフラグ変数を持つ。
onSaveInstanceState()でActivityのdestroy時にフラグ変数を保存する。
onCreate()時にフラグ変数を取り出す。
フラグ変数に応じてLiveDataのObserverの処理を行う。

Activity.kt

class MainActivity : AppCompatActivity() {
    private var isDestroy: Boolean = false

    override fun onCreate(savedInstanceState: Bundle?) {
        if (savedInstanceState != null) {
            isDestroy = savedInstanceState.getBoolean("IS_DESTROY")
        }

        val someLiveDataObserver = Observer<T> { some ->
            if (isDestroy) {

            } else {

            }
        }

        fooViewModel.someLiveData.observe(this, someObserver)
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        isDestroy = true
        outState.putBoolean("IS_DESTROY", true)
    }
}

android開発 ViewModel コルーチンで非同期ワーカースレッド内での処理結果をBoolean値で取得する

2021 Sep. 17.

概要

下記コード例では、
getConnectionSmb()において、非同期ワーカースレッドでSMB接続を行うconnectSmb(): Booleanの結果を取得している。
getConnectionSmb()内でviewModelScope.launch(Dispatchers.IO) { }を利用し、その中でconnectSmb()を呼び出している。
connectSmb()内ではコルーチンに関わるものは何も記述していない。
getConnectionSmb()内のviewModelScope.launch(Dispatchers.IO)により、connectSmb()内は非同期ワーカースレッドでの動作となり、また、そこに記述されたコマンドが、前の処理が終了したら次の処理着手と、順次実行されるので、SMB接続処理終了を待ってconnectSmb()の結果がgetConnectionSmb()に返される。


kotlinコード

SmbViewModel.kt

// 2021 Sep. 17

class SmbViewModel(val user: String, val password: String, val domain: String, val smbUrl: String): ViewModel() {

    lateinit var smb: SmbFile

    fun getConnectionSmb(){
        viewModelScope.launch(Dispatchers.IO) {
            // connect to SMB server in worker thread
            Log.d("MyApp", "Now start Connecting to SMB")
            if (connectSmb()) {
                Log.d("MyApp", "Connected to SMB")

            } else {
                Log.d("MyApp", "Failed to connect to SMB")
            }
        }
    }


    // SMBサーバーに接続し、接続オブジェクトをsmbに入れる
    private fun connectSmb(): Boolean {
        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)
            Log.d("MyApp", "List: "+ smb.list())  // Check if SmbFile() succeeded or failed
        } catch (e: Exception) {
            Log.d("MyApp", "1: " + e.toString())
            try {
                smb.close()
            }
            catch (e: Exception) {
                Log.d("MyApp", "2: " + e.toString())
                return false
            }
            return false
        }
        Log.d("MyApp", "Finished to connect to SMB function")
        return true
    }


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

jcifs-ng SMBサーバーへの接続の成否確認

2021 Sep. 17.

SmbFile#list()で例外が発生するかどうかで確認できる。

接続先パスがファイルでもディレクトリでも同じように動作した。
SmbFileのコンストラクト時に接続成否を把握できなかった。

サンプルkotlin androidコード

    fun connectSmb(): Boolean {
        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)
            Log.d("MyApp", "List: "+ smb.list())  // <- Checking the result of SMB connection here
        } catch (e: Exception) {
            Log.d("MyApp", "1: " + e.toString())
            try {
                smb.close()
            }
            catch (e: java.lang.Exception) {
                Log.d("MyApp", "2: " + e.toString())
                return false
            }
            return false
        }
        Log.d("MyApp", "Finished to connect SMB function")
        return true
    }


SMBサーバーへの接続に失敗した時の出力例

D/MyApp: 1: jcifs.smb.SmbAuthException: Logon failure: unknown user name or bad password.

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

2021 Nov. 14.
2021 Sep. 24.
2021 Sep. 16.

概要

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

インストールしたandroid端末

Android バージョン 7
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)

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

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {

        applicationId "PACKAGE.PROJECT"
        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 '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.1'
    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.3.0-beta01"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-beta01"
    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'

    def preference_version = "1.1.1"
    // Java language implementation
    // implementation "androidx.preference:preference:$preference_version"
    // Kotlin
    implementation "androidx.preference:preference-ktx:$preference_version"
}


許可権限

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

    <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.RandomSmbSoundPlay"
        >
        <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="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/tvInfo02"
        />

    <TextView
        android:id="@+id/tvInfo02"
        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/tvInfo01"
        app:layout_constraintBottom_toTopOf="@+id/tvFilePath"
        />

    <TextView
        android:id="@+id/tvFilePath"
        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/tvInfo02"
        app:layout_constraintBottom_toTopOf="@+id/tvFileSize"
        />

    <TextView
        android:id="@+id/tvFileSize"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/btnQuit"
        app:layout_constraintTop_toBottomOf="@+id/tvFilePath"
        app:layout_constraintBottom_toTopOf="@id/btnPlay"
        />

    <Button
        android:id="@+id/btnQuit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/quit"
        app:layout_constraintLeft_toLeftOf="@id/btnReset"
        app:layout_constraintRight_toRightOf="@id/btnReset"
        app:layout_constraintTop_toBottomOf="@id/tvFilePath"
        app:layout_constraintBottom_toTopOf="@id/btnReset"
        app:layout_constraintBaseline_toBaselineOf="@id/tvFileSize"
        />

    <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:text="@string/replay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        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

// 2021 Nov. 14.
// 2021 Oct. 31.
// 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.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 PACKAGE.PROJECT.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 = "/"
    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

    val defaultSharedPreferences by lazy {PreferenceManager.getDefaultSharedPreferences(getApplicationContext())}

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

    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 )

                val smbSettingdialog = SmbServerSettingDialogFragment()
                smbSettingdialog.setArguments(args)

                // ダイアログを表示
                smbSettingdialog.show(supportFragmentManager, "simple")
            }
        }
        return super.onOptionsItemSelected(item)
    }

    // SmbServerSettingDialogFragment.DialogListenerインターフェースのメソッドの実装
    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 onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // setContentView(R.layout.activity_main)

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

        // get smb setting values
        with (defaultSharedPreferences) {
            smbDomain = getString("smbDomain", "") ?: ""
            smbRoot = getString("smbPath", "") ?: ""
            smbUser = getString("smbUser", "") ?: ""
            smbPassword = getString("smbPassword", "") ?: ""
        }

        // if any of values is empty, show dialog to get value
        if (    smbDomain.length < 1 ||
                smbRoot.length < 1 ||
                smbUser.length < 1 ||
                smbPassword.length < 1 ) {

            // smb設定ダイアログに渡す値をBundleにセット
            val args = Bundle()
            with(args) {
                putString("smbDomain", smbDomain )
                putString("smbRootPath", smbRoot )
                putString("smbUser", smbUser )
                putString("smbPassword", smbPassword )
            }

            val smbSettingdialog = SmbServerSettingDialogFragment()
            smbSettingdialog.setArguments(args)

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

        // 画面回転等によりDestroyされた後の場合
        if (savedInstanceState != null) {
            isDestroy = savedInstanceState.getBoolean("IS_DESTROY")
        }

        // リプレイボタン・リセットボタン
        if (binding.btnPlay.isEnabled) {
            binding.btnReplay.isEnabled = false
            binding.btnReset.isEnabled = false
        } else {
            binding.btnReplay.isEnabled = true
            binding.btnReset.isEnabled = true
        }

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


            // 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.ProcessStatus> { status ->
                    when(status) {
                        SmbViewModel.ProcessStatus.notYetGot-> binding.tvInfo01.setText(R.string.notConnectedSmb)
                        SmbViewModel.ProcessStatus.fromNow -> binding.tvInfo01.setText(R.string.fromNowTryConnectSmb)
                        SmbViewModel.ProcessStatus.trying -> binding.tvInfo01.setText(R.string.tryConnectingSmb)
                        SmbViewModel.ProcessStatus.got -> binding.tvInfo01.setText(R.string.connectedSmb)
                        SmbViewModel.ProcessStatus.failed -> {
                            binding.tvInfo01.setText(R.string.failedConnectSmb)
                            binding.tvInfo02.setText(R.string.tryGetAudioFile)

                            if (!isDestroy) {  // 直前にActivityがdestroyされたのではない場合
                                // play an audio file in external storage in android device
                                playAndroidExternalAudio()
                                binding.btnReplay.isEnabled = true
                                binding.btnReset.isEnabled = true
                            }
                        }
                    }
            }

            // SMBサーバーの音声ファイル取得状況に応じた処理
            val getSmbAudioFileStatusObserver = Observer<SmbViewModel.ProcessStatus> { status ->
                when (status) {
                    SmbViewModel.ProcessStatus.notYetGot -> binding.tvInfo02.setText(R.string.notGetSmbAudioFile)
                    SmbViewModel.ProcessStatus.fromNow -> binding.tvInfo02.setText(R.string.fromNowTryGetSmbAudioFile)
                    SmbViewModel.ProcessStatus.trying -> binding.tvInfo02.setText(R.string.tryGetSmbAudioFile)
                    SmbViewModel.ProcessStatus.got -> {
                        binding.tvInfo02.setText(R.string.gotSmbAudioFile)

                        if (!isDestroy) {  // 直前にActivityがdestroyされたのではない場合
                            // 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.ProcessStatus.failed -> { // SMBサーバーの音声ファイル取得に失敗
                        binding.tvInfo02.setText(R.string.failedGetSmbAudioFile)

                        if (!isDestroy) {  // 直前にActivityがdestroyされたのではない場合
                            // 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 {
            // restart application
            val launchIntent = baseContext.packageManager
                .getLaunchIntentForPackage(baseContext.packageName)
            launchIntent!!.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
            finish()
            startActivity(launchIntent)
        }

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

    // Activityのdestroy時のインスタンス保存
    //   onPauseの直後に呼ばれる
    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: String = getPathFromUri(this, audioUri) ?: throw Exception("audioPath is null.")
            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   //並べ替え
        ) ?: throw Exception("resolver.query returned 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)
    }

    /*
     * Thank for https://stackoverflow.com/questions/61356281/android-files-unable-to-get-file-path-from-content-uri-when-file-is-selected-f
     * URIからファイルパスを取得する
     */
    private 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)
                    val contentUri = ContentUris.withAppendedId(
                        Uri.parse("content://downloads/public_downloads"),
                        java.lang.Long.valueOf(id)
                    )
                    return getDataColumn(context, contentUri, null, null) ?: throw Exception("getDataColumn in downloadsDocument is 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
                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
                } else {
                    throw Exception("Can not get contentUri.")
                }
                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 ?: throw Exception("uri.lastPathSegment is null.")
                else getDataColumn(
                    context,
                    uri,
                    null,
                    null
                )

        } else if ("file".equals(uri.scheme, ignoreCase = true)) {
            return uri.path ?: throw Exception("uri.path is null.")
        }
        throw Exception("Can not getPathFromUri.")
    }

    // a function used in getPathFromUri()
    private fun getDataColumn(
        context: Context, uri: Uri, selection: String?,
        selectionArgs: Array<String>?
        ): String {

        // var cursor: Cursor = null
        val column = "_data"
        val projection = arrayOf(
            column
        )
        val cursor by lazy {
            context.contentResolver.query(
                uri, projection, selection, selectionArgs,
                null
            ) ?: throw Exception("context.contentResolver.query is null.")
        }
        try {
            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"
    }

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

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

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

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


SmbServerSettingDialogFragment.kt

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

// 2021 Oct. 30.
// 2021 Sep. 20.

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

class SmbServerSettingDialogFragment: DialogFragment()  {

    // Thank for https://qiita.com/gino_gino/items/9390e1a0ce5e42d16466
    interface DialogListener{
        public fun onDialogMapReceive(dialog: DialogFragment, smbSettings: MutableMap<String, String>) //Activity側へMutableMapを渡す
    }

    var listener:DialogListener? = null

    // ダイアログ上のEditTextに文字列があるか無いかで buttonの enable を制御する
    class EditsWatcher(val button: Button, val edit1: EditText, val edit2: EditText, val edit3: EditText, val edit4: EditText):
        TextWatcher {
        init {
            edit1.addTextChangedListener(this)
            edit2.addTextChangedListener(this)
            edit3.addTextChangedListener(this)
            edit4.addTextChangedListener(this)
            afterTextChanged(null)
        }

        override fun beforeTextChanged(
            s: CharSequence?,
            start: Int,
            count: Int,
            after: Int
        ) {
        } //ignore

        override fun onTextChanged(
            s: CharSequence?,
            start: Int,
            before: Int,
            count: Int
        ) {
        } //ignore

        override fun afterTextChanged(s: Editable?) {
            button.isEnabled =
                edit1.text.toString().trim().isNotEmpty() &&
                edit2.text.toString().trim().isNotEmpty() &&
                edit3.text.toString().trim().isNotEmpty() &&
                edit4.text.toString().trim().isNotEmpty()
        }
    }

    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)

        // Dialog上のEditTextオブジェクトを取得する
        val edDomain = smbServerSettingView.findViewById<EditText>(R.id.etDomain)
        val edSmbPath = smbServerSettingView.findViewById<EditText>(R.id.etSmbPath)
        val edSmbUser = smbServerSettingView.findViewById<EditText>(R.id.etSmbUser)
        val edSmbPassword = smbServerSettingView.findViewById<EditText>(R.id.etSmbPassword)

        // 呼び出し元でBundleにセットした値を取得し、Viewにセットする
        edDomain.setText(requireArguments().getString("smbDomain", ""))
        edSmbPath.setText(requireArguments().getString("smbRootPath", ""))
        edSmbUser.setText(requireArguments().getString("smbUser", ""))
        edSmbPassword.setText(requireArguments().getString("smbPassword", ""))

        builder.setView(smbServerSettingView)
            .setTitle("SMB Server Setting")
            .setPositiveButton("OK") { dialog, id ->
                with(smbSettings) {
                    put(
                        "smbDomain",
                        edDomain.text.toString()
                    )
                    put(
                        "smbPath",
                        edSmbPath.text.toString()
                    )
                    put(
                        "smbUser",
                        edSmbUser.text.toString()
                    )
                    put(
                        "smbPassword",
                        edSmbPassword.text.toString()
                    )
                }

                // pass data to the Activity having called this dialog
                listener?.onDialogMapReceive(this,smbSettings) ?: throw Exception("listener is null.")

            }
            .setNegativeButton("Cancel") { dialog, id ->
                // nothing is done
            }

        return builder.create().also { dialog ->  // dialogを返すと共にdialogにOnShowListenerをセットする
            dialog.setOnShowListener({
                val button = dialog.getButton(Dialog.BUTTON_POSITIVE)
                EditsWatcher(button, edDomain, edSmbPath, edSmbUser, edSmbPassword)
            })
        }
    }

    // DialogListenerインターフェースをlistenerにセットする
    //   MainActivityで実装したDialogListenerインターフェースのメソッドを利用できるようになる
    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

// 2021 Oct. 30.
// 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 ProcessStatus {
        notYetGot,
        fromNow,
        trying,
        got,
        failed
    }

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

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

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

    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(ProcessStatus.trying) // tyring to connect to SMB
                connectSmb()
            } catch (e: Exception) {
                _smbConnectionStatus.postValue(ProcessStatus.failed) // Failed to connect to SMB"
                return@launch
            }
            // got connection to SMB
            _smbConnectionStatus.postValue(ProcessStatus.got) // connected to SMB

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

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

        if ( ProcessStatus.failed == smbConnectionStatus.value) {
            // Failed to connect to SMB
            return
        }
        if (ProcessStatus.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
    }
}

SMBサーバーからランダムに1つの音声ファイルを取得するkotlinコード

2021 Sep. 18.
2021 Sep. 12.

SMBサーバーへの接続にjfite-ngを利用する。

data class AudioFileProperty(val smbPath: String, val fileSize: Long)
val audioExtension: String = "\\.mp3|\\.wav|\\.aac|\\.au|\\.gsm|\\.m4a|\\.ogg|\\.flac"


    /*
     * ランダムにSMBサーバーの音楽ファイルを1つ選び、そのパスとファイルサイズをAudioFilePropertyデータクラスで返す
     *  data class AudioFileProperty(val smbPath: String, val fileSize: Long)
     * 失敗した時は AudioFileProperty("",0L) を返す。
     * 変数smbは、jcifs-ngによって取得したSmbFileインスタンス
     * audioExtensionは、音声ファイル拡張子集合の文字列
     *  val audioExtension: String = "\\.mp3|\\.wav|\\.aac|\\.au|\\.gsm|\\.m4a|\\.ogg|\\.flac"
     */
    fun selectAudioSmbFile(smb: SmbFile, audioExtension: String ): 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
                        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
                        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
                        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
                    return AudioFileProperty(smb.path, smb.length())
                }
            } else { // in case of smb is abnormal
                return AudioFileProperty("",0L)
            }
        } catch (e: Exception) {
            return AudioFileProperty("",0L)
        }
    }
/*
    * 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)
    }

android開発 URIからファイルパスを取得する

2021 Sep. 12.

出典 Android: Files: Unable to get file path from content URI when file is selected from RECENT section in file browser - Stack Overflow

    fun getPathFromUri(context: Context, uri: Uri): String? {
        // DocumentProvider
        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]

                // TODO handle non-primary volumes
            } 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
    }

android開発 Intentで起動するアプリを指定する

2021 Sep. 12.

Intent#setPackageでアプリを指定する

VLCメディアプレーヤーを指定したインテント
val audioIntent = Intent()
audioIntent.action = Intent.ACTION_VIEW
audioIntent.setDataAndType(uri, "audio/*")
audioIntent.setPackage("org.videolan.vlc")
startActivity(audioIntent)