

android・コルーチンにおいて、非同期スレッドを実行中に、非同期スレッドで取得したデータをアンドロイドの画面に表示する方法をお教えください。

具体例として、jcifs-ng、コルーチンを使って、アンドロイド端末からSMBサーバーに接続してファイルをダウンロードする下記コードを書きました。  
このコードにおいて、SMBサーバーに接続した時点でサーバーのファイルの情報（ファイル名とかサイズとか）をアンドロイドの画面に表示するにはどうすればよいでしょうか？  
下記コードのconnectSmbDownload()関数内でのLogcat出力をアンドロイドの画面のTextView（tvInfo2）に表示させたいです。

[code]

    import android.os.Bundle
    import android.util.Log
    import android.view.View
    import android.widget.TextView
    import androidx.appcompat.app.AppCompatActivity
    import jcifs.CIFSContext
    import jcifs.config.PropertyConfiguration
    import jcifs.context.BaseContext
    import jcifs.smb.NtlmPasswordAuthentication
    import jcifs.smb.SmbFile
    import kotlinx.coroutines.*
    import java.io.File
    import java.io.FileOutputStream
    import java.util.*
    import kotlin.coroutines.CoroutineContext
    
    
    class MainActivity : AppCompatActivity(), CoroutineScope {
        // 認証情報
        //////////////////////////////////////////////
        // Please replace these values to your data //
        val user = "USER"
        val password = "PASS"
        val domain = "192.168.1.1"
        val smbroot = "smb://" + domain + "/SMB/SERVER/FILE/"
        //////////////////////////////////////////////
    
        val TAG: String = "MySMB"
    
        // coroutine準備
        private val job = Job()
        override val coroutineContext: CoroutineContext
            get() = Dispatchers.Main + job
    
        // 終了時のcoroutineのキャンセル設定
        override fun onDestroy() {
            job.cancel()
            super.onDestroy()
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            val tvInfo1 = findViewById<View>(R.id.txtVwInfo1) as TextView
            val tvInfo2 = findViewById<View>(R.id.txtVwInfo2) as TextView
            val tvInfo3 = findViewById<View>(R.id.txtVwInfo3) as TextView
    
            launch {
                // output start-message in display
                tvInfo1.setText("Start!")
    
                // connect SMB server and download a file
                val localFile: File? = connectSmbDownload(user, password, domain, smbroot)
    
                // output finish message in display
                if (localFile != null) {
                    tvInfo3.setText(localFile.length().toString() + "Byte downloaded.")
                } else {
                    tvInfo3.setText( "no file downloaded.")
                }
            }
        }
    
        private suspend fun connectSmbDownload(user: String, password: String, domain: String, smbroot: String): File? {
            return withContext(Dispatchers.IO) {
                // connect to SMB server
                val smb = connectSMB(user, password, domain, smbroot)
                Log.d(TAG, "Got SMB file: " + smb.path)
    
                // download a SMB file to android-device
                var downloadedFile: File? = null
                if (smb.isFile) {
                    downloadedFile = cpSmbFile2android(smb)
                } else {
                    Log.d(TAG, smb.path + " is not a file.")
                }
                downloadedFile
            }
        }
    
        private suspend fun connectSMB(user: String, password: String, domain: String, smbroot: String): SmbFile {
            return withContext(Dispatchers.IO) {
                val prop = Properties()
                prop.setProperty("jcifs.smb.client.minVersion", "SMB202")
                prop.setProperty("jcifs.smb.client.maxVersion", "SMB300")
                val bc = BaseContext(PropertyConfiguration(prop))
                val creds = NtlmPasswordAuthentication(bc, domain, user, password)
                val auth: CIFSContext = bc.withCredentials(creds)
                SmbFile(smbroot, auth)
            }
        }
    
        /*
         * リモートのSMBファイルをandroid端末にコピーする
         * 　コピー先は、android端末の外部ストレージのアプリキャッシュ領域
         *  smbFile  コピー元リモートのSMBファイル
         *  返り値　コピー先ファイル
         */
        private fun cpSmbFile2android(smbFile: SmbFile): File? {
            if ( !smbFile.isFile ) {
                return null
            }
            val fileName = smbFile.name
            val exterCacheFile = File(this.externalCacheDir!!.path + "/" + fileName)
            // copy remote smb-file to local
            val inStream = smbFile.openInputStream()
            val fileOutStream = FileOutputStream(exterCacheFile)
            val buf = ByteArray(1024)
            var len: Int = 0
            while (true) {
                len = inStream.read(buf)
                if (len < 0) break
                fileOutStream.write(buf)
            }
            fileOutStream.flush();
            fileOutStream.close();
            inStream.close();
            return exterCacheFile
        }
    }
    
