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()
|
get() = prefs.getString(KEY_BACKUP_PERIODICAL_OUTPUT, null)?.toUriOrNull()
|
||||||
set(value) = prefs.edit { putString(KEY_BACKUP_PERIODICAL_OUTPUT, value?.toString()) }
|
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
|
val isReadingTimeEstimationEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_READING_TIME, true)
|
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_VERSION = "sources_version"
|
||||||
const val KEY_SOURCES_ENABLED_ALL = "sources_enabled_all"
|
const val KEY_SOURCES_ENABLED_ALL = "sources_enabled_all"
|
||||||
const val KEY_QUICK_FILTER = "quick_filter"
|
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
|
// keys for non-persistent preferences
|
||||||
const val KEY_APP_VERSION = "app_version"
|
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_PROXY_TEST = "proxy_test"
|
||||||
const val KEY_OPEN_BROWSER = "open_browser"
|
const val KEY_OPEN_BROWSER = "open_browser"
|
||||||
const val KEY_HANDLE_LINKS = "handle_links"
|
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"
|
const val KEY_CLEAR_MANGA_DATA = "manga_data_clear"
|
||||||
|
|
||||||
// old keys are for migration only
|
// 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.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
|
||||||
@@ -15,6 +16,9 @@ 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 +47,9 @@ class PeriodicalBackupService : CoroutineIntentService() {
|
|||||||
}
|
}
|
||||||
externalBackupStorage.put(output.file)
|
externalBackupStorage.put(output.file)
|
||||||
externalBackupStorage.trim(settings.periodicalBackupMaxCount)
|
externalBackupStorage.trim(settings.periodicalBackupMaxCount)
|
||||||
|
if (settings.isBackupTelegramUploadEnabled) {
|
||||||
|
telegramBackupUploader.uploadBackup(output.file)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
output.file.delete()
|
output.file.delete()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,28 +8,38 @@ import android.view.View
|
|||||||
import androidx.activity.result.ActivityResultCallback
|
import androidx.activity.result.ActivityResultCallback
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.preference.EditTextPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.R
|
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.exceptions.resolve.SnackbarErrorObserver
|
||||||
|
import org.koitharu.kotatsu.core.nav.router
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||||
import org.koitharu.kotatsu.core.util.ext.observe
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||||
|
import org.koitharu.kotatsu.settings.utils.EditTextFallbackSummaryProvider
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodic_backups),
|
class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodic_backups),
|
||||||
ActivityResultCallback<Uri?> {
|
ActivityResultCallback<Uri?> {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var telegramBackupUploader: TelegramBackupUploader
|
||||||
|
|
||||||
private val viewModel by viewModels<PeriodicalBackupSettingsViewModel>()
|
private val viewModel by viewModels<PeriodicalBackupSettingsViewModel>()
|
||||||
|
|
||||||
private val outputSelectCall = registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), this)
|
private val outputSelectCall = registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), this)
|
||||||
|
|
||||||
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)
|
||||||
|
findPreference<EditTextPreference>(AppSettings.KEY_BACKUP_TG_CHAT)?.summaryProvider =
|
||||||
|
EditTextFallbackSummaryProvider(R.string.telegram_chat_id_summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@@ -37,11 +47,19 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
|
|||||||
viewModel.lastBackupDate.observe(viewLifecycleOwner, ::bindLastBackupInfo)
|
viewModel.lastBackupDate.observe(viewLifecycleOwner, ::bindLastBackupInfo)
|
||||||
viewModel.backupsDirectory.observe(viewLifecycleOwner, ::bindOutputSummary)
|
viewModel.backupsDirectory.observe(viewLifecycleOwner, ::bindOutputSummary)
|
||||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(listView, this))
|
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 {
|
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||||
val result = when (preference.key) {
|
val result = when (preference.key) {
|
||||||
AppSettings.KEY_BACKUP_PERIODICAL_OUTPUT -> outputSelectCall.tryLaunch(null)
|
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)
|
else -> return super.onPreferenceTreeClick(preference)
|
||||||
}
|
}
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
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.BackupZipOutput.Companion.DIR_BACKUPS
|
||||||
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.BaseViewModel
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
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 org.koitharu.kotatsu.core.util.ext.resolveFile
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -21,18 +24,32 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class PeriodicalBackupSettingsViewModel @Inject constructor(
|
class PeriodicalBackupSettingsViewModel @Inject constructor(
|
||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
|
private val telegramUploader: TelegramBackupUploader,
|
||||||
private val backupStorage: ExternalBackupStorage,
|
private val backupStorage: ExternalBackupStorage,
|
||||||
@ApplicationContext private val appContext: Context,
|
@ApplicationContext private val appContext: Context,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val lastBackupDate = MutableStateFlow<Date?>(null)
|
val lastBackupDate = MutableStateFlow<Date?>(null)
|
||||||
val backupsDirectory = MutableStateFlow<String?>("")
|
val backupsDirectory = MutableStateFlow<String?>("")
|
||||||
|
val isTelegramCheckLoading = MutableStateFlow(false)
|
||||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
updateSummaryData()
|
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() {
|
fun updateSummaryData() {
|
||||||
updateBackupsDirectory()
|
updateBackupsDirectory()
|
||||||
updateLastBackupDate()
|
updateLastBackupDate()
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
<string name="acra_password" translatable="false">kgpuhoNJpSsQDCwu</string>
|
<string name="acra_password" translatable="false">kgpuhoNJpSsQDCwu</string>
|
||||||
<string name="sync_authority_history" translatable="false">org.koitharu.kotatsu.history</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="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">
|
<string-array name="values_theme" translatable="false">
|
||||||
<item>-1</item>
|
<item>-1</item>
|
||||||
<item>1</item>
|
<item>1</item>
|
||||||
|
|||||||
@@ -48,5 +48,31 @@
|
|||||||
android:persistent="false"
|
android:persistent="false"
|
||||||
android:selectable="false"
|
android:selectable="false"
|
||||||
app:isPreferenceVisible="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>
|
</androidx.preference.PreferenceScreen>
|
||||||
|
|||||||
Reference in New Issue
Block a user