rokkonet

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

android開発 SMBサーバーの音声ファイルをインテントを使ってアプリでストリーミング再生

2021 Sep. 06.

概要

ViewModelを使ったワーカースレッド内でSMBサーバーに接続する。
SMBサーバーへの接続にはjcifs-ngを利用。
ViewModelのLiveData機能でSMBサーバーファイルのファイルパスをメインスレッドに渡す。
メインスレッド内で、SMBサーバーファイルのファイルパスをURIにパースして音声再生Intentに渡し、Intentを発出する。


インストールした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')
    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'


全体

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

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        applicationId "net.sytes.rokkosan.smbplayby3rdapp"
        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 "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'

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


MainActivity.kt

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import net.sytes.rokkosan.smbplayby3rdapp.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 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

        // prepare for connecting SMB server
        var smbUrl = "smb://" + DOMAIN + SMBROOT
        if (smbUrl.takeLast(1) != "/") {
            smbUrl += "/"
            // smbUrl文字列の末尾はスラッシュ(/)とする
        }

        // get ViewModel for SmbConnection
        val smbViewModelFactory = SmbViewModelFactory(USER, PASSWORD, DOMAIN, smbUrl)
        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

            // create and send Intent of audio-player
            try {
                val audioIntent = Intent()
                audioIntent.action = Intent.ACTION_VIEW
                audioIntent.setDataAndType(Uri.parse(sPath), "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())
            }
        }

        val sizeObserver = Observer<Long> { sSize ->
            // Update the UI
            binding.tvSmbSize.text = (sSize / 1000L).toString() + "KB"
        }

        // create Observer working in case of success to connect SMB server
        val isConnectedObserver = Observer<Boolean> { isConnected ->
            if (!isConnected) {
                isSmb = false
                binding.tvInfo01.setText(R.string.failedConnectSmb)
            } else {
                isSmb = true

                // get filepath and filesize in SMB server with LiveData
                //   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)
            }
        }

        // connect SMB server
        smbViewModel.connectSmbFile()

        // 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.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.util.*

class SmbViewModel(val user: String, val password: String, val domain: String, val smbUrl: 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

    // SMBサーバーへの接続
    fun connectSmbFile() {
        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())
        }
    }

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