rokkonet

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

android開発 TextView Do not concatenate text displayed with setText. Use resource string with placeholders.

2021 Sep. 11.

事象

TextViewへのsetText()の引数に、変数・式といったリテラル以外を含む式を入れると"Do not concatenate text displayed with setText. Use resource string with placeholders."と警告される。

対応

コードではなく、Stringリソース(R.stringのstring.xml)に文字列フォーマットを利用して記述する。

参考ページ
【5分で理解】JavaのString.formatの使い方まとめ | 侍エンジニアブログ

コードでファイルサイズを取得し、KB単位でTextViewに表示する例

string.xml
 <string name="kb">%sKB</string>
  %sがコードのgetString()の第2引数に置き換わる。


MainActivity.ktでのコード
val myfileSizeInByte: Long = myFile.length()
textViewFileSize.setText(getString( R.string.kb, (myfileSizeInByte / 1000L).toString()))

android開発 kotlin コルーチン内でのThread.sleep( )はコルーチン内でsleep( )を実行したスレッドを指定時間停止する

2021 Sep. 09.

下記コードではワーキングスレッド(Dispatchers.Default)が5秒間停止する。

viewModelScope.launch {
    withContext(Dispatchers.Default) {
        Thread.sleep(5000L)
    }
}

参考ページ Kotlin Coroutine 入門1: 起動と suspend - Qiita

kotlin 文字列の一番後ろのピリオド以降に指定した正規表現文字列が含まれていればtrueを返す関数

2021 Sep. 06.

    /*
     * 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開発 ViewModel、LiveData、jcifs-ngを使ったSMBサーバーからのファイルダウンロード、進捗プログレスバー表示

2022 Nov. 23.
2021 Sep. 05.
2021 Sep. 04.

インストールしたandroid端末

android バージョン 10

コンパイル環境

compileSdkVersion 30
minSdkVersion 24
targetSdkVersion 30


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

jcifs-ng-2.1.6.jar

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

bcprov-jdk15to18-1.69.jar

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

build.gradle(Module:app)

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

    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-alpha03"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0-alpha03"
    implementation files('libs/jcifs-ng-2.1.6.jar')  <- app/libsに配置したバージョンに合わせる
    implementation group: 'org.bouncycastle', name: 'bcprov-jdk15to18', version: '1.69'  <- app/libsに配置したバージョンに合わせる
    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 {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        applicationId "net.sytes.rokkosan.smbclient2"
        minSdkVersion 24
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

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

dependencies {
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-alpha03"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0-alpha03"
    implementation files('libs/jcifs-ng-2.1.6.jar')
    implementation group: 'org.bouncycastle', name: 'bcprov-jdk15to18', version: '1.69'
    implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32'
    implementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.32'
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.6.0'
    implementation 'androidx.appcompat:appcompat:1.3.1'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}


権限許可

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


AndroidManifest.xml

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

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

    <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.SmbClient2">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>


MainActivity.kt

// 2021 Sep. 05.
// 2021 Aug. 29.

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)

                // Observerインスタンスの生成はせず、下記のようにまてめてもよい
                /*
                smbViewModel.smbPath.observe(this, Observer { sPath -> binding.tvSmbPath.setText(sPath) })
                smbViewModel.smbSize.observe(this, Observer { sSize -> binding.tvSmbSize.setText(sSize.toString()) })
                */

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


SmbViewModel.kt

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

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

    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サーバーからのデータをセットする
            //   ワーカースレッド空間のデータをLiveDataにセットするにはpostValue()を使う
            _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()

            smb.close()
        }
    }

    override fun onCleared() {
        super.onCleared()
        viewModelScope.cancel()
    }
}


SmbViewModelFactory.kt

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

android開発 LiveDataの利用パターン

2022 Jul. 23.
2021 Sep. 03.

ViewModelクラス内でLiveDataを設定する

 各LiveData対象について、書き換え可能なMutableLiveDataと読み込み専用のLiveDataの2つの変数を作る。

  private val _FOO: MutableLiveData by lazy { MutableLiveData() }
  val FOO: LiveData get() = _FOO

 値のセットは、MutableLiveData#postValue(HOGE)を使う。


MainActivity内で読み込み専用のLiveDataを読み込む

 Observerを作る。
 LiveData#observe()で読み込む。


サンプル
SmbViewModel.kt

class SmbViewModel(): ViewModel() {

    // LiveDataを設定する
    private val _smbPath: MutableLiveData<String> by lazy {
        // MutableLiveData<T>は、valでmutableな変数を宣言できる
        MutableLiveData<String>()
    }
    val smbPath: LiveData<String> get() = _smbPath
        // smbPathの読み込み時は _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

    // SMBサーバーへの接続
    fun connectSmb() {
        viewModelScope.launch(Dispatchers.IO) {
            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))
            val smb = SmbFile(smbUrl, auth)
            if (smb.exists()) {
                // LiveDataにSMBサーバーからのデータをセットする
                //   ワーカースレッド空間のデータをLiveDataにセットするにはpostValue()を使う
                _smbPath.postValue(smb.path)
                _smbSize.postValue(smb.length())
                _isConnectedSmb.postValue(true)
            } else {
                _isConnectedSmb.postValue(false)
            }
        }
    }
}