[/code]


CC BY-SA 4.0
2021-08-15T05:53:42.737
80813
2021-09-05T05:51:12.873
32105
question
android samba smb
android開発　コルーチンの非同期スレッド実行中に、非同期スレッド内で取得したデータをディスプレイに表示する方法は？


自己レスです。  
下記コードのように、非同期スレッドでの作業を1つのブロックにせず、メインスレッドにデータを渡したい場所の前後をそれぞれ別の関数とすることで非同期スレッド内のデータをメイスレッドに渡せました。  
今回の例で言えば、SMBサーバーへの接続とファイルダウンロードを1つの関数にしていたのを、SMBサーバー接続関数とファイルダウンロード関数に分け、SMBサーバー接続関数実行後にサーバーファイルのパスをアンドロイド画面に表示し、次にファイルダウンロード関数を実行することで解決しました。  
非同期スレッドの作業状況をプログレスバーで表示するようなことはどうすればよいか未だわかりませんので、お教えくだされば助かります。  
  

[code]

    import android.os.Bundle
    import android.view.View
    import android.widget.TextView
    import androidx.appcompat.app.AppCompatActivity
    import jcifs.CIFSContext
    import jcifs.config.PropertyConfiguration
    import jcifs.context.BaseContext
    import jcifs.smb.NtlmPasswordAuthentication
    import jcifs.smb.SmbFile
    import kotlinx.coroutines.*
    import java.io.File
    import java.io.FileOutputStream
    import java.util.*
    import kotlin.coroutines.CoroutineContext
    
    
    class MainActivity : AppCompatActivity(), CoroutineScope {
        // 認証情報
        //////////////////////////////////////////////
        // Please replace these values to your data //
        val user = "USER"
        val password = "PASS"
        val domain = "192.168.1.1"
        val smbroot = "smb://" + domain + "/SMB/SERVER/FILE/"
        //////////////////////////////////////////////
    
        val TAG: String = "MySMB"
        var tmpStr: String = ""
    
        // coroutine準備
        private val job = Job()
        override val coroutineContext: CoroutineContext
            get() = Dispatchers.Main + job
    
        // 終了時のcoroutineのキャンセル設定
        override fun onDestroy() {
            job.cancel()
            super.onDestroy()
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            val tvInfo1 = findViewById<View>(R.id.txtVwInfo1) as TextView
            val tvInfo2 = findViewById<View>(R.id.txtVwInfo2) as TextView
            val tvInfo3 = findViewById<View>(R.id.txtVwInfo3) as TextView
    
            launch {
                // output start-message in display
                tvInfo1.setText("Start!")
    
                withContext(Dispatchers.IO) {
                    // connect SMB
                    val smb: SmbFile = connectSmb(user, password, domain, smbroot)
                    tmpStr = java.lang.String.format("Got SMB file: %s", smb.path)
                    tvInfo2.setText(tmpStr)
    
                    // download a file from SMB to android
                    val localFile: File? = cpSmbFile2android(smb)
                    if ( null == localFile ) {
                        tmpStr = java.lang.String.format("Can't download: %s", smb.path)
                        tvInfo3.setText(tmpStr)
                    } else {
                        tmpStr = java.lang.String.format("Downloaded %s. %sByte", smb.path, localFile.length())
                        tvInfo3.setText(tmpStr)
                    }
                }
            }
        }
    
        private suspend fun connectSmb(user: String, password: String, domain: String, smbroot: String): SmbFile {
            return withContext(Dispatchers.IO) {
                val prop = Properties()
                prop.setProperty("jcifs.smb.client.minVersion", "SMB202")
                prop.setProperty("jcifs.smb.client.maxVersion", "SMB300")
                val bc = BaseContext(PropertyConfiguration(prop))
                val creds = NtlmPasswordAuthentication(bc, domain, user, password)
                val auth: CIFSContext = bc.withCredentials(creds)
                SmbFile(smbroot, auth)
            }
        }
    
        /*
         * リモートのSMBファイルをandroid端末にコピーする
         * 　コピー先は、android端末の外部ストレージのアプリキャッシュ領域
         *  smbFile  コピー元リモートのSMBファイル
         *  返り値　コピー先ファイル
         */
        private fun cpSmbFile2android(smbFile: SmbFile): File? {
            if ( !smbFile.isFile ) {
                return null
            }
            val fileName = smbFile.name
            val exterCacheFile = File(this.externalCacheDir!!.path + "/" + fileName)
            // copy remote smb-file to local
            val inStream = smbFile.openInputStream()
            val fileOutStream = FileOutputStream(exterCacheFile)
            val buf = ByteArray(1024)
            var len: Int = 0
            while (true) {
                len = inStream.read(buf)
                if (len < 0) break
                fileOutStream.write(buf)
            }
            fileOutStream.flush();
            fileOutStream.close();
            inStream.close();
            return exterCacheFile
        }
    }
    
