rokkonet

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

android開発 SMBサーバーの音声ファイルをダウンロードしてMediaPlayerで再生する

2021 Sep. 06.
2021 Aug. 12.

インストールしたandroid端末

android バージョン 10

コンパイル環境

compileSdkVersion 30
minSdkVersion 24
targetSdkVersion 30

概要

* SMBサーバーへの接続はメインスレッドでは禁止されており、非同期スレッドで行う必要がある
* SMBサーバーへの接続にはjcifs-ngを利用できる

 参考ページ https://rasumus.hatenablog.com/entry/2021/07/24/184813

* 非同期スレッドにはcoroutineを利用できる

 参考ページ android開発 coroutineによる非同期スレッド実行 - rokkonet

* INTERNET、ACCESS_NETWORK_STATE、ACCESS_WIFI_STATEの各パーミッションをAndroidManifest.xmlで許可設定する


プロジェクトのapp/libsに配置したjarライブラリ

jcifs-ng-2.1.6.jar
 Maven Repository: eu.agno3.jcifs » jcifs-ng » 2.1.6 のFilesのbundleをクリックしてダウンロード

bcprov-jdk15to18-1.69.jar
 Maven Repository: org.bouncycastle » bcprov-jdk15to18 » 1.69 のFilesのjarをクリックしてダウンロード

build.gradle(Module)

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

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        applicationId "MY.PACKAGE.PROJECT"
        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'
    }
}

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.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

    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.kotlinx:kotlinx-coroutines-core:1.5.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1'
}


AndroidManifest.xml

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

    <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.PlayAudioLan">
        <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

import android.media.MediaPlayer
import android.os.Bundle
import android.util.Log
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 domain: String = "192.168.1.1"
    val smbroot: String = "/PATH/OF/SOUND/FILE/IN/SMB/SERVER/"
        //  Example :  val smbroot: String = "/taro/sound/sample.mp3/"
    val user: String = "USER"
    val password: String = "PASSWORD"
    //////////////////////////////////////////////

    private val TAG: String = "MySMB"
    private val myPlayer: MediaPlayer = MediaPlayer()
    lateinit var externalCacheFile: File

    // coroutine準備
    private val job = Job()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun onDestroy() {
        // 終了時のMediaPlayerの破棄
        if (myPlayer != null) {
            myPlayer.reset()
            myPlayer.release()
        }

        // 終了時のcoroutineのキャンセル
        job.cancel()

        // ローカルキャッシュファイル削除
        if (externalCacheFile.exists()) {
            externalCacheFile.delete()
        }
        super.onDestroy()
    }

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

        myPlayer?.setOnCompletionListener { mp -> audioStop() }

        // smbアクセス処理はActivityとは別スレッドとする必要があるのでcoroutineを利用する
        launch {
            lateinit var smb: SmbFile
            val smbUrl = "smb://" + domain + smbroot

            // 非同期スレッド
            val deferred = async(Dispatchers.IO) {
                val prop = Properties()  // java.util.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)

                try {
                    smb = SmbFile(smbUrl, auth)

                    // smbファイルの確認
                    if (!smb.isFile){
                        Log.d(TAG,"Not file")
                        finishAndRemoveTask()
                    }
                    Log.d(TAG,smb.path)
                    Log.d(TAG,smb.length().toString() + "bytes")

                    // ローカルファイル設定(キャッシュファイルにする)
                    val indexDot = smb.name.lastIndexOf(".")
                    if (indexDot < 1) {
                        Log.d(TAG,"No file-extension")
                        finishAndRemoveTask()
                    }
                    val extStr = smb.name.substring(indexDot)
                    externalCacheFile = File(this@MainActivity.externalCacheDir, "tmpsound" + extStr)
                    Log.d(TAG, externalCacheFile.path)

                    // copy remote file to local
                    val inStream = smb.openInputStream()
                    val fileOutStream = FileOutputStream(externalCacheFile)
                    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();
                    Log.d(TAG, "Copied. " + externalCacheFile.path + " " + externalCacheFile.length().toString() + "bytes")

                }  catch( e: Exception){
                    e.printStackTrace()
                    Log.d(TAG, e.toString())
                } finally {
                    smb.close();
                }
            }
            withContext(Dispatchers.Main) {
                deferred.await()
                if (externalCacheFile.isFile) {
                    myPlayer.setDataSource(externalCacheFile.path)
                    myPlayer.prepare()
                    myPlayer.start()

                }
            }
        }
    }

    private fun audioStop(){
        myPlayer?.run {
            stop()
            reset()
            release()
            finishAndRemoveTask()
        }
    }
}