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つの音声ファイルをストリーミング再生する

2022 Jul. 18.
2022 Feb. 13.
2021 Dec. 05.
2021 Nov. 14.
2021 Sep. 16.

ソース保管場所

https://bitbucket.org/arsmus/randomsmbsoundplay/src/master/

概要

SMBサーバーからランダムに1つの音声ファイルをストリーミング再生する。
SMBサーバーにつながらなかったら、android端末の外部共有ストレージからランダムに1つの音声ファイルをストリーミング再生する。
再生にはサードパーティ再生アプリをIntent起動する。
ViewModelを使ったワーカースレッド内でSMBサーバーに接続する。
SMBサーバーへの接続にはjcifs-ngを利用。
ViewModelのLiveData機能でSMBサーバーファイルのファイルパスをメインスレッドに渡す。
メインスレッド内で、SMBサーバーファイルのファイルパスをURIにパースして音声再生Intentに渡し、Intentを発出する。
ContentResolverで得たURIをそのままIntentに与えても動作しない。パスを取得し、そのパスをURIにパースする必要がある。パスは _dataカラムから取得できる。

インストールしたandroid端末

android バージョン 7
android バージョン 11

コンパイル環境

compileSdkVersion 31
minSdkVersion 24
targetSdkVersion 31


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

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.1"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
    implementation files('libs/jcifs-ng-2.1.7.jar')
    implementation group: 'org.bouncycastle', name: 'bcprov-jdk15to18', version: '1.70'
    implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32'
    implementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.32'


全体

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