[/code]


CC BY-SA 4.0
2021-08-16T11:05:45.250
80845
2021-08-16T11:05:45.250
32105
80813
answer


自己レスです。  
lifecycleScopeを使って、非同期スレッドでの処理中にandroid端末画面にプログレスバーを表示することができました。  
次のコードです。

  

[code]

    import android.os.Bundle
    import android.view.View
    import android.widget.ProgressBar
    import android.widget.TextView
    import androidx.appcompat.app.AppCompatActivity
    import androidx.lifecycle.lifecycleScope
    import jcifs.CIFSContext
    import jcifs.config.PropertyConfiguration
    import jcifs.context.BaseContext
    import jcifs.smb.NtlmPasswordAuthentication
    import jcifs.smb.SmbFile
    import kotlinx.coroutines.*
    import java.io.File
    import java.io.FileOutputStream
    import java.util.*
    import kotlin.coroutines.CoroutineContext
    
    
    class MainActivity : AppCompatActivity(), CoroutineScope {
        // 認証情報
        //////////////////////////////////////////////
        // Please replace these values to your data //
        val user = ""
        val password = ""
        val domain = "192.168.1.1"
        val smbroot = "smb://" + domain + "/SMB/SERVER/FILE/"
        //////////////////////////////////////////////
    
        val TAG: String = "MySMB"
        var tmpStr: String = ""
    
        // coroutine準備
        private val job = Job()
        override val coroutineContext: CoroutineContext
            get() = Dispatchers.Main + job
    
        // 終了時のcoroutineのキャンセル設定
        override fun onDestroy() {
            job.cancel()
            super.onDestroy()
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            val tvInfo1 = findViewById<View>(R.id.txtVwInfo1) as TextView
            val tvInfo2 = findViewById<View>(R.id.txtVwInfo2) as TextView
            val tvInfo3 = findViewById<View>(R.id.txtVwInfo3) as TextView
            val prgrsBar = findViewById<View>(R.id.progressBar) as ProgressBar
    
            tvInfo1.setText("Start!")
    
            // Display prograssBar in the screen
            prgrsBar.visibility = ProgressBar.VISIBLE
    
            lifecycleScope.launch {
                val smb: SmbFile = connectSmb(user, password, domain, smbroot)
                tmpStr = java.lang.String.format("Got SMB file: %s", smb.path)
                tvInfo2.setText(tmpStr)
    
                // download a file from SMB to android
                val localFile: File = cpSmbFile2android(smb)
    
                // Turn off prograssBar in the screen
                prgrsBar.visibility = ProgressBar.INVISIBLE
    
                tmpStr = java.lang.String.format("Downloaded %s. %sByte", smb.path, localFile.length())
                tvInfo3.setText(tmpStr)
            }
        }
    
        private suspend fun connectSmb(user: String, password: String, domain: String, smbroot: String): SmbFile {
            return withContext(Dispatchers.IO) {
                val prop = Properties()
                prop.setProperty("jcifs.smb.client.minVersion", "SMB202")
                prop.setProperty("jcifs.smb.client.maxVersion", "SMB300")
    
                val smbConnection = runCatching {
                    val bc = BaseContext(PropertyConfiguration(prop))
                    val creds = NtlmPasswordAuthentication(bc, domain, user, password)
                    val auth: CIFSContext = bc.withCredentials(creds)
                    SmbFile(smbroot, auth)
                }
                smbConnection.getOrElse {
                    throw Exception("connectSmb failed.")
                }
    
                /* このコードでもOK
                return@withContext runCatching {
                    val bc = BaseContext(PropertyConfiguration(prop))
                    val creds = NtlmPasswordAuthentication(bc, domain, user, password)
                    val auth: CIFSContext = bc.withCredentials(creds)
                    SmbFile(smbroot, auth)
                }
                .getOrElse {
                    throw Exception("connectSmb failed.")
                }
                */
            }
        }
    
        /*
         * リモートのSMBファイルをandroid端末にコピーする
         * 　コピー先は、android端末の外部ストレージのアプリキャッシュ領域
         *  smbFile  コピー元リモートのSMBファイル
         *  返り値　コピー先ファイル
         */
        private suspend fun cpSmbFile2android(smbFile: SmbFile): File {
            return withContext(Dispatchers.IO) {
                if (!smbFile.isFile) {
                    null
                }
                val fileName = smbFile.name
                val exterCacheFile = File( this@MainActivity.externalCacheDir!!.path + "/" + fileName)
                // copy remote smb-file to local
                return@withContext runCatching {
                    val inStream = smbFile.openInputStream()
                    val fileOutStream = FileOutputStream(exterCacheFile)
                    val buf = ByteArray(1024)
                    var len: Int = 0
                    while (true) {
                        len = inStream.read(buf)
                        if (len < 0) break
                        fileOutStream.write(buf)
                    }
                    fileOutStream.flush();
                    fileOutStream.close();
                    inStream.close();
                    exterCacheFile
                }
                .getOrElse {
                    throw Exception("downloadSmb2Android failed.")
                }
            }
        }
    }
    
