Telegram backups refactoring stage 2
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.UiContext
|
||||
import androidx.core.net.toUri
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -14,12 +14,15 @@ import okhttp3.MultipartBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.network.BaseHttpClient
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
|
||||
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -31,56 +34,60 @@ class TelegramBackupUploader @Inject constructor(
|
||||
|
||||
private val botToken = context.getString(R.string.tg_backup_bot_token)
|
||||
|
||||
suspend fun uploadBackupToTelegram(file: File) = withContext(Dispatchers.IO) {
|
||||
|
||||
val mediaType = "application/zip".toMediaTypeOrNull()
|
||||
val requestBody = file.asRequestBody(mediaType)
|
||||
|
||||
suspend fun uploadBackup(file: File) = withContext(Dispatchers.IO) {
|
||||
val requestBody = file.asRequestBody("application/zip".toMediaTypeOrNull())
|
||||
val multipartBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("chat_id", requireChatId())
|
||||
.addFormDataPart("document", file.name, requestBody)
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://api.telegram.org/bot$botToken/sendDocument")
|
||||
.post(multipartBody)
|
||||
.build()
|
||||
|
||||
client.newCall(request).await().ensureSuccess().closeQuietly()
|
||||
client.newCall(request).await().consume()
|
||||
}
|
||||
|
||||
suspend fun checkTelegramBotApiKey(apiKey: String) {
|
||||
suspend fun sendTestMessage() {
|
||||
val request = Request.Builder()
|
||||
.url("https://api.telegram.org/bot$apiKey/getMe")
|
||||
.url("https://api.telegram.org/bot$botToken/getMe")
|
||||
.build()
|
||||
client.newCall(request).await().ensureSuccess().closeQuietly()
|
||||
sendMessageToTelegram(apiKey, context.getString(R.string.backup_tg_echo))
|
||||
client.newCall(request).await().consume()
|
||||
sendMessage(context.getString(R.string.backup_tg_echo))
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeImplicitIntentLaunch")
|
||||
fun openTelegramBot(@UiContext context: Context) {
|
||||
fun openBotInApp(@UiContext context: Context): Boolean {
|
||||
val botUsername = context.getString(R.string.tg_backup_bot_name)
|
||||
try {
|
||||
val telegramIntent = Intent(Intent.ACTION_VIEW)
|
||||
telegramIntent.data = Uri.parse("tg://resolve?domain=$botUsername")
|
||||
context.startActivity(telegramIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/$botUsername"))
|
||||
context.startActivity(browserIntent)
|
||||
}
|
||||
return runCatching {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, "tg://resolve?domain=$botUsername".toUri()))
|
||||
}.recoverCatching {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, "https://t.me/$botUsername".toUri()))
|
||||
}.onFailure {
|
||||
Toast.makeText(context, R.string.operation_not_supported, Toast.LENGTH_SHORT).show()
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
private suspend fun sendMessageToTelegram(apiKey: String, message: String) {
|
||||
val url = "https://api.telegram.org/bot$apiKey/sendMessage?chat_id=${requireChatId()}&text=$message"
|
||||
private suspend fun sendMessage(message: String) {
|
||||
val url = "https://api.telegram.org/bot$botToken/sendMessage?chat_id=${requireChatId()}&text=$message"
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
client.newCall(request).await().ensureSuccess().closeQuietly()
|
||||
client.newCall(request).await().consume()
|
||||
}
|
||||
|
||||
private fun requireChatId() = checkNotNull(settings.backupTelegramChatId) {
|
||||
"Telegram chat ID not set in settings"
|
||||
}
|
||||
|
||||
private fun Response.consume() {
|
||||
if (isSuccessful) {
|
||||
closeQuietly()
|
||||
return
|
||||
}
|
||||
val jo = parseJson()
|
||||
if (!jo.getBooleanOrDefault("ok", true)) {
|
||||
throw RuntimeException(jo.getStringOrNull("description"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,8 +489,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
get() = prefs.getString(KEY_BACKUP_PERIODICAL_OUTPUT, null)?.toUriOrNull()
|
||||
set(value) = prefs.edit { putString(KEY_BACKUP_PERIODICAL_OUTPUT, value?.toString()) }
|
||||
|
||||
val isBackupTelegramUploadEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_BACKUP_TG_ENABLED, false)
|
||||
|
||||
val backupTelegramChatId: String?
|
||||
get() = prefs.getString(KEY_BACKUP_TG_CHAT, null)
|
||||
get() = prefs.getString(KEY_BACKUP_TG_CHAT, null)?.takeUnless { it.isEmpty() }
|
||||
|
||||
val isReadingTimeEstimationEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READING_TIME, true)
|
||||
@@ -719,7 +722,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
||||
const val KEY_SOURCES_VERSION = "sources_version"
|
||||
const val KEY_QUICK_FILTER = "quick_filter"
|
||||
const val KEY_BACKUP_TG_CHAT = "telegram_chat_id"
|
||||
const val KEY_BACKUP_TG_ENABLED = "backup_periodic_tg_enabled"
|
||||
const val KEY_BACKUP_TG_CHAT = "backup_periodic_tg_chat_id"
|
||||
|
||||
// keys for non-persistent preferences
|
||||
const val KEY_APP_VERSION = "app_version"
|
||||
@@ -733,6 +737,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_PROXY_TEST = "proxy_test"
|
||||
const val KEY_OPEN_BROWSER = "open_browser"
|
||||
const val KEY_HANDLE_LINKS = "handle_links"
|
||||
const val KEY_BACKUP_TG_OPEN = "backup_periodic_tg_open"
|
||||
const val KEY_BACKUP_TG_TEST = "backup_periodic_tg_test"
|
||||
|
||||
// old keys are for migration only
|
||||
private const val KEY_IMAGES_PROXY_OLD = "images_proxy"
|
||||
|
||||
@@ -15,8 +15,10 @@ class PeriodicalBackupService : CoroutineIntentService() {
|
||||
|
||||
@Inject
|
||||
lateinit var externalBackupStorage: ExternalBackupStorage
|
||||
|
||||
@Inject
|
||||
lateinit var telegramBackupUploader: TelegramBackupUploader
|
||||
|
||||
@Inject
|
||||
lateinit var repository: BackupRepository
|
||||
|
||||
@@ -45,7 +47,9 @@ class PeriodicalBackupService : CoroutineIntentService() {
|
||||
}
|
||||
externalBackupStorage.put(output.file)
|
||||
externalBackupStorage.trim(settings.periodicalBackupMaxCount)
|
||||
telegramBackupUploader.uploadBackupToTelegram(output.file)
|
||||
if (settings.isBackupTelegramUploadEnabled) {
|
||||
telegramBackupUploader.uploadBackup(output.file)
|
||||
}
|
||||
} finally {
|
||||
output.file.delete()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
@@ -8,56 +7,58 @@ import android.text.format.DateUtils
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipOutput.Companion.DIR_BACKUPS
|
||||
import org.koitharu.kotatsu.core.backup.ExternalBackupStorage
|
||||
import org.koitharu.kotatsu.core.backup.TelegramBackupUploader
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveFile
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
import java.io.File
|
||||
import org.koitharu.kotatsu.settings.utils.EditTextFallbackSummaryProvider
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodic_backups),
|
||||
ActivityResultCallback<Uri?> {
|
||||
|
||||
@Inject
|
||||
lateinit var backupStorage: ExternalBackupStorage
|
||||
|
||||
@Inject
|
||||
lateinit var telegramBackupUploader: TelegramBackupUploader
|
||||
|
||||
private val viewModel by viewModels<PeriodicalBackupSettingsViewModel>()
|
||||
|
||||
private val outputSelectCall = registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), this)
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_backup_periodic)
|
||||
|
||||
val openTelegramBotPreference = findPreference<Preference>("open_telegram_chat")
|
||||
|
||||
openTelegramBotPreference?.setOnPreferenceClickListener {
|
||||
telegramBackupUploader.openTelegramBot(it.context, "kotatsu_backup_bot")
|
||||
true
|
||||
}
|
||||
findPreference<EditTextPreference>(AppSettings.KEY_BACKUP_TG_CHAT)?.summaryProvider =
|
||||
EditTextFallbackSummaryProvider(R.string.telegram_chat_id_summary)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
bindOutputSummary()
|
||||
bindLastBackupInfo()
|
||||
viewModel.lastBackupDate.observe(viewLifecycleOwner, ::bindLastBackupInfo)
|
||||
viewModel.backupsDirectory.observe(viewLifecycleOwner, ::bindOutputSummary)
|
||||
viewModel.isTelegramCheckLoading.observe(viewLifecycleOwner) {
|
||||
findPreference<Preference>(AppSettings.KEY_BACKUP_TG_TEST)?.isEnabled = !it
|
||||
}
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(listView, this))
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
AppSettings.KEY_BACKUP_PERIODICAL_OUTPUT -> outputSelectCall.tryLaunch(null)
|
||||
AppSettings.KEY_BACKUP_TG_OPEN -> telegramBackupUploader.openBotInApp(preference.context)
|
||||
AppSettings.KEY_BACKUP_TG_TEST -> {
|
||||
viewModel.checkTelegram()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
@@ -67,45 +68,28 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
|
||||
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
context?.contentResolver?.takePersistableUriPermission(result, takeFlags)
|
||||
settings.periodicalBackupDirectory = result
|
||||
bindOutputSummary()
|
||||
bindLastBackupInfo()
|
||||
viewModel.updateSummaryData()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindOutputSummary() {
|
||||
private fun bindOutputSummary(path: String?) {
|
||||
val preference = findPreference<Preference>(AppSettings.KEY_BACKUP_PERIODICAL_OUTPUT) ?: return
|
||||
viewLifecycleScope.launch {
|
||||
preference.summary = withContext(Dispatchers.Default) {
|
||||
val value = settings.periodicalBackupDirectory
|
||||
value?.toUserFriendlyString(preference.context) ?: preference.context.run {
|
||||
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
|
||||
}.path
|
||||
}
|
||||
preference.summary = when (path) {
|
||||
null -> getString(R.string.invalid_value_message)
|
||||
"" -> null
|
||||
else -> path
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindLastBackupInfo() {
|
||||
private fun bindLastBackupInfo(lastBackupDate: Date?) {
|
||||
val preference = findPreference<Preference>(AppSettings.KEY_BACKUP_PERIODICAL_LAST) ?: return
|
||||
viewLifecycleScope.launch {
|
||||
val lastDate = withContext(Dispatchers.Default) {
|
||||
backupStorage.getLastBackupDate()
|
||||
}
|
||||
preference.summary = lastDate?.let {
|
||||
preference.context.getString(
|
||||
R.string.last_successful_backup,
|
||||
DateUtils.getRelativeTimeSpanString(it.time),
|
||||
)
|
||||
}
|
||||
preference.isVisible = lastDate != null
|
||||
preference.summary = lastBackupDate?.let {
|
||||
preference.context.getString(
|
||||
R.string.last_successful_backup,
|
||||
DateUtils.getRelativeTimeSpanString(it.time),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Uri.toUserFriendlyString(context: Context): String {
|
||||
val df = DocumentFile.fromTreeUri(context, this)
|
||||
if (df?.canWrite() != true) {
|
||||
return context.getString(R.string.invalid_value_message)
|
||||
}
|
||||
return resolveFile(context)?.path ?: toString()
|
||||
preference.isVisible = lastBackupDate != null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipOutput.Companion.DIR_BACKUPS
|
||||
import org.koitharu.kotatsu.core.backup.ExternalBackupStorage
|
||||
import org.koitharu.kotatsu.core.backup.TelegramBackupUploader
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveFile
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class PeriodicalBackupSettingsViewModel @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
private val telegramUploader: TelegramBackupUploader,
|
||||
private val backupStorage: ExternalBackupStorage,
|
||||
@ApplicationContext private val appContext: Context,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val lastBackupDate = MutableStateFlow<Date?>(null)
|
||||
val backupsDirectory = MutableStateFlow<String?>("")
|
||||
val isTelegramCheckLoading = MutableStateFlow(false)
|
||||
|
||||
init {
|
||||
updateSummaryData()
|
||||
}
|
||||
|
||||
fun checkTelegram() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
isTelegramCheckLoading.value = true
|
||||
telegramUploader.sendTestMessage()
|
||||
} finally {
|
||||
isTelegramCheckLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSummaryData() {
|
||||
updateBackupsDirectory()
|
||||
updateLastBackupDate()
|
||||
}
|
||||
|
||||
private fun updateBackupsDirectory() = launchJob(Dispatchers.Default) {
|
||||
val dir = settings.periodicalBackupDirectory
|
||||
backupsDirectory.value = if (dir != null) {
|
||||
dir.toUserFriendlyString()
|
||||
} else {
|
||||
(appContext.getExternalFilesDir(DIR_BACKUPS) ?: File(appContext.filesDir, DIR_BACKUPS)).path
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLastBackupDate() = launchJob(Dispatchers.Default) {
|
||||
lastBackupDate.value = backupStorage.getLastBackupDate()
|
||||
}
|
||||
|
||||
private fun Uri.toUserFriendlyString(): String? {
|
||||
val df = DocumentFile.fromTreeUri(appContext, this)
|
||||
if (df?.canWrite() != true) {
|
||||
return null
|
||||
}
|
||||
return resolveFile(appContext)?.path ?: toString()
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,15 @@ package org.koitharu.kotatsu.settings.utils
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||
|
||||
class EditTextDefaultSummaryProvider(
|
||||
private val defaultValue: String
|
||||
private val defaultValue: String,
|
||||
) : Preference.SummaryProvider<EditTextPreference> {
|
||||
|
||||
override fun provideSummary(preference: EditTextPreference): CharSequence {
|
||||
val text = preference.text
|
||||
return if (text.isNullOrEmpty()) {
|
||||
preference.context.getString(R.string.default_s, defaultValue)
|
||||
} else {
|
||||
text
|
||||
}
|
||||
override fun provideSummary(
|
||||
preference: EditTextPreference,
|
||||
): CharSequence = preference.text.ifNullOrEmpty {
|
||||
preference.context.getString(R.string.default_s, defaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.koitharu.kotatsu.settings.utils
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||
|
||||
class EditTextFallbackSummaryProvider(
|
||||
@StringRes private val fallbackResId: Int,
|
||||
) : Preference.SummaryProvider<EditTextPreference> {
|
||||
|
||||
override fun provideSummary(
|
||||
preference: EditTextPreference,
|
||||
): CharSequence = preference.text.ifNullOrEmpty {
|
||||
preference.context.getString(fallbackResId)
|
||||
}
|
||||
}
|
||||
@@ -783,4 +783,8 @@
|
||||
<string name="backup_tg_id_not_set">Chat ID is not set</string>
|
||||
<string name="telegram_chat_id">Telegram chat ID</string>
|
||||
<string name="open_telegram_bot">Open the Telegram bot</string>
|
||||
<string name="send_backups_telegram">Send backups in Telegram</string>
|
||||
<string name="test_connection">Test connection</string>
|
||||
<string name="telegram_chat_id_summary">Enter the chat ID where backups should be sent</string>
|
||||
<string name="open_telegram_bot_summary">Press to open chat with Kotatsu Backup Bot</string>
|
||||
</resources>
|
||||
|
||||
@@ -47,19 +47,32 @@
|
||||
android:key="backup_periodic_last"
|
||||
android:persistent="false"
|
||||
android:selectable="false"
|
||||
app:allowDividerAbove="true"
|
||||
app:isPreferenceVisible="false" />
|
||||
|
||||
<EditTextPreference
|
||||
android:inputType="text"
|
||||
android:key="telegram_chat_id"
|
||||
android:summary="Enter the chat ID where backups should be sent"
|
||||
android:title="@string/telegram_chat_id"
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:dependency="backup_periodic"
|
||||
android:key="backup_periodic_tg_enabled"
|
||||
android:title="@string/send_backups_telegram"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
<EditTextPreference
|
||||
android:dependency="backup_periodic_tg_enabled"
|
||||
android:inputType="text"
|
||||
android:key="backup_periodic_tg_chat_id"
|
||||
android:title="@string/telegram_chat_id" />
|
||||
|
||||
<Preference
|
||||
android:key="open_telegram_chat"
|
||||
android:summary="Press to open chat with Kotatsu Backup Bot"
|
||||
android:dependency="backup_periodic_tg_enabled"
|
||||
android:key="backup_periodic_tg_open"
|
||||
android:persistent="false"
|
||||
android:summary="@string/open_telegram_bot_summary"
|
||||
android:title="@string/open_telegram_bot" />
|
||||
|
||||
<Preference
|
||||
android:dependency="backup_periodic_tg_enabled"
|
||||
android:key="backup_periodic_tg_test"
|
||||
android:persistent="false"
|
||||
android:title="@string/test_connection" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<Preference
|
||||
android:key="proxy_test"
|
||||
android:persistent="false"
|
||||
android:title="Test connection"
|
||||
android:title="@string/test_connection"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
Reference in New Issue
Block a user