android {
    compileSdk 31

    defaultConfig {
        applicationId "YOURPROJECT"
        minSdk 24
        targetSdk 31
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        viewBinding true
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    implementation 'androidx.recyclerview:recyclerview:1.2.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.4.1"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
    implementation files('libs/jcifs-ng-2.1.7.jar')
    implementation group: 'org.bouncycastle', name: 'bcprov-jdk15to18', version: '1.70'
    implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32'
    implementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.32'

    implementation "androidx.preference:preference-ktx:1.2.0"
}


AndroidManifest.xml

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

    <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"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

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

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


played_files_list.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/listViewFiles"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>


optionsmenu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/itemSmbServer"
        android:title="@string/menuItemServer" />
    <item
        android:id="@+id/itemPlayedFileHistory"
        android:title="@string/menuItemPlayedHistory" />
</menu>


strings.xml

<resources>
    <string name="app_name">RandomSmbSoundPlay</string>
    <string name="kb">%sKB</string>
    <string name="sentIntent">Sent Intent for playing audio</string>
    <string name="waitMinutes">Wait minutes to connect SMB server</string>
    <string name="hintPath">Filepath</string>
    <string name="hintSize">Filesize</string>
    <string name="play">Play</string>
    <string name="replay">Replay</string>
    <string name="reset">Reset</string>
    <string name="quit">Quit</string>
    <string name="tryPlayAndroidFile">Trying to play a file in local device</string>

    <string name="notConnectedSmb">Not connected to SMB server</string>
    <string name="fromNowTryConnectSmb">Connect to SMB server from now</string>
    <string name="tryConnectingSmb">Trying to connect to SMB server</string>
    <string name="connectedSmb">Connected to SMB server</string>
    <string name="failedConnectSmb">Failed to connect to SMB server</string>

    <string name="notGetAudioFile">Not got an audio file</string>
    <string name="fromNowTryGetAudioFile">Get an audio file from now</string>
    <string name="tryGetAudioFile">Trying to get an audio file</string>
    <string name="gotAudioFile">Got an audio file</string>
    <string name="failedGetAudioFile">Failed to get an audio file</string>

    <string name="notGetSmbAudioFile">Not got a smb audio file</string>
    <string name="fromNowTryGetSmbAudioFile">Get a smb audio file from now</string>
    <string name="tryGetSmbAudioFile">Trying to get a smb audio file</string>
    <string name="gotSmbAudioFile">Got a smb audio file</string>
    <string name="failedGetSmbAudioFile">Failed to get a smb audio file</string>

    <string name="gotLocalAudioFile">Got an audio file from local device</string>
    <string name="server">Server</string>
    <string name="domain">Domain</string>
    <string name="smbPath">SMB_Path</string>
    <string name="userName">UserName</string>
    <string name="password">Password</string>
    <string name="hintDomain">192.168.1.1</string>
    <string name="hintSmbRootPath">/SMB/PATH/</string>
    <string name="hintUserName">user</string>
    <string name="hintPassword">password</string>
    <string name="noNetwork">No network</string>

    <string name="menuItemServer">Server Setting</string>
    <string name="menuItemPlayedHistory">Play History</string>

    <string name="pathHistoryFile">path_history.txt</string>
    <string name="noHistory">No history</string>
    <string name="noPermission">No permission is granted.</string>
</resources>


MainActivity.kt

// 2022 Feb. 13.
// 2021 Dec. 28.
// 2021 Nov. 14.
// 2021 Oct. 31.
// 2021 Sep. 06.

// Ryuichi Hashimoto

import android.Manifest
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
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.*
import java.util.*
import java.util.concurrent.TimeUnit


class MainActivity : AppCompatActivity(),SmbServerSettingDialogFragment.DialogListener {

    //////////////////////////////////////////////
    // Please replace these values to your data //
    /*
    private var smbDomain: String = "192.168.1.1"
    private var smbRoot: String = "/SMB/SERVER/DIR/"
    private var smbUser: String = ""
    private var smbPassword: String = ""
    */
    private var smbDomain: String = "192.168.1.1"
    private var smbRoot: String = "/"
    private var smbUser: String = ""
    private var smbPassword: String = ""
    //////////////////////////////////////////////

    private var smbFilepath: String = ""
    private lateinit var binding: ActivityMainBinding
    private lateinit var audioUri: Uri

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

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

    private val PERMISSION_READ_EX_STOR: Int = 100
    lateinit var appContext: Context

    // option menu
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        super.onCreateOptionsMenu(menu)
        val inflater = menuInflater
        inflater.inflate(R.menu.optionsmenu, 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")
            }

            R.id.itemPlayedFileHistory -> {
                val playedFilesDialog = ListPlayedFilesDialogFragment()
                playedFilesDialog.show(supportFragmentManager, "simple")
            }
        }
        return super.onOptionsItemSelected(item)
    }

    // SmbServerSettingDialogFragment.DialogListenerインターフェースのメソッドの実装
    override fun onDialogMapReceive(dialog: 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)

        appContext = applicationContext

        // 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")
        }

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

        // play-button to connect to SMB server and play audio
        binding.btnPlay.setOnClickListener {

            // Check network status.
            // Grateful for https://qiita.com/taowata/items/4609dcddc3ddb4840fd6

            // Network確認のため、ConnectivityManagerを取得する
            val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

            // NetworkCapabilitiesの取得
            // 引数にcm.activeNetworkを指定し、現在アクティブなデフォルトネットワークに対応するNetworkオブジェクトを渡している
            val capabilities = cm.getNetworkCapabilities(cm.activeNetwork)

            if (capabilities == null) {
                binding.tvInfo01.setText(R.string.noNetwork)
                playAndroidExternalAudio()
            } else {
                binding.tvInfo01.setText(R.string.waitMinutes)
                binding.btnPlay.isEnabled = false
                binding.btnReplay.isEnabled = true
                binding.btnReset.isEnabled = true
                binding.btnQuit.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)

                            // 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)

                            // 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)

                                // save audio-file-path to storage
                                lateinit var fw: FileWriter
                                val savePathFile =
                                    File(appContext.filesDir, getString(R.string.pathHistoryFile))
                                if (savePathFile.exists()) {
                                    fw = FileWriter(savePathFile, true)
                                } else {
                                    fw = FileWriter(savePathFile)
                                }
                                try {
                                    fw.write("$smbFilepath,");
                                } catch(e: IOException) {
                                    binding.tvInfo01.setText(e.toString())
                                } finally {
                                    try {
                                        fw.close()
                                    } catch(e: IOException) {
                                        binding.tvInfo01.setText(e.toString())
                                    }
                                }
                            } catch (e: Exception) {
                                binding.tvInfo01.setText(e.toString())
                            }
                        }

                        SmbViewModel.ProcessStatus.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 {
            binding.btnPlay.isEnabled = false
            binding.btnReplay.isEnabled = true
            binding.btnReset.isEnabled = true
            binding.btnQuit.isEnabled = true

            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())
                }
            } else {
                // in case to play audio in the 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.btnPlay.isEnabled = true
            binding.btnReplay.isEnabled = false
            binding.btnReset.isEnabled = true
            binding.btnQuit.isEnabled = true

            // restart application
            try { smbViewModel.smb.close() } catch (e: Exception) { }
            val launchIntent = baseContext.packageManager
                .getLaunchIntentForPackage(baseContext.packageName)
            launchIntent!!.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
            finish()
            startActivity(launchIntent)
        }

        // quit-button
        binding.btnQuit.setOnClickListener {
            try { smbViewModel.smb.close()
            } catch (e: Exception) { }
            finishAndRemoveTask()
        }
    }

    // Activityのdestroy時のインスタンス保存
    //   onPauseの直後に呼ばれる
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)

        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 )

        outState.putBoolean("btnPlay", binding.btnPlay.isEnabled)
        outState.putBoolean("btnReplay", binding.btnReplay.isEnabled)
        outState.putBoolean("btnReset", binding.btnReset.isEnabled)
        outState.putBoolean("btnQuit", binding.btnQuit.isEnabled)
    }

    // 保存されたインスタンスの読み込み
    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)
        smbFilepath = pathText

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

        binding.btnPlay.isEnabled = savedInstanceState.getBoolean("btnPlay")
        binding.btnReplay.isEnabled = savedInstanceState.getBoolean("btnReplay")
        binding.btnReset.isEnabled = savedInstanceState.getBoolean("btnReset")
        binding.btnQuit.isEnabled = savedInstanceState.getBoolean("btnQuit")
    }

    /*
     * create and send Intent of audio-player
     *  play with 3rd party media play application
     */
    private fun playAndroidExternalAudio() {
        // パーミッションがあれば処理を進める。無ければ取得する
        if ( isGrantedReadStorage()) {
            getPlayStorageMedia()
        } else {
            requestPermissions(
                arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
                PERMISSION_READ_EX_STOR
            )
            // requestPermissions()の結果に応じた処理がonRequestPermissionsResult()で行われる
        }
    }

    // アプリにパーミッションが付与されているかどうかを確認するメソッド
    fun isGrantedReadStorage(): Boolean {
        return (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
                == PackageManager.PERMISSION_GRANTED)
    }

    // requestPermissions()の結果に対する処理を行うメソッド
    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults: IntArray) {

        when (requestCode) {
            PERMISSION_READ_EX_STOR -> {
                if (grantResults.isEmpty()) {
                    // STOP WORK
                    throw RuntimeException("Empty permission result")
                }
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    // permission is got.
                    // YOU CAN CONTINUE YOUR WORK WITH EXTERNAL_STORAGE HERE
                    getPlayStorageMedia()

                } else {  // no permission is got
                    if (shouldShowRequestPermissionRationale(
                            Manifest.permission.READ_EXTERNAL_STORAGE)) {
                        // permission request was denied.
                        // User declined, but system can still ask for more
                        // request permission agein
                        requestPermissions( arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSION_READ_EX_STOR)
                    } else {
                        // permission request was denied and requested "no not ask".
                        // User declined and system can't ask.
                        // CODES in case of rejection to get permission here
                        Toast.makeText(applicationContext, R.string.noPermission, Toast.LENGTH_LONG).show()
                    }
                }
            }
        }
    }

    fun getPlayStorageMedia() {

        var pathOfUri: String= ""

        val collection =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                MediaStore.Audio.Media.getContentUri(
                    MediaStore.VOLUME_EXTERNAL
                )
            } else {
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
            }

        val columns = arrayOf(
            MediaStore.Audio.Media._ID,
            MediaStore.Audio.Media.DISPLAY_NAME,
            MediaStore.Audio.Media.SIZE,
            "_data"
        )

        // 1分間以上のAudioを指定する
        val selection = "${MediaStore.Audio.Media.DURATION} >= ?"
        val selectionArgs = arrayOf(
            TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES).toString()
        )

        val resolver = applicationContext.contentResolver
        val query = resolver.query(
            collection,  //データの種類
            columns, //取得する項目 nullは全部
            selection, //フィルター条件 nullはフィルタリング無し
            selectionArgs, //フィルター用のパラメータ
            null   //並べ替え
        )
        // Log.d( "MyApp" , Arrays.toString( query?.getColumnNames() ) )  //項目名の一覧を出力
        val numCount = query?.count
        // Log.d("MyApp", "Num raws : $numCount")

        query?.use { cursor ->
            val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
            val displayNameColumn =
                cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
            val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
            val column_index = cursor.getColumnIndexOrThrow("_data")

            cursor.moveToPosition(getRandomNum(numCount!!))
            val id = cursor.getLong(idColumn)
            val displayName = cursor.getString(displayNameColumn)
            val mediaSize = cursor.getInt(sizeColumn)
            val contentUri: Uri = ContentUris.withAppendedId(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI,id)
            pathOfUri = cursor.getString(column_index)
            binding.tvInfo02.setText(R.string.gotLocalAudioFile)
            binding.tvFilePath.setText(pathOfUri)
            val sizeKb = (mediaSize / 1000 ).toString() + "KB"
            binding.tvFileSize.setText(sizeKb)

            // play audio
            val audioIntent = Intent()
            audioIntent.action = Intent.ACTION_VIEW
            audioIntent.setDataAndType(Uri.parse(pathOfUri), "audio/*")
            // audioIntent.setDataAndType(contentUri, "audio/*")  <- NG
            audioIntent.setPackage("org.videolan.vlc")
            startActivity(audioIntent)
            binding.tvInfo02.setText(R.string.sentIntent)

            // save audio-file-path to storage
            lateinit var fw: FileWriter
            val savePathFile =
                File(appContext.filesDir, getString(R.string.pathHistoryFile))

            fw = if (savePathFile.exists()) {
                FileWriter(savePathFile, true)
            } else {
                FileWriter(savePathFile)
            }

            try {
                fw.write("$pathOfUri,");
            } catch(e: IOException) {
                binding.tvInfo01.setText(e.toString())
            } finally {
                try {
                    fw.close()
                } catch(e: IOException) {
                    binding.tvInfo01.setText(e.toString())
                }
            }

            /*
            Log.d(
                "MyApp", "id: $id, name: $displayName, ${mediaSize}Byte, uri: $contentUri"
            )
            */

            // Log.d("MyApp", "Selected " + pathOfUri!!)

        }
        query?.close()
        return
    }

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