[/code]


CC BY-SA 4.0
2021-08-19T11:50:46.300
80919
2021-08-19T11:50:46.300
32105
80813
answer


自己レスです。  
進行状況を表示するプログレスバーの表示他をViewModel、LiveData、ViewModelScopeで行えましたので、下記に掲載します。  
  

MainActivity.kt

[code]

    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.widget.ProgressBar
    import androidx.lifecycle.Observer
    import androidx.lifecycle.ViewModelProvider
    import net.sytes.rokkosan.smbclient2.databinding.ActivityMainBinding
    
    class MainActivity : AppCompatActivity() {
    
        //////////////////////////////////////////////
        // Please replace these values to your data //
    
        val DOMAIN: String = "192.168.1.1"
        val SMBROOT: String = "/SMB/SERVER/FILE/"
        val USER: String = ""
        val PASSWORD: String = ""
    
        //////////////////////////////////////////////
    
        lateinit var androidPath: String
        lateinit var smbViewModel: SmbViewModel
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            val binding = ActivityMainBinding.inflate(layoutInflater)
            setContentView(binding.root)
    
            var isSmb: Boolean = false
            binding.progBarHori.max = 100
    
            // prepare for connecting SMB server
            var smbUrl = "smb://" + DOMAIN + SMBROOT
            if (smbUrl.takeLast(1) != "/") {
                smbUrl += "/"
                // smbUrl文字列の末尾はスラッシュ（/）にしておく
            }
    
            // get the cache directory in android device
            androidPath = externalCacheDir!!.absolutePath
    
            // get ViewModel for SmbConnection
            val smbViewModelFactory = SmbViewModelFactory(USER, PASSWORD, DOMAIN, smbUrl, androidPath)
            smbViewModel = ViewModelProvider(this, smbViewModelFactory).get(SmbViewModel::class.java)
    
            // Create observers which updates the UI.
            val pathObserver = Observer<String> { sPath ->
                // Update the UI
                binding.tvSmbPath.text = sPath
            }
            val sizeObserver = Observer<Long> { sSize ->
                // Update the UI
                binding.tvSmbSize.text = (sSize / 1000L).toString() + "KB"
            }
            val downloadedRatioObserver = Observer<Int> { dRatio ->
                // Update the UI
                binding.progBarHori.progress = dRatio
            }
            val isConnectedObserver = Observer<Boolean> { isConnected ->
                if (!isConnected) {
                    isSmb = false
                    binding.tvInfo01.setText(R.string.failedConnectSmb)
                } else {
                    isSmb = true
    
                    /*
                     * get data in SMB server
                     *     参考ページ　https://developer.android.com/topic/libraries/architecture/livedata?hl=ja
                     */
                    // Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
                    //   observe(Owner, ...)のOwnerは、Activityの時はthis、Fragmentの時はviewLifecycleOwnerとする
                    smbViewModel.smbPath.observe(this, pathObserver)
                    smbViewModel.smbSize.observe(this, sizeObserver)
    
                    // Display prograssBar in the screen
                    smbViewModel.downloadedRatio.observe(this, downloadedRatioObserver)
                    binding.progBarHori.visibility = ProgressBar.VISIBLE
                }
            }
    
            // connect SMB server and download a file to android device
            binding.tvInfo01.setText(R.string.wait)
            smbViewModel.downloadSmbFile()
    
            // proceed according to smbViewModel.isConnectedSmb
            smbViewModel.isConnectedSmb.observe(this,isConnectedObserver)
            if (!isSmb) {
                return
            }
        }
    }
    
