From 355251694d63640f028f3e2c17235d12a8573df6 Mon Sep 17 00:00:00 2001 From: pacien Date: Tue, 8 Dec 2020 18:03:20 +0100 Subject: ConfigurationAccessService: prevent service from being stopped when app loses focus This makes the ConfigurationAccessService (formerly ConfigurationFtpService) start in foreground through the use of a persistent notification so that it isn't stopped by the system after the app loses the focus on the user's screen, which happens when the user switches to an FTP client application on the same device. --- app/src/main/AndroidManifest.xml | 5 +- .../configure/ConfigurationAccessServerFragment.kt | 72 +++++++++ .../configure/ConfigurationFtpServerFragment.kt | 72 --------- .../tincapp/context/AppNotificationManager.kt | 39 +++-- .../tincapp/service/ConfigurationAccessService.kt | 174 +++++++++++++++++++++ .../tincapp/service/ConfigurationFtpService.kt | 150 ------------------ .../ic_baseline_folder_open_primary_24dp.xml | 29 ++++ app/src/main/res/layout/configure_activity.xml | 4 +- ...nfigure_tools_configuration_access_fragment.xml | 86 ++++++++++ ...ure_tools_configuration_ftp_server_fragment.xml | 86 ---------- app/src/main/res/values/strings.xml | 6 +- 11 files changed, 400 insertions(+), 323 deletions(-) create mode 100644 app/src/main/java/org/pacien/tincapp/activities/configure/ConfigurationAccessServerFragment.kt delete mode 100644 app/src/main/java/org/pacien/tincapp/activities/configure/ConfigurationFtpServerFragment.kt create mode 100644 app/src/main/java/org/pacien/tincapp/service/ConfigurationAccessService.kt delete mode 100644 app/src/main/java/org/pacien/tincapp/service/ConfigurationFtpService.kt create mode 100644 app/src/main/res/drawable/ic_baseline_folder_open_primary_24dp.xml create mode 100644 app/src/main/res/layout/configure_tools_configuration_access_fragment.xml delete mode 100644 app/src/main/res/layout/configure_tools_configuration_ftp_server_fragment.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 020d62d..c98555f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,6 +24,9 @@ + + + @@ -82,7 +85,7 @@ + android:name="org.pacien.tincapp.service.ConfigurationAccessService"> diff --git a/app/src/main/java/org/pacien/tincapp/activities/configure/ConfigurationAccessServerFragment.kt b/app/src/main/java/org/pacien/tincapp/activities/configure/ConfigurationAccessServerFragment.kt new file mode 100644 index 0000000..9fee749 --- /dev/null +++ b/app/src/main/java/org/pacien/tincapp/activities/configure/ConfigurationAccessServerFragment.kt @@ -0,0 +1,72 @@ +/* + * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon + * Copyright (C) 2017-2020 Pacien TRAN-GIRARD + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.pacien.tincapp.activities.configure + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.Observable +import androidx.databinding.ObservableBoolean +import org.pacien.tincapp.activities.BaseFragment +import org.pacien.tincapp.databinding.ConfigureToolsConfigurationAccessFragmentBinding +import org.pacien.tincapp.service.ConfigurationAccessService + +/** + * @author pacien + */ +class ConfigurationAccessServerFragment : BaseFragment() { + private val ftpServerStartListener = object : Observable.OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable, propertyId: Int) { + binding.ftpEnabled = (sender as ObservableBoolean).get() + } + } + + private lateinit var binding: ConfigureToolsConfigurationAccessFragmentBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = ConfigureToolsConfigurationAccessFragmentBinding.inflate(inflater, container, false) + binding.ftpUsername = ConfigurationAccessService.FTP_USERNAME + binding.ftpPassword = ConfigurationAccessService.FTP_PASSWORD + binding.ftpPort = ConfigurationAccessService.FTP_PORT + binding.toggleFtpState = { toggleServer() } + return binding.root + } + + override fun onResume() { + super.onResume() + ConfigurationAccessService.runningState.addOnPropertyChangedCallback(ftpServerStartListener) + binding.ftpEnabled = ConfigurationAccessService.runningState.get() + } + + override fun onPause() { + ConfigurationAccessService.runningState.removeOnPropertyChangedCallback(ftpServerStartListener) + super.onPause() + } + + private fun toggleServer() { + val targetServiceIntent = Intent(requireContext(), ConfigurationAccessService::class.java) + + if (binding.ftpEnabled) + requireContext().stopService(targetServiceIntent) + else + requireContext().startService(targetServiceIntent) + } +} diff --git a/app/src/main/java/org/pacien/tincapp/activities/configure/ConfigurationFtpServerFragment.kt b/app/src/main/java/org/pacien/tincapp/activities/configure/ConfigurationFtpServerFragment.kt deleted file mode 100644 index b97a15e..0000000 --- a/app/src/main/java/org/pacien/tincapp/activities/configure/ConfigurationFtpServerFragment.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon - * Copyright (C) 2017-2020 Pacien TRAN-GIRARD - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.pacien.tincapp.activities.configure - -import android.content.Intent -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.databinding.Observable -import androidx.databinding.ObservableBoolean -import org.pacien.tincapp.activities.BaseFragment -import org.pacien.tincapp.databinding.ConfigureToolsConfigurationFtpServerFragmentBinding -import org.pacien.tincapp.service.ConfigurationFtpService - -/** - * @author pacien - */ -class ConfigurationFtpServerFragment : BaseFragment() { - private val ftpServerStartListener = object : Observable.OnPropertyChangedCallback() { - override fun onPropertyChanged(sender: Observable, propertyId: Int) { - binding.ftpEnabled = (sender as ObservableBoolean).get() - } - } - - private lateinit var binding: ConfigureToolsConfigurationFtpServerFragmentBinding - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = ConfigureToolsConfigurationFtpServerFragmentBinding.inflate(inflater, container, false) - binding.ftpUsername = ConfigurationFtpService.FTP_USERNAME - binding.ftpPassword = ConfigurationFtpService.FTP_PASSWORD - binding.ftpPort = ConfigurationFtpService.FTP_PORT - binding.toggleFtpState = { toggleServer() } - return binding.root - } - - override fun onResume() { - super.onResume() - ConfigurationFtpService.runningState.addOnPropertyChangedCallback(ftpServerStartListener) - binding.ftpEnabled = ConfigurationFtpService.runningState.get() - } - - override fun onPause() { - ConfigurationFtpService.runningState.removeOnPropertyChangedCallback(ftpServerStartListener) - super.onPause() - } - - private fun toggleServer() { - val targetServiceIntent = Intent(requireContext(), ConfigurationFtpService::class.java) - - if (binding.ftpEnabled) - requireContext().stopService(targetServiceIntent) - else - requireContext().startService(targetServiceIntent) - } -} diff --git a/app/src/main/java/org/pacien/tincapp/context/AppNotificationManager.kt b/app/src/main/java/org/pacien/tincapp/context/AppNotificationManager.kt index 38bf6e4..9d731a5 100644 --- a/app/src/main/java/org/pacien/tincapp/context/AppNotificationManager.kt +++ b/app/src/main/java/org/pacien/tincapp/context/AppNotificationManager.kt @@ -1,6 +1,6 @@ /* * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon - * Copyright (C) 2017-2019 Pacien TRAN-GIRARD + * Copyright (C) 2017-2020 Pacien TRAN-GIRARD * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,6 +18,7 @@ package org.pacien.tincapp.context +import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent @@ -35,16 +36,19 @@ import org.pacien.tincapp.R */ class AppNotificationManager(private val context: Context) { companion object { - private const val CHANNEL_ID = "org.pacien.tincapp.notification.channels.error" - private const val ERROR_NOTIFICATION_ID = 0 + private const val ERROR_CHANNEL_ID = "org.pacien.tincapp.notification.channels.error" + private const val CONFIG_ACCESS_CHANNEL_ID = "org.pacien.tincapp.notification.channels.configuration" + + const val ERROR_NOTIFICATION_ID = 0 + const val CONFIG_ACCESS_NOTIFICATION_ID = 1 } init { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) registerChannel() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) registerChannels() } fun notifyError(title: String, message: String, manualLink: String? = null) { - val notification = NotificationCompat.Builder(context, CHANNEL_ID) + val notification = NotificationCompat.Builder(context, ERROR_CHANNEL_ID) .setSmallIcon(R.drawable.ic_warning_primary_24dp) .setContentTitle(title) .setContentText(message) @@ -62,13 +66,26 @@ class AppNotificationManager(private val context: Context) { NotificationManagerCompat.from(context).cancelAll() } + fun newConfigurationAccessNotificationBuilder() = + NotificationCompat.Builder(context, CONFIG_ACCESS_CHANNEL_ID) + @RequiresApi(Build.VERSION_CODES.O) - private fun registerChannel() { - val name = context.getString(R.string.notification_error_channel_name) - val importance = NotificationManager.IMPORTANCE_HIGH - val channel = NotificationChannel(CHANNEL_ID, name, importance) - val notificationManager = context.getSystemService(NotificationManager::class.java) - notificationManager.createNotificationChannel(channel) + private fun registerChannels() { + context.getSystemService(NotificationManager::class.java) + .apply { + createNotificationChannel(NotificationChannel( + ERROR_CHANNEL_ID, + context.getString(R.string.notification_error_channel_name), + NotificationManager.IMPORTANCE_HIGH + )) + } + .apply { + createNotificationChannel(NotificationChannel( + CONFIG_ACCESS_CHANNEL_ID, + context.getString(R.string.notification_config_access_channel_name), + NotificationManager.IMPORTANCE_MIN + )) + } } private fun NotificationCompat.Builder.setHighPriority() = apply { diff --git a/app/src/main/java/org/pacien/tincapp/service/ConfigurationAccessService.kt b/app/src/main/java/org/pacien/tincapp/service/ConfigurationAccessService.kt new file mode 100644 index 0000000..b083a83 --- /dev/null +++ b/app/src/main/java/org/pacien/tincapp/service/ConfigurationAccessService.kt @@ -0,0 +1,174 @@ +/* + * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon + * Copyright (C) 2017-2020 Pacien TRAN-GIRARD + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.pacien.tincapp.service + +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.os.IBinder +import androidx.databinding.ObservableBoolean +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import org.apache.ftpserver.FtpServer +import org.apache.ftpserver.FtpServerFactory +import org.apache.ftpserver.ftplet.* +import org.apache.ftpserver.listener.ListenerFactory +import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication +import org.apache.ftpserver.usermanager.impl.WritePermission +import org.pacien.tincapp.R +import org.pacien.tincapp.activities.configure.ConfigureActivity +import org.pacien.tincapp.context.App +import org.pacien.tincapp.context.AppNotificationManager +import org.pacien.tincapp.extensions.Java.defaultMessage +import org.slf4j.LoggerFactory +import java.io.IOException + +/** + * FTP server service allowing a remote and local user to access and modify configuration files in + * the application's context. + * + * @author pacien + */ +class ConfigurationAccessService : Service() { + companion object { + // Apache Mina FtpServer's INFO log level is actually VERBOSE. + // The object holds static references to those loggers so that they stay around. + @Suppress("unused") + private val MINA_FTP_LOGGER_OVERRIDER = MinaLoggerOverrider(Level.WARN) + + const val FTP_PORT = 65521 // tinc port `concat` FTP port + const val FTP_USERNAME = "tincapp" + val FTP_HOME_DIR = App.getContext().applicationInfo.dataDir!! + val FTP_PASSWORD = generateRandomString(8) + + val runningState = ObservableBoolean(false) + + private fun generateRandomString(length: Int): String { + val alphabet = ('a'..'z') + ('A'..'Z') + ('0'..'9') + return List(length) { alphabet.random() }.joinToString("") + } + } + + private val log by lazy { LoggerFactory.getLogger(this.javaClass)!! } + private val notificationManager by lazy { App.notificationManager } + private var sftpServer: FtpServer? = null + + override fun onBind(intent: Intent): IBinder? = null // non-bindable service + + override fun onDestroy() { + sftpServer?.stop() + sftpServer = null + runningState.set(false) + log.info("Stopped FTP server") + super.onDestroy() + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + val ftpUser = StaticFtpUser(FTP_USERNAME, FTP_PASSWORD, FTP_HOME_DIR, listOf(WritePermission())) + sftpServer = setupSingleUserServer(ftpUser).also { + try { + it.start() + runningState.set(true) + log.info("Started FTP server on port {}", FTP_PORT) + pinInForeground() + } catch (e: IOException) { + log.error("Could not start FTP server", e) + App.alert(R.string.notification_error_title_unable_to_start_ftp_server, e.defaultMessage()) + } + } + + return START_NOT_STICKY + } + + /** + * Pins the service in the foreground so that it doesn't get stopped by the system when the + * application's activities are put in the background, which is the case when the user sets the + * focus on an FTP client app for example. + */ + private fun pinInForeground() { + startForeground( + AppNotificationManager.CONFIG_ACCESS_NOTIFICATION_ID, + notificationManager.newConfigurationAccessNotificationBuilder() + .setSmallIcon(R.drawable.ic_baseline_folder_open_primary_24dp) + .setContentTitle(resources.getString(R.string.notification_config_access_server_running_title)) + .setContentText(resources.getString(R.string.notification_config_access_server_running_message)) + .setContentIntent(Intent(this, ConfigureActivity::class.java).let { + PendingIntent.getActivity(this, 0, it, 0) + }) + .build() + ) + } + + private fun setupSingleUserServer(ftpUser: User): FtpServer { + return FtpServerFactory() + .apply { addListener("default", ListenerFactory().apply { port = FTP_PORT }.createListener()) } + .apply { userManager = StaticFtpUserManager(listOf(ftpUser)) } + .createServer() + } + + private class StaticFtpUserManager(users: List) : UserManager { + private val userMap: Map = users.map { it.name to it }.toMap() + override fun getUserByName(username: String?): User? = userMap[username] + override fun getAllUserNames(): Array = userMap.keys.toTypedArray() + override fun doesExist(username: String?): Boolean = username in userMap + override fun delete(username: String?) = throw UnsupportedOperationException() + override fun save(user: User?) = throw UnsupportedOperationException() + override fun getAdminName(): String = throw UnsupportedOperationException() + override fun isAdmin(username: String?): Boolean = throw UnsupportedOperationException() + override fun authenticate(authentication: Authentication?): User = when (authentication) { + is UsernamePasswordAuthentication -> getUserByName(authentication.username).let { + if (it != null && authentication.password == it.password) it + else throw AuthenticationFailedException() + } + else -> throw IllegalArgumentException() + } + } + + private data class StaticFtpUser( + private val name: String, + private val password: String, + private val homeDirectory: String, + private val authorities: List + ) : User { + override fun getName(): String = name + override fun getPassword(): String = password + override fun getAuthorities(): List = authorities + override fun getAuthorities(clazz: Class): List = authorities.filter(clazz::isInstance) + override fun getMaxIdleTime(): Int = 0 // unlimited + override fun getEnabled(): Boolean = true + override fun getHomeDirectory(): String = homeDirectory + override fun authorize(request: AuthorizationRequest?): AuthorizationRequest? = + authorities.filter { it.canAuthorize(request) }.fold(request) { req, auth -> auth.authorize(req) } + } + + /** + * This registers package loggers filtering the output of the Mina FtpServer. + * The object holds static references to those loggers so that they stay around. + */ + private class MinaLoggerOverrider(logLevel: Level) { + @Suppress("unused") + private val ftpServerLogger = forceLogLevel("org.apache.ftpserver", logLevel) + + @Suppress("unused") + private val minaLogger = forceLogLevel("org.apache.mina", logLevel) + + private fun forceLogLevel(pkgName: String, logLevel: Level) = + (LoggerFactory.getLogger(pkgName) as Logger).apply { level = logLevel } + } +} diff --git a/app/src/main/java/org/pacien/tincapp/service/ConfigurationFtpService.kt b/app/src/main/java/org/pacien/tincapp/service/ConfigurationFtpService.kt deleted file mode 100644 index 2ea4a16..0000000 --- a/app/src/main/java/org/pacien/tincapp/service/ConfigurationFtpService.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon - * Copyright (C) 2017-2020 Pacien TRAN-GIRARD - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.pacien.tincapp.service - -import android.app.Service -import android.content.Intent -import android.os.IBinder -import androidx.databinding.ObservableBoolean -import ch.qos.logback.classic.Level -import ch.qos.logback.classic.Logger -import org.apache.ftpserver.FtpServer -import org.apache.ftpserver.FtpServerFactory -import org.apache.ftpserver.ftplet.* -import org.apache.ftpserver.listener.ListenerFactory -import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication -import org.apache.ftpserver.usermanager.impl.WritePermission -import org.pacien.tincapp.R -import org.pacien.tincapp.context.App -import org.pacien.tincapp.extensions.Java.defaultMessage -import org.slf4j.LoggerFactory -import java.io.IOException - -/** - * FTP server service allowing a remote and local user to access and modify configuration files in - * the application's context. - * - * @author pacien - */ -class ConfigurationFtpService : Service() { - companion object { - // Apache Mina FtpServer's INFO log level is actually VERBOSE. - // The object holds static references to those loggers so that they stay around. - @Suppress("unused") - private val MINA_FTP_LOGGER_OVERRIDER = MinaLoggerOverrider(Level.WARN) - - const val FTP_PORT = 65521 // tinc port `concat` FTP port - const val FTP_USERNAME = "tincapp" - val FTP_HOME_DIR = App.getContext().applicationInfo.dataDir!! - val FTP_PASSWORD = generateRandomString(8) - - val runningState = ObservableBoolean(false) - - private fun generateRandomString(length: Int): String { - val alphabet = ('a'..'z') + ('A'..'Z') + ('0'..'9') - return List(length) { alphabet.random() }.joinToString("") - } - } - - private val log by lazy { LoggerFactory.getLogger(this.javaClass)!! } - private var sftpServer: FtpServer? = null - - override fun onBind(intent: Intent): IBinder? = null // non-bindable service - - override fun onDestroy() { - sftpServer?.stop() - sftpServer = null - runningState.set(false) - log.info("Stopped FTP server") - super.onDestroy() - } - - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - val ftpUser = StaticFtpUser(FTP_USERNAME, FTP_PASSWORD, FTP_HOME_DIR, listOf(WritePermission())) - sftpServer = setupSingleUserServer(ftpUser).also { - try { - it.start() - runningState.set(true) - log.info("Started FTP server on port {}", FTP_PORT) - } catch (e: IOException) { - log.error("Could not start FTP server", e) - App.alert(R.string.notification_error_title_unable_to_start_ftp_server, e.defaultMessage()) - } - } - - return START_NOT_STICKY - } - - private fun setupSingleUserServer(ftpUser: User): FtpServer { - return FtpServerFactory() - .apply { addListener("default", ListenerFactory().apply { port = FTP_PORT }.createListener()) } - .apply { userManager = StaticFtpUserManager(listOf(ftpUser)) } - .createServer() - } - - private class StaticFtpUserManager(users: List) : UserManager { - private val userMap: Map = users.map { it.name to it }.toMap() - override fun getUserByName(username: String?): User? = userMap[username] - override fun getAllUserNames(): Array = userMap.keys.toTypedArray() - override fun doesExist(username: String?): Boolean = username in userMap - override fun delete(username: String?) = throw UnsupportedOperationException() - override fun save(user: User?) = throw UnsupportedOperationException() - override fun getAdminName(): String = throw UnsupportedOperationException() - override fun isAdmin(username: String?): Boolean = throw UnsupportedOperationException() - override fun authenticate(authentication: Authentication?): User = when (authentication) { - is UsernamePasswordAuthentication -> getUserByName(authentication.username).let { - if (it != null && authentication.password == it.password) it - else throw AuthenticationFailedException() - } - else -> throw IllegalArgumentException() - } - } - - private data class StaticFtpUser( - private val name: String, - private val password: String, - private val homeDirectory: String, - private val authorities: List - ) : User { - override fun getName(): String = name - override fun getPassword(): String = password - override fun getAuthorities(): List = authorities - override fun getAuthorities(clazz: Class): List = authorities.filter(clazz::isInstance) - override fun getMaxIdleTime(): Int = 0 // unlimited - override fun getEnabled(): Boolean = true - override fun getHomeDirectory(): String = homeDirectory - override fun authorize(request: AuthorizationRequest?): AuthorizationRequest? = - authorities.filter { it.canAuthorize(request) }.fold(request) { req, auth -> auth.authorize(req) } - } - - /** - * This registers package loggers filtering the output of the Mina FtpServer. - * The object holds static references to those loggers so that they stay around. - */ - private class MinaLoggerOverrider(logLevel: Level) { - @Suppress("unused") - private val ftpServerLogger = forceLogLevel("org.apache.ftpserver", logLevel) - - @Suppress("unused") - private val minaLogger = forceLogLevel("org.apache.mina", logLevel) - - private fun forceLogLevel(pkgName: String, logLevel: Level) = - (LoggerFactory.getLogger(pkgName) as Logger).apply { level = logLevel } - } -} diff --git a/app/src/main/res/drawable/ic_baseline_folder_open_primary_24dp.xml b/app/src/main/res/drawable/ic_baseline_folder_open_primary_24dp.xml new file mode 100644 index 0000000..da24678 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_folder_open_primary_24dp.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/app/src/main/res/layout/configure_activity.xml b/app/src/main/res/layout/configure_activity.xml index 2fde4a2..08f29af 100644 --- a/app/src/main/res/layout/configure_activity.xml +++ b/app/src/main/res/layout/configure_activity.xml @@ -30,11 +30,11 @@ + android:text="@string/configure_activity_title_configuration_access"/> diff --git a/app/src/main/res/layout/configure_tools_configuration_access_fragment.xml b/app/src/main/res/layout/configure_tools_configuration_access_fragment.xml new file mode 100644 index 0000000..24f3c36 --- /dev/null +++ b/app/src/main/res/layout/configure_tools_configuration_access_fragment.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/configure_tools_configuration_ftp_server_fragment.xml b/app/src/main/res/layout/configure_tools_configuration_ftp_server_fragment.xml deleted file mode 100644 index 24f3c36..0000000 --- a/app/src/main/res/layout/configure_tools_configuration_ftp_server_fragment.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cae7fe5..6c1a934 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,6 +75,10 @@ Invalid network configuration in network.conf:\n%1$s Could not decrypt private keys:\n%1$s + Configuration access + Configuration access server active + The configuration directory is accessible through FTP. + Configure Connect to network @@ -85,7 +89,7 @@ Invalid network name. Network configuration written. - Configuration server + Configuration access FTP access User: %1$s, password: %2$s, port: %3$d Not active -- cgit v1.2.3