2022 Jul. 18.
2022 Feb. 13.
2021 Dec. 05.
2021 Nov. 14.
2021 Sep. 16.
ソース保管場所
https://bitbucket.org/arsmus/randomsmbsoundplay/src/master/
概要
SMBサーバーからランダムに1つの音声ファイルをストリーミング再生する。
SMBサーバーにつながらなかったら、android端末の外部共有ストレージからランダムに1つの音声ファイルをストリーミング再生する。
再生にはサードパーティ再生アプリをIntent起動する。
ViewModelを使ったワーカースレッド内でSMBサーバーに接続する。
SMBサーバーへの接続にはjcifs-ngを利用。
ViewModelのLiveData機能でSMBサーバーファイルのファイルパスをメインスレッドに渡す。
メインスレッド内で、SMBサーバーファイルのファイルパスをURIにパースして音声再生Intentに渡し、Intentを発出する。
ContentResolverで得たURIをそのままIntentに与えても動作しない。パスを取得し、そのパスをURIにパースする必要がある。パスは _dataカラムから取得できる。
android バージョン 7
android バージョン 11
compileSdkVersion 31
minSdkVersion 24
targetSdkVersion 31
android studioのプロジェクトのapp/libsにjcifs-ng, bcprovライブラリを配置
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.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
implementation files('libs/jcifs-ng-2.1.7.jar')
implementation group: 'org.bouncycastle', name: 'bcprov-jdk15to18', version: '1.70'
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 {
compileSdk 31
defaultConfig {
applicationId "YOURPROJECT"
minSdk 24
targetSdk 31
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.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
testImplementation 'junit:junit:4.13.2'
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.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
implementation files('libs/jcifs-ng-2.1.7.jar')
implementation group: 'org.bouncycastle', name: 'bcprov-jdk15to18', version: '1.70'
implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32'
implementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.32'
implementation "androidx.preference:preference-ktx:1.2.0"
}
AndroidManifest.xml
xml version="1.0" encoding="utf-8"
<manifest xmlnsandroid="http://schemas.android.com/apk/res/android"
package="YOURPROJECT">
<uses-permission androidname="android.permission.INTERNET"/>
<uses-permission androidname="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission androidname="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission androidname="android.permission.READ_EXTERNAL_STORAGE" />
<application
androidallowBackup="true"
androidicon="@mipmap/ic_launcher"
androidlabel="@string/app_name"
androidroundIcon="@mipmap/ic_launcher_round"
androidsupportsRtl="true"
androidtheme="@style/Theme.RandomSmbSoundPlay">
<activity
androidname=".MainActivity"
androidexported="true">
<intent-filter>
<action androidname="android.intent.action.MAIN" />
<category androidname="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
activity_main.xml
xml version="1.0" encoding="utf-8"
<androidxconstraintlayoutwidgetConstraintLayout xmlnsandroid="http://schemas.android.com/apk/res/android"
xmlnsapp="http://schemas.android.com/apk/res-auto"
xmlnstools="http://schemas.android.com/tools"
androidlayout_width="match_parent"
androidlayout_height="match_parent"
toolscontext=".MainActivity">
<TextView
androidid="@+id/tvInfo01"
androidlayout_width="match_parent"
androidlayout_height="wrap_content"
applayout_constraintLeft_toLeftOf="parent"
applayout_constraintRight_toRightOf="parent"
applayout_constraintTop_toTopOf="parent"
applayout_constraintBottom_toTopOf="@+id/tvInfo02"
/>
<TextView
androidid="@+id/tvInfo02"
androidlayout_width="match_parent"
androidlayout_height="wrap_content"
applayout_constraintLeft_toLeftOf="parent"
applayout_constraintRight_toRightOf="parent"
applayout_constraintTop_toBottomOf="@+id/tvInfo01"
applayout_constraintBottom_toTopOf="@+id/tvFilePath"
/>
<TextView
androidid="@+id/tvFilePath"
androidlayout_width="match_parent"
androidlayout_height="wrap_content"
applayout_constraintLeft_toLeftOf="parent"
applayout_constraintRight_toRightOf="parent"
applayout_constraintTop_toBottomOf="@+id/tvInfo02"
applayout_constraintBottom_toTopOf="@+id/tvFileSize"
/>
<TextView
androidid="@+id/tvFileSize"
androidlayout_width="wrap_content"
androidlayout_height="wrap_content"
applayout_constraintLeft_toLeftOf="parent"
applayout_constraintRight_toLeftOf="@id/btnQuit"
applayout_constraintTop_toBottomOf="@+id/tvFilePath"
applayout_constraintBottom_toTopOf="@id/btnPlay"
/>
<Button
androidid="@+id/btnQuit"
androidlayout_width="wrap_content"
androidlayout_height="wrap_content"
androidtext="@string/quit"
applayout_constraintLeft_toLeftOf="@id/btnReset"
applayout_constraintRight_toRightOf="@id/btnReset"
applayout_constraintTop_toBottomOf="@id/tvFilePath"
applayout_constraintBottom_toTopOf="@id/btnReset"
applayout_constraintBaseline_toBaselineOf="@id/tvFileSize"
/>
<Button
androidid="@+id/btnPlay"
androidlayout_width="wrap_content"
androidlayout_height="wrap_content"
androidtext="@string/play"
applayout_constraintStart_toStartOf="parent"
applayout_constraintEnd_toStartOf="@+id/btnReplay"
applayout_constraintTop_toBottomOf="@+id/tvFileSize"
applayout_constraintBottom_toBottomOf="parent"
/>
<Button
androidid="@+id/btnReplay"
androidtext="@string/replay"
androidlayout_width="wrap_content"
androidlayout_height="wrap_content"
applayout_constraintStart_toEndOf="@+id/btnPlay"
applayout_constraintEnd_toStartOf="@+id/btnReset"
applayout_constraintTop_toBottomOf="@+id/tvFileSize"
applayout_constraintBottom_toBottomOf="parent"
applayout_constraintBaseline_toBaselineOf="@+id/btnPlay"
/>
<Button
androidid="@+id/btnReset"
androidlayout_width="wrap_content"
androidlayout_height="wrap_content"
androidtext="@string/reset"
applayout_constraintStart_toEndOf="@+id/btnReplay"
applayout_constraintEnd_toEndOf="parent"
applayout_constraintTop_toBottomOf="@+id/tvFileSize"
applayout_constraintBottom_toBottomOf="parent"
applayout_constraintBaseline_toBaselineOf="@id/btnReplay"
/>
</androidxconstraintlayoutwidgetConstraintLayout>
dialog_smb_server_setting.xml
xml version="1.0" encoding="utf-8"
<androidxconstraintlayoutwidgetConstraintLayout xmlnsandroid="http://schemas.android.com/apk/res/android"
xmlnsapp="http://schemas.android.com/apk/res-auto"
androidlayout_width="match_parent"
androidlayout_height="match_parent">
<EditText
androidid="@+id/etDomain"
androidlayout_width="match_parent"
androidlayout_height="wrap_content"
androidhint="@string/hintDomain"
androidautofillHints="@string/hintDomain"
androidinputType="text"
applayout_constraintBottom_toTopOf="@id/etSmbPath"
applayout_constraintEnd_toEndOf="parent"
applayout_constraintStart_toStartOf="parent"
applayout_constraintTop_toTopOf="parent" />
<EditText
androidid="@+id/etSmbPath"
androidlayout_width="match_parent"
androidlayout_height="wrap_content"
androidhint="@string/hintSmbRootPath"
androidautofillHints="@string/hintSmbRootPath"
androidinputType="textUri"
applayout_constraintBottom_toTopOf="@+id/etSmbUser"
applayout_constraintEnd_toEndOf="parent"
applayout_constraintStart_toStartOf="parent"
applayout_constraintTop_toBottomOf="@+id/etDomain" />
<EditText
androidid="@+id/etSmbUser"
androidlayout_width="match_parent"
androidlayout_height="wrap_content"
androidautofillHints="@string/hintUserName"
androidhint="@string/hintUserName"
androidinputType="textUri"
applayout_constraintBottom_toTopOf="@+id/etSmbPassword"
applayout_constraintEnd_toEndOf="parent"
applayout_constraintStart_toStartOf="parent"
applayout_constraintTop_toBottomOf="@+id/etSmbPath" />
<EditText
androidid="@+id/etSmbPassword"
androidlayout_width="match_parent"
androidlayout_height="wrap_content"
androidautofillHints="@string/hintPassword"
androidhint="@string/hintPassword"
androidinputType="textPassword"
applayout_constraintBottom_toBottomOf="parent"
applayout_constraintEnd_toEndOf="parent"
applayout_constraintStart_toStartOf="parent"
applayout_constraintTop_toBottomOf="@+id/etSmbUser" />
</androidxconstraintlayoutwidgetConstraintLayout>
played_files_list.xml
xml version="1.0" encoding="utf-8"
<androidxconstraintlayoutwidgetConstraintLayout xmlnsandroid="http://schemas.android.com/apk/res/android"
androidlayout_width="match_parent"
androidlayout_height="match_parent">
<ListView
androidid="@+id/listViewFiles"
androidlayout_width="match_parent"
androidlayout_height="match_parent" />
</androidxconstraintlayoutwidgetConstraintLayout>
optionsmenu.xml
xml version="1.0" encoding="utf-8"
<menu xmlnsandroid="http://schemas.android.com/apk/res/android">
<item
androidid="@+id/itemSmbServer"
androidtitle="@string/menuItemServer" />
<item
androidid="@+id/itemPlayedFileHistory"
androidtitle="@string/menuItemPlayedHistory" />
</menu>
strings.xml
<resources>
<string name="app_name">RandomSmbSoundPlay</string>
<string name="kb">%sKB</string>
<string name="sentIntent">Sent Intent for playing audio</string>
<string name="waitMinutes">Wait minutes to connect SMB server</string>
<string name="hintPath">Filepath</string>
<string name="hintSize">Filesize</string>
<string name="play">Play</string>
<string name="replay">Replay</string>
<string name="reset">Reset</string>
<string name="quit">Quit</string>
<string name="tryPlayAndroidFile">Trying to play a file in local device</string>
<string name="notConnectedSmb">Not connected to SMB server</string>
<string name="fromNowTryConnectSmb">Connect to SMB server from now</string>
<string name="tryConnectingSmb">Trying to connect to SMB server</string>
<string name="connectedSmb">Connected to SMB server</string>
<string name="failedConnectSmb">Failed to connect to SMB server</string>
<string name="notGetAudioFile">Not got an audio file</string>
<string name="fromNowTryGetAudioFile">Get an audio file from now</string>
<string name="tryGetAudioFile">Trying to get an audio file</string>
<string name="gotAudioFile">Got an audio file</string>
<string name="failedGetAudioFile">Failed to get an audio file</string>
<string name="notGetSmbAudioFile">Not got a smb audio file</string>
<string name="fromNowTryGetSmbAudioFile">Get a smb audio file from now</string>
<string name="tryGetSmbAudioFile">Trying to get a smb audio file</string>
<string name="gotSmbAudioFile">Got a smb audio file</string>
<string name="failedGetSmbAudioFile">Failed to get a smb audio file</string>
<string name="gotLocalAudioFile">Got an audio file from local device</string>
<string name="server">Server</string>
<string name="domain">Domain</string>
<string name="smbPath">SMB_Path</string>
<string name="userName">UserName</string>
<string name="password">Password</string>
<string name="hintDomain">192.168.1.1</string>
<string name="hintSmbRootPath">/SMB/PATH/</string>
<string name="hintUserName">user</string>
<string name="hintPassword">password</string>
<string name="noNetwork">No network</string>
<string name="menuItemServer">Server Setting</string>
<string name="menuItemPlayedHistory">Play History</string>
<string name="pathHistoryFile">path_history.txt</string>
<string name="noHistory">No history</string>
<string name="noPermission">No permission is granted.</string>
</resources>
MainActivity.kt
import android.Manifest
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import net.sytes.rokkosan.randomsmbsoundplay.databinding.ActivityMainBinding
import java.io.*
import java.util.*
import java.util.concurrent.TimeUnit
class MainActivity : AppCompatActivity(),SmbServerSettingDialogFragment.DialogListener {
private var smbDomain: String = "192.168.1.1"
private var smbRoot: String = "/"
private var smbUser: String = ""
private var smbPassword: String = ""
private var smbFilepath: String = ""
private lateinit var binding: ActivityMainBinding
private lateinit var audioUri: Uri
val defaultSharedPreferences by lazy {PreferenceManager.getDefaultSharedPreferences(getApplicationContext())}
lateinit var smbViewModel: SmbViewModel
lateinit var smbViewModelFactory: SmbViewModelFactory
lateinit var smbUrl: String
private val PERMISSION_READ_EX_STOR: Int = 100
lateinit var appContext: Context
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
val inflater = menuInflater
inflater.inflate(R.menu.optionsmenu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
R.id.itemSmbServer -> {
val args = Bundle()
args.putString("smbDomain", smbDomain )
args.putString("smbRootPath", smbRoot )
args.putString("smbUser", smbUser )
args.putString("smbPassword", smbPassword )
val smbSettingdialog = SmbServerSettingDialogFragment()
smbSettingdialog.setArguments(args)
smbSettingdialog.show(supportFragmentManager, "simple")
}
R.id.itemPlayedFileHistory -> {
val playedFilesDialog = ListPlayedFilesDialogFragment()
playedFilesDialog.show(supportFragmentManager, "simple")
}
}
return super.onOptionsItemSelected(item)
}
override fun onDialogMapReceive(dialog: DialogFragment, smbSettings: MutableMap<String, String>) {
with ( defaultSharedPreferences.edit()) {
putString("smbDomain", smbSettings["smbDomain"])
putString("smbPath", smbSettings["smbPath"])
putString("smbUser", smbSettings["smbUser"])
putString("smbPassword", smbSettings["smbPassword"])
apply()
}
smbDomain = smbSettings["smbDomain"] ?: ""
smbRoot = smbSettings["smbPath"] ?: ""
smbUser = smbSettings["smbUser"] ?: ""
smbPassword = smbSettings["smbPassword"] ?: ""
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
appContext = applicationContext
with (defaultSharedPreferences) {
smbDomain = getString("smbDomain", "") ?: ""
smbRoot = getString("smbPath", "") ?: ""
smbUser = getString("smbUser", "") ?: ""
smbPassword = getString("smbPassword", "") ?: ""
}
if ( smbDomain.length < 1 ||
smbRoot.length < 1 ||
smbUser.length < 1 ||
smbPassword.length < 1 ) {
val args = Bundle()
with(args) {
putString("smbDomain", smbDomain )
putString("smbRootPath", smbRoot )
putString("smbUser", smbUser )
putString("smbPassword", smbPassword )
}
val smbSettingdialog = SmbServerSettingDialogFragment()
smbSettingdialog.setArguments(args)
smbSettingdialog.show(supportFragmentManager, "simple")
}
if (binding.btnPlay.isEnabled) {
binding.btnReplay.isEnabled = false
binding.btnReset.isEnabled = true
binding.btnQuit.isEnabled = true
} else {
binding.btnReplay.isEnabled = true
binding.btnReset.isEnabled = true
binding.btnQuit.isEnabled = true
}
binding.btnPlay.setOnClickListener {
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork)
if (capabilities == null) {
binding.tvInfo01.setText(R.string.noNetwork)
playAndroidExternalAudio()
} else {
binding.tvInfo01.setText(R.string.waitMinutes)
binding.btnPlay.isEnabled = false
binding.btnReplay.isEnabled = true
binding.btnReset.isEnabled = true
binding.btnQuit.isEnabled = true
smbUrl = "smb://" + smbDomain + smbRoot
if (smbUrl.takeLast(1) != "/") {
smbUrl += "/"
}
smbViewModelFactory = SmbViewModelFactory(smbUser, smbPassword, smbDomain, smbUrl)
smbViewModel =
ViewModelProvider(this, smbViewModelFactory).get(SmbViewModel::class.java)
val pathObserver = Observer<String> { sPath ->
smbFilepath = sPath
binding.tvFilePath.text = sPath
binding.btnReplay.isEnabled = true
binding.btnReset.isEnabled = true
}
val sizeObserver = Observer<Long> { sSize ->
binding.tvFileSize.setText(getString(R.string.kb, (sSize / 1000L).toString()))
}
val connectStatusObserver = Observer<SmbViewModel.ProcessStatus> { status ->
when (status!!) {
SmbViewModel.ProcessStatus.NotYetGot -> binding.tvInfo01.setText(R.string.notConnectedSmb)
SmbViewModel.ProcessStatus.FromNow -> binding.tvInfo01.setText(R.string.fromNowTryConnectSmb)
SmbViewModel.ProcessStatus.Trying -> binding.tvInfo01.setText(R.string.tryConnectingSmb)
SmbViewModel.ProcessStatus.Got -> binding.tvInfo01.setText(R.string.connectedSmb)
SmbViewModel.ProcessStatus.Failed -> {
binding.tvInfo01.setText(R.string.failedConnectSmb)
binding.tvInfo02.setText(R.string.tryGetAudioFile)
playAndroidExternalAudio()
binding.btnReplay.isEnabled = true
binding.btnReset.isEnabled = true
}
}
}
val getSmbAudioFileStatusObserver = Observer<SmbViewModel.ProcessStatus> { status ->
when (status!!) {
SmbViewModel.ProcessStatus.NotYetGot -> binding.tvInfo02.setText(R.string.notGetSmbAudioFile)
SmbViewModel.ProcessStatus.FromNow -> binding.tvInfo02.setText(R.string.fromNowTryGetSmbAudioFile)
SmbViewModel.ProcessStatus.Trying -> binding.tvInfo02.setText(R.string.tryGetSmbAudioFile)
SmbViewModel.ProcessStatus.Got -> {
binding.tvInfo02.setText(R.string.gotSmbAudioFile)
try {
val audioIntent = Intent()
audioIntent.action = Intent.ACTION_VIEW
audioIntent.setDataAndType(Uri.parse(smbFilepath), "audio/*")
startActivity(audioIntent)
binding.tvInfo01.setText(R.string.sentIntent)
lateinit var fw: FileWriter
val savePathFile =
File(appContext.filesDir, getString(R.string.pathHistoryFile))
if (savePathFile.exists()) {
fw = FileWriter(savePathFile, true)
} else {
fw = FileWriter(savePathFile)
}
try {
fw.write("$smbFilepath,");
} catch(e: IOException) {
binding.tvInfo01.setText(e.toString())
} finally {
try {
fw.close()
} catch(e: IOException) {
binding.tvInfo01.setText(e.toString())
}
}
} catch (e: Exception) {
binding.tvInfo01.setText(e.toString())
}
}
SmbViewModel.ProcessStatus.Failed -> {
binding.tvInfo02.setText(R.string.failedGetSmbAudioFile)
binding.tvInfo01.setText(R.string.failedConnectSmb)
binding.tvInfo02.setText(R.string.tryPlayAndroidFile)
playAndroidExternalAudio()
}
}
}
smbViewModel.connectSmbSelectAudioFile()
smbViewModel.smbConnectionStatus.observe(this, connectStatusObserver)
smbViewModel.smbPath.observe(this, pathObserver)
smbViewModel.smbSize.observe(this, sizeObserver)
smbViewModel.getAudioFileStatus.observe(this, getSmbAudioFileStatusObserver)
}
}
binding.btnReplay.setOnClickListener {
binding.btnPlay.isEnabled = false
binding.btnReplay.isEnabled = true
binding.btnReset.isEnabled = true
binding.btnQuit.isEnabled = true
if ( "smb" == binding.tvFilePath.text.toString().substring(0, 3)) {
try {
val audioIntent = Intent()
audioIntent.action = Intent.ACTION_VIEW
audioIntent.setDataAndType(Uri.parse(smbFilepath), "audio/*")
startActivity(audioIntent)
binding.tvInfo01.setText(R.string.sentIntent)
} catch (e: Exception) {
binding.tvInfo01.setText(e.toString())
}
} else {
try {
val audioIntent = Intent()
audioIntent.action = Intent.ACTION_VIEW
audioIntent.setDataAndType(audioUri, "audio/*")
startActivity(audioIntent)
binding.tvInfo02.setText(R.string.sentIntent)
} catch (e: Exception) {
binding.tvInfo02.setText(e.toString())
}
}
}
binding.btnReset.setOnClickListener {
binding.btnPlay.isEnabled = true
binding.btnReplay.isEnabled = false
binding.btnReset.isEnabled = true
binding.btnQuit.isEnabled = true
try { smbViewModel.smb.close() } catch (e: Exception) { }
val launchIntent = baseContext.packageManager
.getLaunchIntentForPackage(baseContext.packageName)
launchIntent!!.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
finish()
startActivity(launchIntent)
}
binding.btnQuit.setOnClickListener {
try { smbViewModel.smb.close()
} catch (e: Exception) { }
finishAndRemoveTask()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val info01Text: String = binding.tvInfo01.text.toString()
outState.putString("INFO01", info01Text)
val info02Text: String = binding.tvInfo02.text.toString()
outState.putString("INFO02", info02Text)
val pathText: String = binding.tvFilePath.text.toString()
outState.putString("PATH", pathText )
val sizeText: String = binding.tvFileSize.text.toString()
outState.putString("SIZE", sizeText )
outState.putBoolean("btnPlay", binding.btnPlay.isEnabled)
outState.putBoolean("btnReplay", binding.btnReplay.isEnabled)
outState.putBoolean("btnReset", binding.btnReset.isEnabled)
outState.putBoolean("btnQuit", binding.btnQuit.isEnabled)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
val info01Text: String = savedInstanceState.getString("INFO01") ?: ""
binding.tvInfo01.setText(info01Text)
val info02Text: String = savedInstanceState.getString("INFO02") ?: ""
binding.tvInfo02.setText(info02Text)
val pathText: String = savedInstanceState.getString("PATH") ?: ""
binding.tvFilePath.setText(pathText)
smbFilepath = pathText
val sizeText: String = savedInstanceState.getString("SIZE") ?: ""
binding.tvFileSize.setText(sizeText)
binding.btnPlay.isEnabled = savedInstanceState.getBoolean("btnPlay")
binding.btnReplay.isEnabled = savedInstanceState.getBoolean("btnReplay")
binding.btnReset.isEnabled = savedInstanceState.getBoolean("btnReset")
binding.btnQuit.isEnabled = savedInstanceState.getBoolean("btnQuit")
}
private fun playAndroidExternalAudio() {
if ( isGrantedReadStorage()) {
getPlayStorageMedia()
} else {
requestPermissions(
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
PERMISSION_READ_EX_STOR
)
}
}
fun isGrantedReadStorage(): Boolean {
return (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED)
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
when (requestCode) {
PERMISSION_READ_EX_STOR -> {
if (grantResults.isEmpty()) {
throw RuntimeException("Empty permission result")
}
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getPlayStorageMedia()
} else {
if (shouldShowRequestPermissionRationale(
Manifest.permission.READ_EXTERNAL_STORAGE)) {
requestPermissions( arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSION_READ_EX_STOR)
} else {
Toast.makeText(applicationContext, R.string.noPermission, Toast.LENGTH_LONG).show()
}
}
}
}
}
fun getPlayStorageMedia() {
var pathOfUri: String= ""
val collection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Audio.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL
)
} else {
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
val columns = arrayOf(
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.SIZE,
"_data"
)
val selection = "${MediaStore.Audio.Media.DURATION} >= ?"
val selectionArgs = arrayOf(
TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES).toString()
)
val resolver = applicationContext.contentResolver
val query = resolver.query(
collection,
columns,
selection,
selectionArgs,
null
)
val numCount = query?.count
query?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
val displayNameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
val column_index = cursor.getColumnIndexOrThrow("_data")
cursor.moveToPosition(getRandomNum(numCount!!))
val id = cursor.getLong(idColumn)
val displayName = cursor.getString(displayNameColumn)
val mediaSize = cursor.getInt(sizeColumn)
val contentUri: Uri = ContentUris.withAppendedId(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,id)
pathOfUri = cursor.getString(column_index)
binding.tvInfo02.setText(R.string.gotLocalAudioFile)
binding.tvFilePath.setText(pathOfUri)
val sizeKb = (mediaSize / 1000 ).toString() + "KB"
binding.tvFileSize.setText(sizeKb)
val audioIntent = Intent()
audioIntent.action = Intent.ACTION_VIEW
audioIntent.setDataAndType(Uri.parse(pathOfUri), "audio/*")
audioIntent.setPackage("org.videolan.vlc")
startActivity(audioIntent)
binding.tvInfo02.setText(R.string.sentIntent)
lateinit var fw: FileWriter
val savePathFile =
File(appContext.filesDir, getString(R.string.pathHistoryFile))
fw = if (savePathFile.exists()) {
FileWriter(savePathFile, true)
} else {
FileWriter(savePathFile)
}
try {
fw.write("$pathOfUri,");
} catch(e: IOException) {
binding.tvInfo01.setText(e.toString())
} finally {
try {
fw.close()
} catch(e: IOException) {
binding.tvInfo01.setText(e.toString())
}
}
}
query?.close()
return
}
fun getRandomNum(maxNum: Int): Int {
val random = Random()
return random.nextInt(maxNum)
}
}
SmbViewModel.kt
// 2021 Dec. 05.
// 2021 Oct. 30.
// 2021 Sep. 06.
// Ryuichi Hashimoto
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.util.*
class SmbViewModel(private val user: String, private val password: String, private val domain: String, private val smbUrl: String): ViewModel() {
private val audioExtension: String = "\\.mp3|\\.wav|\\.aac|\\.au|\\.gsm|\\.m4a|\\.ogg|\\.flac"
lateinit var smb: SmbFile
data class AudioFileProperty(val smbPath: String, val fileSize: Long)
enum class ProcessStatus {
NotYetGot,
FromNow,
Trying,
Got,
Failed
}
// LiveDataを設定する
private var _smbConnectionStatus = MutableLiveData(ProcessStatus.NotYetGot)
val smbConnectionStatus: LiveData<ProcessStatus>
get() = _smbConnectionStatus
private var _getAudioFileStatus = MutableLiveData(ProcessStatus.NotYetGot)
val getAudioFileStatus: LiveData<ProcessStatus>
get() = _getAudioFileStatus
private val _smbPath: MutableLiveData<String> by lazy {
// MutableLiveData<T>は、valでmutableな変数を宣言できる
MutableLiveData<String>()
}
val smbPath: LiveData<String>
// 下記により、smbPathの読み込み時は _smbPathが読み出される
get() = _smbPath
val _smbSize: MutableLiveData<Long> by lazy {
MutableLiveData<Long>()
}
val smbSize: LiveData<Long>
get() = _smbSize
// Main process in this ViewModel
fun connectSmbSelectAudioFile() {
viewModelScope.launch(Dispatchers.IO) {
// connect to SMB server in worker thread
try {
_smbConnectionStatus.postValue(ProcessStatus.Trying) // tyring to connect to SMB
connectSmb()
} catch (e: Exception) {
_smbConnectionStatus.postValue(ProcessStatus.Failed) // Failed to connect to SMB"
return@launch
}
// got connection to SMB
_smbConnectionStatus.postValue(ProcessStatus.Got) // connected to SMB
// select an audio file in SMB
_getAudioFileStatus.postValue(ProcessStatus.Trying) // trying to get an audio file
val audioFileProperty = selectAudioSmbFile()
if ( audioFileProperty.fileSize < 1L ) {
// no audio file
_getAudioFileStatus.postValue(ProcessStatus.Failed) // failed to get an audio file
smb.close()
} else {
// got an audio file
_smbPath.postValue(audioFileProperty.smbPath)
_smbSize.postValue(audioFileProperty.fileSize)
_getAudioFileStatus.postValue(ProcessStatus.Got) // got an audio file
// play audio in MainActivity
}
} // end of launch
if ( ProcessStatus.Failed == smbConnectionStatus.value) {
// Failed to connect to SMB
return
}
if (ProcessStatus.Failed == getAudioFileStatus.value) {
// Failed to get an audio file
return
}
}
// SMBサーバーに接続し、接続オブジェクトをsmbに入れる
private fun connectSmb() {
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))
// connect to SMB server
smb = SmbFile(smbUrl, auth)
smb.list() // check if SMB-connection succeeded or failed
} catch (e1: Exception) {
try {
smb.close()
}
catch (e2: Exception) {
throw e2
}
throw e1
}
}
/*
* ランダムにSMBサーバーの音楽ファイルを1つ選び、そのパスとファイルサイズをAudioFilePropertyデータクラスで返す
* data class AudioFileProperty(val smbPath: String, val fileSize: Long)
* 失敗した時は AudioFileProperty("",0L) を返す。
* 変数smbは、関数の外でjcifs-ngによって取得したSmbFileインスタンス
* audioExtensionは、関数の外で設定された、音声ファイル拡張子集合の文字列
*/
fun selectAudioSmbFile(): AudioFileProperty {
try {
if (smb.isDirectory) {
// get all files in dir
val smbNormalFiles: MutableList<SmbFile>
val tmpSmbNormalFiles: MutableList<SmbFile> = mutableListOf()
smbNormalFiles = getNormalSmbFiles(smb, tmpSmbNormalFiles)
if (1 > smbNormalFiles.size) {
// No file in the directory
return AudioFileProperty("",0L)
} else if (1 == smbNormalFiles.size) {
// only one file in the directory
if (!isMatchTail(smbNormalFiles[0].name, audioExtension)) {
// No audio file
return AudioFileProperty("",0L)
} else {
// Got one sound file.
// Play Audio in main thread
return AudioFileProperty(smbNormalFiles[0].path, smbNormalFiles[0].length())
}
} else { // smbNormalFiles.size > 1
// in case of plural files.
// collect audio files to audioSmbFiles
val audioSmbFiles: MutableList<SmbFile> = mutableListOf()
for (eachSmbNormalFile in smbNormalFiles) {
if (isMatchTail(eachSmbNormalFile.name, audioExtension)) {
audioSmbFiles.add(eachSmbNormalFile)
}
}
if (1 > audioSmbFiles.size) {
// No audio file
return AudioFileProperty("",0L)
} else if (1 == audioSmbFiles.size) {
// Got one sound file.
// PlayAudio in main thread
return AudioFileProperty(audioSmbFiles[0].path, audioSmbFiles[0].length())
} else { // audioSmbFiles.size > 1
// select one audio file randomly
val countFiles: Int = audioSmbFiles.size
val random = Random()
val randomNum = random.nextInt(countFiles)
// Got one sound file.
// PlayAudio in main thread
return AudioFileProperty(audioSmbFiles[randomNum].path, audioSmbFiles[randomNum].length())
}
}
} else if (smb.isFile) {
return if (!isMatchTail(smb.name, audioExtension)) {
// No audio file
AudioFileProperty("",0L)
} else { // a normal file
// Got one sound file.
// PlayAudio in main thread
AudioFileProperty(smb.path, smb.length())
}
} else { // in case of smb is abnormal
return AudioFileProperty("",0L)
}
} catch (e: Exception) {
return AudioFileProperty("",0L)
}
}
override fun onCleared() {
try {
smb.close()
} catch (e: Exception) { }
finally {
viewModelScope.cancel()
super.onCleared()
}
}
/*
* SMBサーバーのすべてのノーマルファイルを取得する再帰関数
* パラメータ
* givenDir 取得対象のSMBディレクトリパス。
* tmpSmbNormalFiles 空のMutableList<SmbFile>型変数。
* 1つ前の再帰関数実行結果を次の再帰関数に渡すための変数。
* ユーザーは最初にこの関数を起動するので、空のMutableListを与える。
*/
private fun getNormalSmbFiles(givenDir: SmbFile, tmpSmbNormalFiles: MutableList<SmbFile>): MutableList<SmbFile> {
val childSmbs = givenDir.listFiles()
if (childSmbs.isNotEmpty()) {
for (eachChild in childSmbs) {
if (eachChild.isDirectory) {
getNormalSmbFiles(eachChild, tmpSmbNormalFiles)
} else if (eachChild.isFile) {
tmpSmbNormalFiles.add(eachChild)
} else {
continue
}
}
}
return tmpSmbNormalFiles
}
/*
* 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)
}
}
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
}
}
SmbServerSettingDialogFragment.kt
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.widget.Button
import android.widget.EditText
import androidx.fragment.app.DialogFragment
class SmbServerSettingDialogFragment: DialogFragment() {
interface DialogListener{
public fun onDialogMapReceive(dialog: DialogFragment, smbSettings: MutableMap<String, String>)
}
var listener:DialogListener? = null
class EditsWatcher(val button: Button, val edit1: EditText, val edit2: EditText, val edit3: EditText, val edit4: EditText):
TextWatcher {
init {
edit1.addTextChangedListener(this)
edit2.addTextChangedListener(this)
edit3.addTextChangedListener(this)
edit4.addTextChangedListener(this)
afterTextChanged(null)
}
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
}
override fun afterTextChanged(s: Editable?) {
button.isEnabled =
edit1.text.toString().trim().isNotEmpty() &&
edit2.text.toString().trim().isNotEmpty() &&
edit3.text.toString().trim().isNotEmpty() &&
edit4.text.toString().trim().isNotEmpty()
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val smbSettings: MutableMap<String, String> = mutableMapOf(
"smbDomain" to "",
"smbPath" to "",
"smbUser" to "",
"smbPassword" to "")
val builder = AlertDialog.Builder(activity)
val inflater = requireActivity().layoutInflater
val smbServerSettingView = inflater.inflate(R.layout.dialog_smb_server_setting, null)
val edDomain = smbServerSettingView.findViewById<EditText>(R.id.etDomain)
val edSmbPath = smbServerSettingView.findViewById<EditText>(R.id.etSmbPath)
val edSmbUser = smbServerSettingView.findViewById<EditText>(R.id.etSmbUser)
val edSmbPassword = smbServerSettingView.findViewById<EditText>(R.id.etSmbPassword)
edDomain.setText(requireArguments().getString("smbDomain", ""))
edSmbPath.setText(requireArguments().getString("smbRootPath", ""))
edSmbUser.setText(requireArguments().getString("smbUser", ""))
edSmbPassword.setText(requireArguments().getString("smbPassword", ""))
builder.setView(smbServerSettingView)
.setTitle("SMB Server Setting")
.setPositiveButton("OK") { dialog, id ->
with(smbSettings) {
put(
"smbDomain",
edDomain.text.toString()
)
put(
"smbPath",
edSmbPath.text.toString()
)
put(
"smbUser",
edSmbUser.text.toString()
)
put(
"smbPassword",
edSmbPassword.text.toString()
)
}
listener?.onDialogMapReceive(this,smbSettings) ?: throw Exception("listener is null.")
}
.setNegativeButton("Cancel") { dialog, id ->
}
return builder.create().also { dialog ->
dialog.setOnShowListener {
val button = dialog.getButton(Dialog.BUTTON_POSITIVE)
EditsWatcher(button, edDomain, edSmbPath, edSmbUser, edSmbPassword)
}
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
try {
listener = context as DialogListener
}catch (e: Exception){
Log.e("MyApp","CANNOT FIND LISTENER")
}
}
override fun onDetach() {
super.onDetach()
listener = null
}
}
ListPlayedFilesDialogFragment.kt
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import android.app.AlertDialog
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.Toast
import java.io.*
class ListPlayedFilesDialogFragment: DialogFragment(){
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(activity)
val inflater = requireActivity().layoutInflater
val myDialogView = inflater.inflate(R.layout.played_files_list, null)
var playedFiles = ""
lateinit var br: BufferedReader
try {
val savePathFile = File(this.context?.filesDir, getString(R.string.pathHistoryFile))
if (savePathFile.exists()) {
br = BufferedReader(FileReader(savePathFile))
try {
playedFiles = br.readLine()
} catch(e: IOException) {
playedFiles = e.toString()
} finally {
try {
br.close()
} catch(e: IOException) {
playedFiles = playedFiles + " " + e.toString()
}
}
}
} catch (e: Exception){
playedFiles = playedFiles + " " + e.toString()
}
if (playedFiles == "") {
playedFiles = getString(R.string.noHistory)
}
playedFiles = playedFiles.removeSuffix(",")
val playedFileList = playedFiles.split(",").asReversed()
val myAdapter =
ArrayAdapter(this.requireContext(), android.R.layout.simple_list_item_1, playedFileList)
val listViewFiles = myDialogView.findViewById<ListView>(R.id.listViewFiles)
listViewFiles.adapter = myAdapter
builder.setView(myDialogView)
.setTitle("Played Files")
.setPositiveButton("Clear") { dialog, which ->
lateinit var fos: FileOutputStream
try {
val savePathFile =
File( context?.filesDir, getString(R.string.pathHistoryFile))
fos = FileOutputStream(savePathFile)
fos.write(','.code.toInt())
} catch (e: Exception) {
Toast.makeText(context,e.toString(), Toast.LENGTH_LONG ).show()
} finally {
fos.close()
}
}
.setNeutralButton("OK") { _, _ ->
}
return builder.create()
}
}