[/code]

  
  

SmbViewModel.kt

[code]

    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.io.File
    import java.io.FileOutputStream
    import java.util.*
    
    class SmbViewModel(val user: String, val password: String, val domain: String, val smbUrl: String, val androidPath: String): ViewModel() {
        // LiveDataを設定する
        private val _smbPath: MutableLiveData<String> by lazy {
            MutableLiveData<String>()
        }
        val smbPath: LiveData<String>
            get() = _smbPath
    
        private val _smbSize: MutableLiveData<Long> by lazy {
            MutableLiveData<Long>()
        }
        val smbSize: LiveData<Long>
            get() = _smbSize
    
        private val _isConnectedSmb = MutableLiveData<Boolean>()
        val isConnectedSmb: LiveData<Boolean>
            get() = _isConnectedSmb
    
        private var _downloadedRatio: MutableLiveData<Int> = MutableLiveData<Int>().also {
                mutableLiveData -> mutableLiveData.value = -1
        }
        public val downloadedRatio: LiveData<Int> get() = _downloadedRatio
    
        // SMBサーバーへの接続、ファイルダウンロード
        fun downloadSmbFile() {
            viewModelScope.launch(Dispatchers.IO) {
                lateinit var smb: SmbFile
    
                // connect to SMB server
                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))
                    smb = SmbFile(smbUrl, auth)
                    if (!smb.exists()) {
                        _isConnectedSmb.postValue(false)
                        return@launch
                    }
                } catch (e: Exception) {
                    _isConnectedSmb.postValue(false)
                    return@launch
                }
    
                _isConnectedSmb.postValue(true)
    
                // LiveDataにSMBサーバーからのデータをセットする
                _smbPath.postValue(smb.path)
                _smbSize.postValue(smb.length())
    
                // download a file from SMB server to android device
                val fileName = smb.name
                val fileSize = smb.length()
                val exterCacheFile = File(androidPath + "/" + fileName)
                val inStream = smb.openInputStream()
                val fileOutStream = FileOutputStream(exterCacheFile)
                val buf = ByteArray(1024)
                var len = 0
                var downloadedSize = 0L
                while (true) {
                    len = inStream.read(buf)
                    if (len < 0) {
                        break
                    }
                    downloadedSize += len.toLong()
                    _downloadedRatio.postValue((downloadedSize * 100L / fileSize).toInt())
                    fileOutStream.write(buf)
                }
                fileOutStream.flush();
                fileOutStream.close();
                inStream.close()
            }
        }
    
        override fun onCleared() {
            super.onCleared()
            viewModelScope.cancel()
        }
    }
    
[/code]

  
  

SmbViewModelFactory.kt

[code]

    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.ViewModelProvider
    
    class SmbViewModelFactory(val user: String, val password: String, val domain: String, val smbUrl: String, val androidPath: String): ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return SmbViewModel(user, password, domain, smbUrl, androidPath) as T
        }
    }
    
[/code]


CC BY-SA 4.0
2021-09-05T05:51:12.873
82248
2021-09-05T05:51:12.873
32105
80813
answer