SmbViewModel.kt

// 2021 Dec. 05.
// 2021 Oct. 30.
// 2021 Sep. 06.

// Ryuichi Hashimoto

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(private val user: String, private val password: String, private val domain: String, private val smbUrl: String): ViewModel() {
    private 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を設定する
    private var _smbConnectionStatus = MutableLiveData(ProcessStatus.NotYetGot)
    val smbConnectionStatus: LiveData<ProcessStatus>
        get() = _smbConnectionStatus

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

    private 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

                // 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
                val smbNormalFiles: MutableList<SmbFile>
                val 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
                    val 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) {
                return if (!isMatchTail(smb.name, audioExtension)) {
                    // No audio file
                    AudioFileProperty("",0L)
                } else { // a normal file
                    // Got one sound file.
                    // PlayAudio in main thread
                    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.isNotEmpty()) {
            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
    }
}


SmbServerSettingDialogFragment.kt

// 2021 Dec. 04.
// 2021 Oct. 30.
// 2021 Sep. 20.

// Ryuichi Hashimoto

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


ListPlayedFilesDialogFragment.kt

// 2022 Feb. 13.
// 2022 Feb. 05.

// Ryuichi Hashimoto

import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import android.app.AlertDialog
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.Toast
import java.io.*

class ListPlayedFilesDialogFragment: DialogFragment(){
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        // get Dialog
        val builder = AlertDialog.Builder(activity)
        val inflater = requireActivity().layoutInflater
        val myDialogView = inflater.inflate(R.layout.played_files_list, null)

