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