rokkonet

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

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