Telegram backups refactoring stage 1
This commit is contained in:
@@ -7,12 +7,6 @@ import androidx.documentfile.provider.DocumentFile
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
@@ -21,7 +15,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class ExternalBackupStorage @Inject constructor(
|
||||
@@ -96,36 +89,3 @@ class ExternalBackupStorage @Inject constructor(
|
||||
return checkNotNull(root) { "Cannot obtain DocumentFile from $uri" }
|
||||
}
|
||||
}
|
||||
class TelegramBackupUploader @Inject constructor(private val settings: AppSettings) {
|
||||
|
||||
private val client = OkHttpClient()
|
||||
|
||||
suspend fun uploadBackupToTelegram(file: File) = withContext(Dispatchers.IO) {
|
||||
val botToken = "7455491254:AAGYJKgpP1DZN3d9KZfb8tvtIdaIMxUayXM"
|
||||
val chatId = settings.telegramChatId
|
||||
|
||||
if (botToken.isNullOrEmpty() || chatId.isNullOrEmpty()) {
|
||||
throw IllegalStateException("Telegram API key or chat ID not set in settings.")
|
||||
}
|
||||
|
||||
val mediaType = "application/zip".toMediaTypeOrNull()
|
||||
val requestBody = file.asRequestBody(mediaType)
|
||||
|
||||
val multipartBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("chat_id", chatId)
|
||||
.addFormDataPart("document", file.name, requestBody)
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://api.telegram.org/bot$botToken/sendDocument")
|
||||
.post(multipartBody)
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Failed to send backup to Telegram: ${response.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
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 androidx.annotation.UiContext
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
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 java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class TelegramBackupUploader @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
@BaseHttpClient private val client: OkHttpClient,
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
suspend fun checkTelegramBotApiKey(apiKey: String) {
|
||||
val request = Request.Builder()
|
||||
.url("https://api.telegram.org/bot$apiKey/getMe")
|
||||
.build()
|
||||
client.newCall(request).await().ensureSuccess().closeQuietly()
|
||||
sendMessageToTelegram(apiKey, context.getString(R.string.backup_tg_echo))
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeImplicitIntentLaunch")
|
||||
fun openTelegramBot(@UiContext context: Context) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendMessageToTelegram(apiKey: String, message: String) {
|
||||
val url = "https://api.telegram.org/bot$apiKey/sendMessage?chat_id=${requireChatId()}&text=$message"
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
client.newCall(request).await().ensureSuccess().closeQuietly()
|
||||
}
|
||||
|
||||
private fun requireChatId() = checkNotNull(settings.backupTelegramChatId) {
|
||||
"Telegram chat ID not set in settings"
|
||||
}
|
||||
}
|
||||
@@ -43,14 +43,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val connectivityManager = context.connectivityManager
|
||||
private val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
|
||||
var telegramChatId: String?
|
||||
get() = preferences.getString("telegram_chat_id", null)
|
||||
set(value) {
|
||||
preferences.edit().putString("telegram_chat_id", value).apply()
|
||||
}
|
||||
|
||||
var listMode: ListMode
|
||||
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
|
||||
@@ -497,6 +489,9 @@ 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 backupTelegramChatId: String?
|
||||
get() = prefs.getString(KEY_BACKUP_TG_CHAT, null)
|
||||
|
||||
val isReadingTimeEstimationEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READING_TIME, true)
|
||||
|
||||
@@ -724,6 +719,7 @@ 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"
|
||||
|
||||
// keys for non-persistent preferences
|
||||
const val KEY_APP_VERSION = "app_version"
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
@@ -18,19 +17,13 @@ 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.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveFile
|
||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -40,10 +33,10 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
|
||||
@Inject
|
||||
lateinit var backupStorage: ExternalBackupStorage
|
||||
|
||||
private val outputSelectCall = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocumentTree(),
|
||||
this,
|
||||
)
|
||||
@Inject
|
||||
lateinit var telegramBackupUploader: TelegramBackupUploader
|
||||
|
||||
private val outputSelectCall = registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), this)
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_backup_periodic)
|
||||
@@ -51,92 +44,10 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
|
||||
val openTelegramBotPreference = findPreference<Preference>("open_telegram_chat")
|
||||
|
||||
openTelegramBotPreference?.setOnPreferenceClickListener {
|
||||
openTelegramBot("kotatsu_backup_bot")
|
||||
telegramBackupUploader.openTelegramBot(it.context, "kotatsu_backup_bot")
|
||||
true
|
||||
}
|
||||
val checkApiButton = Preference(requireContext()).apply {
|
||||
key = "check_api_working"
|
||||
title = context.getString(R.string.api_telegram_check)
|
||||
summary = context.getString(R.string.api_check_desc)
|
||||
}
|
||||
|
||||
checkApiButton.setOnPreferenceClickListener {
|
||||
val apiKey = "7455491254:AAGYJKgpP1DZN3d9KZfb8tvtIdaIMxUayXM"
|
||||
if (apiKey.isNotEmpty()) {
|
||||
checkTelegramBotApiKey(apiKey)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
preferenceScreen.addPreference(checkApiButton)
|
||||
}
|
||||
private fun checkTelegramBotApiKey(apiKey: String) {
|
||||
val url = "https://api.telegram.org/bot$apiKey/getMe"
|
||||
|
||||
val client = OkHttpClient()
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
requireActivity().runOnUiThread {
|
||||
if (response.isSuccessful) {
|
||||
context?.let { sendMessageToTelegram(apiKey, it.getString(R.string.api_is_work)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
requireActivity().runOnUiThread {
|
||||
Toast.makeText(requireContext(), R.string.api_net_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
private fun openTelegramBot(botUsername: String) {
|
||||
try {
|
||||
val telegramIntent = Intent(Intent.ACTION_VIEW)
|
||||
telegramIntent.data = Uri.parse("https://t.me/$botUsername")
|
||||
telegramIntent.setPackage("org.telegram.messenger")
|
||||
startActivity(telegramIntent)
|
||||
} catch (e: Exception) {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/$botUsername"))
|
||||
startActivity(browserIntent)
|
||||
}
|
||||
}
|
||||
private fun sendMessageToTelegram(apiKey: String, message: String) {
|
||||
val chatId = settings.telegramChatId
|
||||
if (chatId.isNullOrEmpty()) {
|
||||
Toast.makeText(requireContext(), R.string.id_not_set, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val url = "https://api.telegram.org/bot$apiKey/sendMessage?chat_id=$chatId&text=$message"
|
||||
val client = OkHttpClient()
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
requireActivity().runOnUiThread {
|
||||
if (response.isSuccessful) {
|
||||
Toast.makeText(requireContext(), R.string.api_check_success, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(requireContext(), R.string.api_check_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
requireActivity().runOnUiThread {
|
||||
Toast.makeText(requireContext(), R.string.api_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.await
|
||||
import androidx.work.workDataOf
|
||||
import dagger.Reusable
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipOutput
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName
|
||||
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
||||
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import okhttp3.Response
|
||||
import java.io.File
|
||||
|
||||
@HiltWorker
|
||||
class PeriodicalBackupWorker @AssistedInject constructor(
|
||||
@Assisted appContext: Context,
|
||||
@Assisted params: WorkerParameters,
|
||||
private val repository: BackupRepository,
|
||||
private val settings: AppSettings,
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val resultData = workDataOf(DATA_TIMESTAMP to Date().time)
|
||||
val file = BackupZipOutput(applicationContext).use { backup ->
|
||||
backup.put(repository.createIndex())
|
||||
backup.put(repository.dumpHistory())
|
||||
backup.put(repository.dumpCategories())
|
||||
backup.put(repository.dumpFavourites())
|
||||
backup.put(repository.dumpBookmarks())
|
||||
backup.put(repository.dumpSources())
|
||||
backup.put(repository.dumpSettings())
|
||||
backup.finish()
|
||||
backup.file
|
||||
}
|
||||
val dirUri = settings.periodicalBackupOutput ?: return Result.success(resultData)
|
||||
val target = DocumentFile.fromTreeUri(applicationContext, dirUri)
|
||||
?.createFile("application/zip", file.nameWithoutExtension)
|
||||
?.uri ?: return Result.failure()
|
||||
applicationContext.contentResolver.openOutputStream(target, "wt")?.use { output ->
|
||||
file.inputStream().copyTo(output)
|
||||
} ?: return Result.failure()
|
||||
|
||||
val botToken = "7455491254:AAGYJKgpP1DZN3d9KZfb8tvtIdaIMxUayXM"
|
||||
val chatId = settings.telegramChatId ?: return Result.failure()
|
||||
|
||||
val success = sendBackupToTelegram(file, botToken, chatId)
|
||||
|
||||
file.deleteAwait()
|
||||
|
||||
return if (success) {
|
||||
Result.success(resultData)
|
||||
} else {
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
fun sendBackupToTelegram(file: File, botToken: String, chatId: String): Boolean {
|
||||
val client = OkHttpClient()
|
||||
val mediaType = "application/zip".toMediaTypeOrNull()
|
||||
val requestBody = file.asRequestBody(mediaType)
|
||||
|
||||
val multipartBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("chat_id", chatId)
|
||||
.addFormDataPart("document", file.name, requestBody)
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://api.telegram.org/bot$botToken/sendDocument")
|
||||
.post(multipartBody)
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
return response.isSuccessful
|
||||
}
|
||||
}
|
||||
|
||||
@Reusable
|
||||
class Scheduler @Inject constructor(
|
||||
private val workManager: WorkManager,
|
||||
private val settings: AppSettings,
|
||||
) : PeriodicWorkScheduler {
|
||||
|
||||
override suspend fun schedule() {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiresStorageNotLow(true)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
constraints.setRequiresDeviceIdle(true)
|
||||
}
|
||||
val request = PeriodicWorkRequestBuilder<PeriodicalBackupWorker>(
|
||||
settings.periodicalBackupFrequency,
|
||||
TimeUnit.DAYS,
|
||||
).setConstraints(constraints.build())
|
||||
.keepResultsForAtLeast(20, TimeUnit.DAYS)
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
workManager
|
||||
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||
.await()
|
||||
}
|
||||
|
||||
override suspend fun unschedule() {
|
||||
workManager
|
||||
.cancelUniqueWork(TAG)
|
||||
.await()
|
||||
}
|
||||
|
||||
override suspend fun isScheduled(): Boolean {
|
||||
return workManager
|
||||
.awaitUniqueWorkInfoByName(TAG)
|
||||
.any { !it.state.isFinished }
|
||||
}
|
||||
|
||||
suspend fun getLastSuccessfulBackup(): Date? {
|
||||
return workManager
|
||||
.awaitUniqueWorkInfoByName(TAG)
|
||||
.lastOrNull { x -> x.state == WorkInfo.State.SUCCEEDED }
|
||||
?.outputData
|
||||
?.getLong(DATA_TIMESTAMP, 0)
|
||||
?.let { if (it != 0L) Date(it) else null }
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val TAG = "backups"
|
||||
const val DATA_TIMESTAMP = "ts"
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@
|
||||
<string name="acra_password" translatable="false">kgpuhoNJpSsQDCwu</string>
|
||||
<string name="sync_authority_history" translatable="false">org.koitharu.kotatsu.history</string>
|
||||
<string name="sync_authority_favourites" translatable="false">org.koitharu.kotatsu.favourites</string>
|
||||
<string name="tg_backup_bot_token" translatable="false">7455491254:AAGYJKgpP1DZN3d9KZfb8tvtIdaIMxUayXM</string>
|
||||
<string name="tg_backup_bot_name" translatable="false">kotatsu_backup_bot</string>
|
||||
<string-array name="values_theme" translatable="false">
|
||||
<item>-1</item>
|
||||
<item>1</item>
|
||||
|
||||
@@ -778,12 +778,9 @@
|
||||
<!-- Button label, should be as short as possible -->
|
||||
<string name="incognito">Incognito</string>
|
||||
<string name="error_connection_reset">Connection reset by remote host</string>
|
||||
<string name="api_telegram_check">Check API work</string>
|
||||
<string name="api_check_desc">Click to check the operation of the Telegram Bot API</string>
|
||||
<string name="api_is_work">Kotatsu backup in Telegram is working!!</string>
|
||||
<string name="api_net_error">Network error! Check your Net</string>
|
||||
<string name="id_not_set">Chat ID is not set!</string>
|
||||
<string name="api_check_success">Success! Check Telegram Bot</string>
|
||||
<string name="api_check_error">OOPS! Something went wrong</string>
|
||||
<string name="api_error">Network error!</string>
|
||||
<string name="backup_tg_check">Check if API works</string>
|
||||
<string name="backup_tg_echo">Kotatsu backup in Telegram is working!!</string>
|
||||
<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>
|
||||
</resources>
|
||||
|
||||
@@ -50,15 +50,16 @@
|
||||
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"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
<Preference
|
||||
android:key="open_telegram_chat"
|
||||
android:title="Open Telegram Bot"
|
||||
android:summary="Press to open chat with Kotatsu Backup Bot" />
|
||||
<EditTextPreference
|
||||
android:key="telegram_chat_id"
|
||||
android:title="Telegram Chat ID"
|
||||
android:inputType="text"
|
||||
android:defaultValue=""
|
||||
android:summary="Enter the chat ID where backups should be sent" />
|
||||
android:summary="Press to open chat with Kotatsu Backup Bot"
|
||||
android:title="@string/open_telegram_bot" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
|
||||
Reference in New Issue
Block a user