        // get array of played files from saved file
        var playedFiles = ""
        lateinit var br: BufferedReader
        try {
            val savePathFile = File(this.context?.filesDir, getString(R.string.pathHistoryFile))
                // savePathFile has only one line.
                // the line is filePath data in which each datum is separated by ','.
            if (savePathFile.exists()) {
                br = BufferedReader(FileReader(savePathFile))
                try {
                    playedFiles = br.readLine()
                } catch(e: IOException) {
                    playedFiles = e.toString()
                } finally {
                    try {
                        br.close()
                    } catch(e: IOException) {
                        playedFiles = playedFiles + " " + e.toString()
                    }
                }
            }
        } catch (e: Exception){
            playedFiles = playedFiles + " " + e.toString()
        }
        if (playedFiles == "") {
            playedFiles = getString(R.string.noHistory)
        }
        playedFiles = playedFiles.removeSuffix(",")
        val playedFileList = playedFiles.split(",").asReversed()

        // システムに組み込まれた"android.R.layout.simple_list_item_1"を介して、
        // playedFileListをArrayAdapterに関連づける
        val myAdapter =
            ArrayAdapter(this.requireContext(), android.R.layout.simple_list_item_1, playedFileList)

        // ArrayAdapterをダイアログ上のレイアウト内のListViewにセットする
        val listViewFiles = myDialogView.findViewById<ListView>(R.id.listViewFiles)
        listViewFiles.adapter = myAdapter