MainActivity.kt

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

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

        // get ViewModel for SmbConnection
        val smbViewModelFactory = SmbViewModelFactory(USER, PASSWORD, DOMAIN, smbUrl)
        val smbViewModel: SmbViewModel = ViewModelProvider(this, smbViewModelFactory).get(SmbViewModel::class.java)

        // connect SMB server
        smbViewModel.connectSmb()

        // check if SMB-connection succeeded
        val isConnectedObserver = Observer<Boolean> { isConnected ->
            if (!isConnected) {
                finishAndRemoveTask()
            }
        }
        smbViewModel.isConnectedSmb.observe(this,isConnectedObserver)

        /*
         * get data in SMB server
         *     参考ページ https://developer.android.com/topic/libraries/architecture/livedata?hl=ja
         */
        // Create the observer 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.toString()
        }

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

        // 上記のようにObserverインスタンスの生成はせず、下記のようにまてめてもよい
        /*
        smbViewModel._smbPath.observe(this, Observer { sPath -> binding.tvSmbPath.setText(sPath) })
        smbViewModel._smbSize.observe(this, Observer { sSize -> binding.tvSmbSize.setText(sSize.toString()) })
        */
    }
}

android開発 ViewModel内のコルーチン内の結果でActivityの動作を決めるには。Boolean型suspend関数の結果をActivityに渡す方法

2021 Sep. 05.
2021 Sep. 03.

参考ページ android ViewModelScopeコルーチンでメインスレッドを停止する方法は? - スタック・オーバーフロー

コルーチンのlaunchブロック内のデータをlauchブロック外に渡すことはできない。 コルーチン内の結果で返り値を決めるBoolean型関数は作れない。

次のコードは不可。launch{}の終了を待たず"return retValue"が返される。

fun foo(): Boolean {
    lateinit val retValue: Boolean
    launch(Dispatchers..Default)  {
        retValue = BAR
    }
    return retValue
}


ViewModel内のコルーチン内の結果でActivityの動作を決める場合は、コルーチン内の結果をLiveDataとし、そのLiveDataをActivity内で受け取ってActivityの動作を決める。
下記コードでは、PassResultViewModel内のworkLongTime()メソッドの真偽をLiveDataのisFinishSuccessに格納し、それをMainActivityで読み込み、isFinishWorkに格納している。

MainActivity.kt

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider

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

        val textViewOutput = findViewById<TextView>(R.id.textViewOutput)

        var isFinishWork: Boolean = false

        // get ViewModel
        val smbViewModelFactory = PassResultViewModelFactory()
        val passResultViewModel: PassResultViewModel = ViewModelProvider(this, smbViewModelFactory).get(PassResultViewModel::class.java)

        // Create Observer
        val isFinishSuccessObserver = Observer<Boolean> { isFinish ->
            isFinishWork = isFinish
            if (isFinishWork) {
                textViewOutput.setText(R.string._true)
            } else {
                textViewOutput.setText(R.string._false)
            }
        }

        // work in ViewModel
        passResultViewModel.startWork()

        // set result of method in ViewModel to variable
          // passResultViewModel.workLongTime(): Booleanの結果が、workLongTime()を呼び出したpassResultViewModel.startWork()の実行後にisFinishWorkに格納される
        passResultViewModel.isFinishSuccess.observe(this, isFinishSuccessObserver)
    }
}


PassResultViewModel.kt

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*

class PassResultViewModel: ViewModel() {

    // LiveDataを設定する
    private var _isFinishSuccess = MutableLiveData<Boolean>(false)
    val isFinishSuccess: LiveData<Boolean>
        get() = _isFinishSuccess

    fun startWork() {
        viewModelScope.launch(Dispatchers.Default) {
            _isFinishSuccess.postValue(workLongTime())
        }
    }

    suspend fun workLongTime(): Boolean {
        delay(10000L)
        return true
    }

    override fun onCleared() {
        super.onCleared()
        viewModelScope.cancel()
    }
}


PassResultViewModelFactory.kt

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

class PassResultViewModelFactory: ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return PassResultViewModel() as T
    }
}

jcifs-ngによるSMBサーバー接続認証

2021 Sep. 03.
2021 Aug. 31.

Up-to-date

                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))
                    // Example of domain:  "192.168.1.1"
                val smb = SmbFile(smbUrl, auth)
                    // smbUrl:  smb://DOMAIN/SMB/SERVER/PATH/


Deprecated

                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 creds = NtlmPasswordAuthentication(baseCxt, domain, user, password)
                val auth: CIFSContext = bc.withCredentials(creds)
                val smb = SmbFile(smbUrl, auth)  // smbUrl: smb://DOMAIN/SMB/SERVER/PATH/