Add Telegram backup feature
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.CheckResult
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
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.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.network.BaseHttpClient
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
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
|
||||
|
||||
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 uploadBackup(file: File) {
|
||||
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(urlOf("sendDocument").build())
|
||||
.post(multipartBody)
|
||||
.build()
|
||||
client.newCall(request).await().consume()
|
||||
}
|
||||
|
||||
suspend fun sendTestMessage() {
|
||||
val request = Request.Builder()
|
||||
.url(urlOf("getMe").build())
|
||||
.build()
|
||||
client.newCall(request).await().consume()
|
||||
sendMessage(context.getString(R.string.backup_tg_echo))
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
fun openBotInApp(router: AppRouter): Boolean {
|
||||
val botUsername = context.getString(R.string.tg_backup_bot_name)
|
||||
return router.openExternalBrowser("tg://resolve?domain=$botUsername") ||
|
||||
router.openExternalBrowser("https://t.me/$botUsername")
|
||||
}
|
||||
|
||||
private suspend fun sendMessage(message: String) {
|
||||
val url = urlOf("sendMessage")
|
||||
.addQueryParameter("chat_id", requireChatId())
|
||||
.addQueryParameter("text", message)
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun urlOf(method: String) = HttpUrl.Builder()
|
||||
.scheme("https")
|
||||
.host("api.telegram.org")
|
||||
.addPathSegment("bot$botToken")
|
||||
.addPathSegment(method)
|
||||
}
|
||||
@@ -490,6 +490,12 @@ 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)?.nullIfEmpty()
|
||||
|
||||
val isReadingTimeEstimationEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READING_TIME, true)
|
||||
|
||||
@@ -717,6 +723,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_SOURCES_VERSION = "sources_version"
|
||||
const val KEY_SOURCES_ENABLED_ALL = "sources_enabled_all"
|
||||
const val KEY_QUICK_FILTER = "quick_filter"
|
||||
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"
|
||||
@@ -730,6 +738,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"
|
||||
const val KEY_CLEAR_MANGA_DATA = "manga_data_clear"
|
||||
|
||||
// old keys are for migration only
|
||||
|
||||
@@ -5,6 +5,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipOutput
|
||||
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.CoroutineIntentService
|
||||
import javax.inject.Inject
|
||||
@@ -15,6 +16,9 @@ class PeriodicalBackupService : CoroutineIntentService() {
|
||||
@Inject
|
||||
lateinit var externalBackupStorage: ExternalBackupStorage
|
||||
|
||||
@Inject
|
||||
lateinit var telegramBackupUploader: TelegramBackupUploader
|
||||
|
||||
@Inject
|
||||
lateinit var repository: BackupRepository
|
||||
|
||||
@@ -43,6 +47,9 @@ class PeriodicalBackupService : CoroutineIntentService() {
|
||||
}
|
||||
externalBackupStorage.put(output.file)
|
||||
externalBackupStorage.trim(settings.periodicalBackupMaxCount)
|
||||
if (settings.isBackupTelegramUploadEnabled) {
|
||||
telegramBackupUploader.uploadBackup(output.file)
|
||||
}
|
||||
} finally {
|
||||
output.file.delete()
|
||||
}
|
||||
|
||||
@@ -8,28 +8,38 @@ import android.view.View
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.backup.TelegramBackupUploader
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
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.settings.utils.EditTextFallbackSummaryProvider
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodic_backups),
|
||||
ActivityResultCallback<Uri?> {
|
||||
|
||||
@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)
|
||||
findPreference<EditTextPreference>(AppSettings.KEY_BACKUP_TG_CHAT)?.summaryProvider =
|
||||
EditTextFallbackSummaryProvider(R.string.telegram_chat_id_summary)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -37,11 +47,19 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
|
||||
viewModel.lastBackupDate.observe(viewLifecycleOwner, ::bindLastBackupInfo)
|
||||
viewModel.backupsDirectory.observe(viewLifecycleOwner, ::bindOutputSummary)
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(listView, this))
|
||||
viewModel.isTelegramCheckLoading.observe(viewLifecycleOwner) {
|
||||
findPreference<Preference>(AppSettings.KEY_BACKUP_TG_TEST)?.isEnabled = !it
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
val result = when (preference.key) {
|
||||
AppSettings.KEY_BACKUP_PERIODICAL_OUTPUT -> outputSelectCall.tryLaunch(null)
|
||||
AppSettings.KEY_BACKUP_TG_OPEN -> telegramBackupUploader.openBotInApp(router)
|
||||
AppSettings.KEY_BACKUP_TG_TEST -> {
|
||||
viewModel.checkTelegram()
|
||||
true
|
||||
}
|
||||
else -> return super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
if (!result) {
|
||||
|
||||
@@ -7,12 +7,15 @@ 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.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.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveFile
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
@@ -21,18 +24,32 @@ 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)
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
|
||||
init {
|
||||
updateSummaryData()
|
||||
}
|
||||
|
||||
fun checkTelegram() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
isTelegramCheckLoading.value = true
|
||||
telegramUploader.sendTestMessage()
|
||||
onActionDone.call(ReversibleAction(R.string.connection_ok, null))
|
||||
} finally {
|
||||
isTelegramCheckLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSummaryData() {
|
||||
updateBackupsDirectory()
|
||||
updateLastBackupDate()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -48,5 +48,31 @@
|
||||
android:persistent="false"
|
||||
android:selectable="false"
|
||||
app:isPreferenceVisible="false" />
|
||||
|
||||
<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: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>
|
||||
|
||||
Reference in New Issue
Block a user