        // display dialog
        builder.setView(myDialogView)
            .setTitle("Played Files")
            .setPositiveButton("Clear") { dialog, which ->
                lateinit var fos: FileOutputStream
                try {
                    val savePathFile =
                        File( context?.filesDir, getString(R.string.pathHistoryFile))
                    fos = FileOutputStream(savePathFile)
                    fos.write(','.code.toInt())
                } catch (e: Exception) {
                    Toast.makeText(context,e.toString(), Toast.LENGTH_LONG ).show()
                } finally {
                    fos.close()
                }
            }
            .setNeutralButton("OK") { _, _ ->
                // nothing is done
            }

        return builder.create()
    }
}


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

2022 Feb. 13
2021 Sep. 18.
2021 Sep. 12.

SMBサーバーへの接続にjcifs-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からファイルシステム上のパスを取得する

2022 May 02.
2021 Sep. 12.

出典 Invalid URI at getting path in different android api - Stack Overflow

端末

Android 11 (API レベル 30)

概要

ContentResolverにて、collectionにuriを、projectionに"_data"をセットしqueryする

サンプルkotlinコード
// get filepath of myUri
val proj = arrayOf("_data")
val cursor = this.getContentResolver().query(myUri, proj, null, null, null)
if (cursor == null) {
    findViewById<TextView>(R.id.textViewFilePath).text = "Could not find."
} else {
    val pathColumn = cursor.getColumnIndexOrThrow("_data")
    cursor.moveToFirst()
    findViewById<TextView>(R.id.textViewFilePath).text = cursor.getString(pathColumn)
    cursor.close()
}



(参考)
出典 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で起動するアプリを指定する

2022 Dec. 04.
2022 Jan. 30.
2021 Sep. 12.

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

音声uriVLCメディアプレーヤーを指定するインテント
// audioUri: Uri  URI of an audio content
val audioIntent = Intent()
audioIntent.action = Intent.ACTION_VIEW
audioIntent.setDataAndType(audioUri, "audio/*")
audioIntent.setPackage("org.videolan.vlc")
startActivity(audioIntent)


ContentResolverで得たURIをそのままIntentに与えても動作しない。パスを取得し、そのパスをURIにパースする必要がある。 パスは _dataカラムから取得できる。

ファイル選択にXplorerファイラーを指定したインテント

(ファイル選択可能だがディレクトリは選択できない)

val filerIntent = Intent(Intent.ACTION_GET_CONTENT)
filerIntent.setPackage("com.lonelycatgames.Xplore")
filerIntent.type = "*/*"
filerLauncher.launch(filerIntent)

// ファイラー起動と結果受け取り
val filerLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        result ->
    // result は ActivityResult 型で、resultCode と data (Intent) を持つ。
    if (result.resultCode == RESULT_OK) {
        result.data?.dataString?.let { onFileResult(it) }
    }
}

// ファイルパス取得
private fun onFileResult(dataString: String) {
    try {
        val filePath = dataString.replace("file://", "")
        val decodedfilePath = URLDecoder.decode(filePath, "utf-8")
    } catch (e: UnsupportedEncodingException) {
        // 例外処理
    }
}