Merge branch 'MAPKOBKA135-devel' into devel
This commit is contained in:
@@ -7,6 +7,12 @@ import androidx.documentfile.provider.DocumentFile
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runInterruptible
|
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.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import okio.source
|
import okio.source
|
||||||
@@ -15,6 +21,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
|||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ExternalBackupStorage @Inject constructor(
|
class ExternalBackupStorage @Inject constructor(
|
||||||
@@ -89,3 +96,36 @@ class ExternalBackupStorage @Inject constructor(
|
|||||||
return checkNotNull(root) { "Cannot obtain DocumentFile from $uri" }
|
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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
private val connectivityManager = context.connectivityManager
|
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
|
var listMode: ListMode
|
||||||
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
|
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||||
import org.koitharu.kotatsu.core.backup.BackupZipOutput
|
import org.koitharu.kotatsu.core.backup.BackupZipOutput
|
||||||
import org.koitharu.kotatsu.core.backup.ExternalBackupStorage
|
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.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
|
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,7 +15,8 @@ class PeriodicalBackupService : CoroutineIntentService() {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var externalBackupStorage: ExternalBackupStorage
|
lateinit var externalBackupStorage: ExternalBackupStorage
|
||||||
|
@Inject
|
||||||
|
lateinit var telegramBackupUploader: TelegramBackupUploader
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var repository: BackupRepository
|
lateinit var repository: BackupRepository
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ class PeriodicalBackupService : CoroutineIntentService() {
|
|||||||
}
|
}
|
||||||
externalBackupStorage.put(output.file)
|
externalBackupStorage.put(output.file)
|
||||||
externalBackupStorage.trim(settings.periodicalBackupMaxCount)
|
externalBackupStorage.trim(settings.periodicalBackupMaxCount)
|
||||||
|
telegramBackupUploader.uploadBackupToTelegram(output.file)
|
||||||
} finally {
|
} finally {
|
||||||
output.file.delete()
|
output.file.delete()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultCallback
|
import androidx.activity.result.ActivityResultCallback
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
@@ -22,7 +23,14 @@ import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
|||||||
import org.koitharu.kotatsu.core.util.ext.resolveFile
|
import org.koitharu.kotatsu.core.util.ext.resolveFile
|
||||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
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.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -39,7 +47,96 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
|
|||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
addPreferencesFromResource(R.xml.pref_backup_periodic)
|
addPreferencesFromResource(R.xml.pref_backup_periodic)
|
||||||
|
|
||||||
|
val openTelegramBotPreference = findPreference<Preference>("open_telegram_chat")
|
||||||
|
|
||||||
|
openTelegramBotPreference?.setOnPreferenceClickListener {
|
||||||
|
openTelegramBot("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?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
@@ -100,3 +197,4 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
|
|||||||
return resolveFile(context)?.path ?: toString()
|
return resolveFile(context)?.path ?: toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -671,4 +671,4 @@
|
|||||||
<string name="percent_read">Procenta přečtených</string>
|
<string name="percent_read">Procenta přečtených</string>
|
||||||
<string name="chapters_read">Kapitol přečtených</string>
|
<string name="chapters_read">Kapitol přečtených</string>
|
||||||
<string name="chapters_left">Kapitol zbývajících</string>
|
<string name="chapters_left">Kapitol zbývajících</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -778,4 +778,12 @@
|
|||||||
<!-- Button label, should be as short as possible -->
|
<!-- Button label, should be as short as possible -->
|
||||||
<string name="incognito">Incognito</string>
|
<string name="incognito">Incognito</string>
|
||||||
<string name="error_connection_reset">Connection reset by remote host</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>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -50,4 +50,15 @@
|
|||||||
app:allowDividerAbove="true"
|
app:allowDividerAbove="true"
|
||||||
app:isPreferenceVisible="false" />
|
app:isPreferenceVisible="false" />
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
|
||||||
</androidx.preference.PreferenceScreen>
|
</androidx.preference.PreferenceScreen>
|
||||||
|
|||||||
Reference in New Issue
Block a user