diff --git a/app/build.gradle b/app/build.gradle index 5a84783..59b4267 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,8 +29,8 @@ android { applicationId "ru.nsu.bobrofon.easysshfs" minSdkVersion 16 targetSdkVersion 33 - versionCode 85 - versionName "0.5.11" + versionCode 86 + versionName "0.5.12-dev" externalNativeBuild { cmake { @@ -76,6 +76,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' implementation 'androidx.preference:preference-ktx:1.2.0' + implementation 'androidx.lifecycle:lifecycle-service:2.6.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation "androidx.fragment:fragment-ktx:1.6.0" implementation "androidx.core:core-ktx:1.10.1" diff --git a/app/src/main/java/ru/nsu/bobrofon/easysshfs/EasySSHFSService.kt b/app/src/main/java/ru/nsu/bobrofon/easysshfs/EasySSHFSService.kt index 5170ac5..951a015 100644 --- a/app/src/main/java/ru/nsu/bobrofon/easysshfs/EasySSHFSService.kt +++ b/app/src/main/java/ru/nsu/bobrofon/easysshfs/EasySSHFSService.kt @@ -5,28 +5,41 @@ import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent -import android.app.Service import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.wifi.WifiManager import android.os.Build import android.os.Handler -import android.os.IBinder import android.os.Looper import android.util.Log import androidx.core.app.NotificationCompat +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import com.topjohnwu.superuser.Shell +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import ru.nsu.bobrofon.easysshfs.mountpointlist.MountPointsList +import ru.nsu.bobrofon.easysshfs.settings.SettingsRepository +import ru.nsu.bobrofon.easysshfs.settings.settingsDataStore +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds private const val TAG = "EasySSHFSService" private const val CHANNEL_ID = "Channel Mount" private const val CHANNEL_NAME = "Mount" private const val NOTIFICATION_ID = 1 -class EasySSHFSService : Service() { +class EasySSHFSService : LifecycleService() { private val handler = Handler(Looper.getMainLooper()) private val internetStateChangeReceiver = InternetStateChangeReceiver(handler) private val internetStateChangeFilter = IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION) + private val shell: Shell by lazy { EasySSHFSActivity.initNewShell() } private val notification: Notification by lazy { NotificationCompat.Builder(applicationContext, CHANNEL_ID).apply { @@ -58,8 +71,14 @@ class EasySSHFSService : Service() { override fun onCreate() { super.onCreate() + Log.d(TAG, "register NETWORK_STATE_CHANGED receiver") registerReceiver(internetStateChangeReceiver, internetStateChangeFilter) + + Log.d(TAG, "manage periodic ssh servers check") + lifecycleScope.launch { + managePeriodicServersCheck() + } } override fun onDestroy() { @@ -70,14 +89,54 @@ class EasySSHFSService : Service() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + Log.d(TAG, "onStartCommand") notificationManager.notify(NOTIFICATION_ID, notification) startForeground(NOTIFICATION_ID, notification) return START_STICKY } - override fun onBind(intent: Intent?): IBinder? { - return null + private suspend fun managePeriodicServersCheck() { + val settingsRepository = SettingsRepository(applicationContext.settingsDataStore) + var periodicJob: Job? = null + + settingsRepository.sshServersCheckRequired.collect { isRequired -> + if (isRequired) { + if (periodicJob == null) { + Log.d(TAG, "lunch new periodic ssh servers checker") + periodicJob = lifecycleScope.launch { schedulePeriodicServersChecks() } + } + } else { + if (periodicJob != null) { + Log.d(TAG, "cancel periodic ssh servers checker") + periodicJob?.cancelAndJoin() + periodicJob = null + } + } + } + } + + private suspend fun schedulePeriodicServersChecks() { + while (true) { + Log.d(TAG, "waiting for the next remote servers check") + delay(REMOTE_SERVERS_CHECK_PERIOD) + + Log.d(TAG, "check if some mountpoints are not automounted") + withContext(Dispatchers.IO) { + try { + val mountpoints = MountPointsList.instance(applicationContext).mountPoints + .filter { it.autoMount } + .filter { !it.checkIfMounted(false).first } + .filter { it.checkIfRemoteIsReachable(REMOTE_SERVER_CONNECTION_TIMEOUT) } + + Log.d(TAG, "${mountpoints.size} mountpoints have to be mounted") + mountpoints.forEach { it.mount(shell) } + } catch (e: Exception) { + Log.d(TAG, "periodic remote servers check failed", e) + } + } + } } companion object { @@ -92,5 +151,8 @@ class EasySSHFSService : Service() { fun stop(context: Context) { context.stopService(Intent(context, EasySSHFSService::class.java)) } + + private val REMOTE_SERVERS_CHECK_PERIOD = 5.minutes + private val REMOTE_SERVER_CONNECTION_TIMEOUT = 1.seconds } -} \ No newline at end of file +} diff --git a/app/src/main/java/ru/nsu/bobrofon/easysshfs/mountpointlist/mountpoint/MountPoint.kt b/app/src/main/java/ru/nsu/bobrofon/easysshfs/mountpointlist/mountpoint/MountPoint.kt index 7b3439f..72b4dd4 100644 --- a/app/src/main/java/ru/nsu/bobrofon/easysshfs/mountpointlist/mountpoint/MountPoint.kt +++ b/app/src/main/java/ru/nsu/bobrofon/easysshfs/mountpointlist/mountpoint/MountPoint.kt @@ -15,7 +15,11 @@ import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.lang.ref.WeakReference -import java.util.* +import java.net.InetSocketAddress +import java.net.Socket +import java.net.SocketTimeoutException +import java.util.LinkedList +import kotlin.time.Duration class MountPoint( @@ -161,6 +165,46 @@ class MountPoint( } } + fun checkIfRemoteIsReachable(timeout: Duration): Boolean { + val socket = Socket() + try { + val address = InetSocketAddress(host, port) + socket.connect(address, timeout.inWholeMilliseconds.toInt()) + + } catch (e: IOException) { + Log.d(TAG, "'$host:$port' connection failed", e) + return false + } catch (e: SocketTimeoutException) { + Log.d(TAG, "'$host:$port' connection timeout", e) + return false + } finally { + try { + socket.close() + } catch (e: Exception) { + Log.w(TAG, "failed to close socket", e) + } + } + + return true + } + + fun checkIfMounted(foreground: Boolean): Pair { + val mountLine = StringBuilder() + mountLine.append("$userName@${hostIp(foreground)}:") + + val canonicalLocalPath = File(localPath).canonicalPath + mountLine.append("$remotePath $canonicalLocalPath fuse.sshfs ") + + val expectedLine = mountLine.toString() + + val procmount = File(MOUNT_FILE) + val result = procmount.useLines { lines -> + lines.any { line -> line.contains(expectedLine) } + } + + return Pair(result, expectedLine) + } + companion object { private const val DEFAULT_OPTIONS = ( @@ -179,51 +223,34 @@ class MountPoint( private const val TAG = "MOUNT_POINT" - private class CheckMountTask constructor( + private const val MOUNT_FILE = "/proc/mounts" + + private class CheckMountTask( private val mountPoint: MountPoint, private val context: WeakReference = WeakReference(null) ) : AsyncTask>() { - private val mountFile = "/proc/mounts" - constructor(mountPoint: MountPoint, context: Context?) : this(mountPoint, WeakReference(context)) @Deprecated("Deprecated in Java") override fun doInBackground(vararg params: Void): Pair { val fromForeground = context.get() != null - val mountLine = StringBuilder() - mountLine.append("${mountPoint.userName}@${mountPoint.hostIp(fromForeground)}:") - - val canonicalLocalPath: String - try { - canonicalLocalPath = File(mountPoint.localPath).canonicalPath - } catch (e: IOException) { - return Pair( - null, - "Can't get canonical path of ${mountPoint.localPath}: ${e.message}" - ) - } - - mountLine.append("${mountPoint.remotePath} $canonicalLocalPath fuse.sshfs ") - val result: Boolean + val result: Pair try { - val procmount = File(mountFile) - result = procmount.useLines { lines -> - lines.any { line -> line.contains(mountLine.toString()) } - } + result = mountPoint.checkIfMounted(fromForeground) } catch (e: FileNotFoundException) { return Pair(null, e.message ?: "") } catch (e: IOException) { return Pair(null, e.message ?: "") } - return if (result) { - Pair(true, "Pattern $mountLine is in $mountFile") + return if (result.first) { + Pair(true, "Pattern ${result.second} is in $MOUNT_FILE") } else { - Pair(false, "Pattern $mountLine is not in $mountFile") + Pair(false, "Pattern ${result.second} is not in $MOUNT_FILE") } } @@ -242,7 +269,7 @@ class MountPoint( } } - private class MountTask constructor( + private class MountTask( private val mountPoint: MountPoint, private val shell: Shell, private val context: WeakReference = WeakReference(null) diff --git a/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/Settings.kt b/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/Settings.kt index b2afe89..339708e 100644 --- a/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/Settings.kt +++ b/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/Settings.kt @@ -17,4 +17,9 @@ object Settings { * Start foreground service to automatically mount/umount mountpoints on network changes. */ val autoMountInForegroundService = booleanPreferencesKey("autoMountInForegroundService") + + /** + * Check remote ssh services periodically to automatically mount mountpoints. + */ + val checkSshServersPeriodically = booleanPreferencesKey("checkSshServersPeriodically") } diff --git a/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/SettingsFragment.kt b/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/SettingsFragment.kt index fda1dd3..afa23a1 100644 --- a/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/SettingsFragment.kt +++ b/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/SettingsFragment.kt @@ -24,8 +24,21 @@ class SettingsFragment(viewModelFactory: SettingsViewModel.Factory) : Preference } } + val checkSshServersPeriodicallySwitch = + findPreference(Settings.checkSshServersPeriodically.name)?.apply { + setOnPreferenceChangeListener { _, value -> + viewModel.setCheckSshServersPeriodically(value as Boolean) + true + } + } + viewModel.autoMountInForegroundService.observe(this) { autoMountInForegroundServiceSwitch?.isChecked = it + checkSshServersPeriodicallySwitch?.isEnabled = it + } + + viewModel.checkSshServersPeriodically.observe(this) { + checkSshServersPeriodicallySwitch?.isChecked = it } } diff --git a/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/SettingsRepository.kt b/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/SettingsRepository.kt index 687681b..2424607 100644 --- a/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/SettingsRepository.kt +++ b/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/SettingsRepository.kt @@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map class SettingsRepository(private val settingsDataStore: DataStore) { @@ -18,4 +19,20 @@ class SettingsRepository(private val settingsDataStore: DataStore) settings[Settings.autoMountInForegroundService] = value } } + + val checkSshServersPeriodically: Flow + get() = settingsDataStore.data.map { settings -> + settings[Settings.checkSshServersPeriodically] ?: false + } + + suspend fun setCheckSshServersPeriodically(value: Boolean) { + settingsDataStore.edit { settings -> + settings[Settings.checkSshServersPeriodically] = value + } + } + + val sshServersCheckRequired: Flow + get() = autoMountInForegroundService.combine(checkSshServersPeriodically) { autoMount, periodicCheck -> + autoMount && periodicCheck + } } diff --git a/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/SettingsViewModel.kt b/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/SettingsViewModel.kt index 1f5ad47..d0d23f5 100644 --- a/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/SettingsViewModel.kt +++ b/app/src/main/java/ru/nsu/bobrofon/easysshfs/settings/SettingsViewModel.kt @@ -8,12 +8,21 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel( private val _autoMountInForegroundService = MutableLiveData() val autoMountInForegroundService: LiveData get() = _autoMountInForegroundService + private val _checkSshServersPeriodically = MutableLiveData() + val checkSshServersPeriodically: LiveData get() = _checkSshServersPeriodically + init { viewModelScope.launch { repository.autoMountInForegroundService.collect { _autoMountInForegroundService.value = it } } + + viewModelScope.launch { + repository.checkSshServersPeriodically.collect { + _checkSshServersPeriodically.value = it + } + } } fun setAutoMountInForegroundService(value: Boolean) { @@ -22,6 +31,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel( } } + fun setCheckSshServersPeriodically(value: Boolean) { + viewModelScope.launch { + repository.setCheckSshServersPeriodically(value) + } + } + class Factory(private val repository: SettingsRepository) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { if (modelClass.isAssignableFrom(SettingsViewModel::class.java)) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0c46815..46f9990 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,5 +51,10 @@ Start foreground service to manage network changes all the time Manage network changes only when application is in foreground + Check remote servers periodically + + Check remote servers periodically and automount mountpoints if remote server becomes available + + Don\'t try to automount mountpoints if remote server becomes unavailable Privacy Policy diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 8180518..da3c678 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -10,6 +10,13 @@ app:summaryOn="@string/foreground_service_summary_on" app:title="@string/foreground_service_title" /> + +