Periodical backup improvements
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.backups.ui
|
package org.koitharu.kotatsu.backups.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.app.NotificationChannelCompat
|
import androidx.core.app.NotificationChannelCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@@ -27,24 +28,13 @@ abstract class BaseBackupRestoreService : CoroutineIntentService() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
notificationManager = NotificationManagerCompat.from(applicationContext)
|
notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||||
createNotificationChannel()
|
createNotificationChannel(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun IntentJobContext.onError(error: Throwable) {
|
override fun IntentJobContext.onError(error: Throwable) {
|
||||||
showResultNotification(null, CompositeResult.failure(error))
|
showResultNotification(null, CompositeResult.failure(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNotificationChannel() {
|
|
||||||
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_HIGH)
|
|
||||||
.setName(getString(R.string.backup_restore))
|
|
||||||
.setShowBadge(true)
|
|
||||||
.setVibrationEnabled(false)
|
|
||||||
.setSound(null, null)
|
|
||||||
.setLightsEnabled(false)
|
|
||||||
.build()
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun IntentJobContext.showResultNotification(
|
protected fun IntentJobContext.showResultNotification(
|
||||||
fileUri: Uri?,
|
fileUri: Uri?,
|
||||||
result: CompositeResult,
|
result: CompositeResult,
|
||||||
@@ -128,8 +118,19 @@ abstract class BaseBackupRestoreService : CoroutineIntentService() {
|
|||||||
.setBigContentTitle(title),
|
.setBigContentTitle(title),
|
||||||
)
|
)
|
||||||
|
|
||||||
protected companion object {
|
companion object {
|
||||||
|
|
||||||
const val CHANNEL_ID = "backup_restore"
|
const val CHANNEL_ID = "backup_restore"
|
||||||
|
|
||||||
|
fun createNotificationChannel(context: Context) {
|
||||||
|
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_HIGH)
|
||||||
|
.setName(context.getString(R.string.backup_restore))
|
||||||
|
.setShowBadge(true)
|
||||||
|
.setVibrationEnabled(false)
|
||||||
|
.setSound(null, null)
|
||||||
|
.setLightsEnabled(false)
|
||||||
|
.build()
|
||||||
|
NotificationManagerCompat.from(context).createNotificationChannel(channel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
package org.koitharu.kotatsu.backups.ui.periodical
|
package org.koitharu.kotatsu.backups.ui.periodical
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.app.PendingIntentCompat
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.backups.data.BackupRepository
|
import org.koitharu.kotatsu.backups.data.BackupRepository
|
||||||
import org.koitharu.kotatsu.backups.domain.BackupUtils
|
import org.koitharu.kotatsu.backups.domain.BackupUtils
|
||||||
import org.koitharu.kotatsu.backups.domain.ExternalBackupStorage
|
import org.koitharu.kotatsu.backups.domain.ExternalBackupStorage
|
||||||
|
import org.koitharu.kotatsu.backups.ui.BaseBackupRestoreService
|
||||||
|
import org.koitharu.kotatsu.core.ErrorReporterReceiver
|
||||||
|
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||||
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 org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -48,5 +57,49 @@ class PeriodicalBackupService : CoroutineIntentService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun IntentJobContext.onError(error: Throwable) = Unit
|
override fun IntentJobContext.onError(error: Throwable) {
|
||||||
|
if (!applicationContext.checkNotificationPermission(CHANNEL_ID)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
BaseBackupRestoreService.createNotificationChannel(applicationContext)
|
||||||
|
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setDefaults(0)
|
||||||
|
.setSilent(true)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
val title = getString(R.string.periodic_backups)
|
||||||
|
val message = getString(
|
||||||
|
R.string.inline_preference_pattern,
|
||||||
|
getString(R.string.packup_creation_failed),
|
||||||
|
error.getDisplayMessage(resources),
|
||||||
|
)
|
||||||
|
notification
|
||||||
|
.setContentText(message)
|
||||||
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
|
.setStyle(
|
||||||
|
NotificationCompat.BigTextStyle()
|
||||||
|
.bigText(message)
|
||||||
|
.setSummaryText(getString(R.string.packup_creation_failed))
|
||||||
|
.setBigContentTitle(title),
|
||||||
|
)
|
||||||
|
ErrorReporterReceiver.getNotificationAction(applicationContext, error, startId, TAG)?.let { action ->
|
||||||
|
notification.addAction(action)
|
||||||
|
}
|
||||||
|
notification.setContentIntent(
|
||||||
|
PendingIntentCompat.getActivity(
|
||||||
|
applicationContext,
|
||||||
|
0,
|
||||||
|
AppRouter.periodicBackupSettingsIntent(applicationContext),
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
NotificationManagerCompat.from(applicationContext).notify(TAG, startId, notification.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val CHANNEL_ID = BaseBackupRestoreService.CHANNEL_ID
|
||||||
|
const val TAG = "periodical_backup"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.os.Bundle
|
|||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.result.ActivityResultCallback
|
import androidx.activity.result.ActivityResultCallback
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.preference.EditTextPreference
|
import androidx.preference.EditTextPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
@@ -84,6 +85,13 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
|
|||||||
"" -> null
|
"" -> null
|
||||||
else -> path
|
else -> path
|
||||||
}
|
}
|
||||||
|
preference.icon = if (path == null) {
|
||||||
|
ContextCompat.getDrawable(preference.context, R.drawable.ic_alert_outline)?.also {
|
||||||
|
it.setTint(ContextCompat.getColor(preference.context, R.color.warning))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindLastBackupInfo(lastBackupDate: Date?) {
|
private fun bindLastBackupInfo(lastBackupDate: Date?) {
|
||||||
|
|||||||
@@ -741,6 +741,10 @@ class AppRouter private constructor(
|
|||||||
Intent(context, SettingsActivity::class.java)
|
Intent(context, SettingsActivity::class.java)
|
||||||
.setAction(ACTION_TRACKER)
|
.setAction(ACTION_TRACKER)
|
||||||
|
|
||||||
|
fun periodicBackupSettingsIntent(context: Context) =
|
||||||
|
Intent(context, SettingsActivity::class.java)
|
||||||
|
.setAction(ACTION_PERIODIC_BACKUP)
|
||||||
|
|
||||||
fun proxySettingsIntent(context: Context) =
|
fun proxySettingsIntent(context: Context) =
|
||||||
Intent(context, SettingsActivity::class.java)
|
Intent(context, SettingsActivity::class.java)
|
||||||
.setAction(ACTION_PROXY)
|
.setAction(ACTION_PROXY)
|
||||||
@@ -825,6 +829,7 @@ class AppRouter private constructor(
|
|||||||
const val ACTION_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES"
|
const val ACTION_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES"
|
||||||
const val ACTION_SUGGESTIONS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS"
|
const val ACTION_SUGGESTIONS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS"
|
||||||
const val ACTION_TRACKER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_TRACKER"
|
const val ACTION_TRACKER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_TRACKER"
|
||||||
|
const val ACTION_PERIODIC_BACKUP = "${BuildConfig.APPLICATION_ID}.action.MANAGE_PERIODIC_BACKUP"
|
||||||
|
|
||||||
private const val ACCOUNT_KEY = "account"
|
private const val ACCOUNT_KEY = "account"
|
||||||
private const val ACTION_ACCOUNT_SYNC_SETTINGS = "android.settings.ACCOUNT_SYNC_SETTINGS"
|
private const val ACTION_ACCOUNT_SYNC_SETTINGS = "android.settings.ACCOUNT_SYNC_SETTINGS"
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import androidx.activity.result.contract.ActivityResultContract
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/77555641/saf-no-activity-found-to-handle-intent-android-intent-action-open-document-tr
|
// https://stackoverflow.com/questions/77555641/saf-no-activity-found-to-handle-intent-android-intent-action-open-document-tr
|
||||||
class OpenDocumentTreeHelper(
|
class OpenDocumentTreeHelper(
|
||||||
@@ -28,38 +27,42 @@ class OpenDocumentTreeHelper(
|
|||||||
callback,
|
callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val pickFileTreeLauncherQ = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
private val pickFileTreeLauncherPrimaryStorage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
activityResultCaller.registerForActivityResult(OpenDocumentTreeContractQ(flags), callback)
|
activityResultCaller.registerForActivityResult(OpenDocumentTreeContractPrimaryStorage(flags), callback)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
private val pickFileTreeLauncherLegacy = activityResultCaller.registerForActivityResult(
|
private val pickFileTreeLauncherDefault = activityResultCaller.registerForActivityResult(
|
||||||
contract = OpenDocumentTreeContractLegacy(flags),
|
contract = OpenDocumentTreeContractDefault(flags),
|
||||||
callback = callback,
|
callback = callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun launch(input: Uri?, options: ActivityOptionsCompat?) {
|
override fun launch(input: Uri?, options: ActivityOptionsCompat?) {
|
||||||
if (pickFileTreeLauncherQ == null) {
|
|
||||||
pickFileTreeLauncherLegacy.launch(input, options)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
pickFileTreeLauncherQ.launch(input, options)
|
pickFileTreeLauncherDefault.launch(input, options)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTraceDebug()
|
if (pickFileTreeLauncherPrimaryStorage != null) {
|
||||||
pickFileTreeLauncherLegacy.launch(input, options)
|
try {
|
||||||
|
pickFileTreeLauncherPrimaryStorage.launch(input, options)
|
||||||
|
} catch (e2: Exception) {
|
||||||
|
e.addSuppressed(e2)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unregister() {
|
override fun unregister() {
|
||||||
pickFileTreeLauncherQ?.unregister()
|
pickFileTreeLauncherPrimaryStorage?.unregister()
|
||||||
pickFileTreeLauncherLegacy.unregister()
|
pickFileTreeLauncherDefault.unregister()
|
||||||
}
|
}
|
||||||
|
|
||||||
override val contract: ActivityResultContract<Uri?, *>
|
override val contract: ActivityResultContract<Uri?, *>
|
||||||
get() = pickFileTreeLauncherQ?.contract ?: pickFileTreeLauncherLegacy.contract
|
get() = pickFileTreeLauncherPrimaryStorage?.contract ?: pickFileTreeLauncherDefault.contract
|
||||||
|
|
||||||
private open class OpenDocumentTreeContractLegacy(
|
private open class OpenDocumentTreeContractDefault(
|
||||||
private val flags: Int,
|
private val flags: Int,
|
||||||
) : ActivityResultContracts.OpenDocumentTree() {
|
) : ActivityResultContracts.OpenDocumentTree() {
|
||||||
|
|
||||||
@@ -71,9 +74,9 @@ class OpenDocumentTreeHelper(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
private class OpenDocumentTreeContractQ(
|
private class OpenDocumentTreeContractPrimaryStorage(
|
||||||
private val flags: Int,
|
private val flags: Int,
|
||||||
) : OpenDocumentTreeContractLegacy(flags) {
|
) : OpenDocumentTreeContractDefault(flags) {
|
||||||
|
|
||||||
override fun createIntent(context: Context, input: Uri?): Intent {
|
override fun createIntent(context: Context, input: Uri?): Intent {
|
||||||
val intent = (context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager)
|
val intent = (context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import androidx.preference.PreferenceFragmentCompat
|
|||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.backups.ui.periodical.PeriodicalBackupSettingsFragment
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
@@ -146,6 +147,7 @@ class SettingsActivity :
|
|||||||
AppRouter.ACTION_SUGGESTIONS -> SuggestionsSettingsFragment()
|
AppRouter.ACTION_SUGGESTIONS -> SuggestionsSettingsFragment()
|
||||||
AppRouter.ACTION_HISTORY -> UserDataSettingsFragment()
|
AppRouter.ACTION_HISTORY -> UserDataSettingsFragment()
|
||||||
AppRouter.ACTION_TRACKER -> TrackerSettingsFragment()
|
AppRouter.ACTION_TRACKER -> TrackerSettingsFragment()
|
||||||
|
AppRouter.ACTION_PERIODIC_BACKUP -> PeriodicalBackupSettingsFragment()
|
||||||
AppRouter.ACTION_SOURCES -> SourcesSettingsFragment()
|
AppRouter.ACTION_SOURCES -> SourcesSettingsFragment()
|
||||||
AppRouter.ACTION_PROXY -> ProxySettingsFragment()
|
AppRouter.ACTION_PROXY -> ProxySettingsFragment()
|
||||||
AppRouter.ACTION_MANAGE_DOWNLOADS -> DownloadsSettingsFragment()
|
AppRouter.ACTION_MANAGE_DOWNLOADS -> DownloadsSettingsFragment()
|
||||||
|
|||||||
@@ -855,4 +855,5 @@
|
|||||||
<string name="theme_name_totoro">Totoro</string>
|
<string name="theme_name_totoro">Totoro</string>
|
||||||
<string name="book_effect">Yellowish background (blue filter)</string>
|
<string name="book_effect">Yellowish background (blue filter)</string>
|
||||||
<string name="local_storage_cleanup">Local storage cleanup</string>
|
<string name="local_storage_cleanup">Local storage cleanup</string>
|
||||||
|
<string name="packup_creation_failed">Failed to create backup</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user