Merge pull request #2970 from Isira-Seneviratne/Backup_restore_improvements

Make backup and restore improvements.
This commit is contained in:
Bnyro 2023-02-05 11:16:16 +01:00 committed by GitHub
commit 3d78250daf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 136 additions and 153 deletions

View File

@ -4,6 +4,8 @@ import android.content.Context
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
fun Context.toastFromMainThread(text: String) {
Handler(Looper.getMainLooper()).post {
@ -18,3 +20,11 @@ fun Context.toastFromMainThread(text: String) {
fun Context.toastFromMainThread(stringId: Int) {
toastFromMainThread(getString(stringId))
}
suspend fun Context.toastFromMainDispatcher(text: String) = withContext(Dispatchers.Main) {
Toast.makeText(this@toastFromMainDispatcher, text, Toast.LENGTH_SHORT).show()
}
suspend fun Context.toastFromMainDispatcher(stringId: Int) {
toastFromMainDispatcher(getString(stringId))
}

View File

@ -11,8 +11,6 @@ import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.extensions.TAG
import com.github.libretube.obj.BackupFile
import com.github.libretube.obj.PreferenceItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.decodeFromStream
@ -24,20 +22,18 @@ import kotlinx.serialization.json.longOrNull
/**
* Backup and restore the preferences
*/
class BackupHelper(private val context: Context) {
object BackupHelper {
/**
* Write a [BackupFile] containing the database content as well as the preferences
*/
@OptIn(ExperimentalSerializationApi::class)
fun createAdvancedBackup(uri: Uri?, backupFile: BackupFile) {
uri?.let {
try {
context.contentResolver.openOutputStream(it)?.use { outputStream ->
JsonHelper.json.encodeToStream(backupFile, outputStream)
}
} catch (e: Exception) {
Log.e(TAG(), "Error while writing backup: $e")
fun createAdvancedBackup(context: Context, uri: Uri, backupFile: BackupFile) {
try {
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
JsonHelper.json.encodeToStream(backupFile, outputStream)
}
} catch (e: Exception) {
Log.e(TAG(), "Error while writing backup: $e")
}
}
@ -45,48 +41,44 @@ class BackupHelper(private val context: Context) {
* Restore data from a [BackupFile]
*/
@OptIn(ExperimentalSerializationApi::class)
fun restoreAdvancedBackup(uri: Uri?) {
val backupFile = uri?.let {
context.contentResolver.openInputStream(it)?.use { inputStream ->
JsonHelper.json.decodeFromStream<BackupFile>(inputStream)
}
suspend fun restoreAdvancedBackup(context: Context, uri: Uri) {
val backupFile = context.contentResolver.openInputStream(uri)?.use {
JsonHelper.json.decodeFromStream<BackupFile>(it)
} ?: return
runBlocking(Dispatchers.IO) {
Database.watchHistoryDao().insertAll(
*backupFile.watchHistory.orEmpty().toTypedArray()
)
Database.searchHistoryDao().insertAll(
*backupFile.searchHistory.orEmpty().toTypedArray()
)
Database.watchPositionDao().insertAll(
*backupFile.watchPositions.orEmpty().toTypedArray()
)
Database.localSubscriptionDao().insertAll(backupFile.localSubscriptions.orEmpty())
Database.customInstanceDao().insertAll(
*backupFile.customInstances.orEmpty().toTypedArray()
)
Database.playlistBookmarkDao().insertAll(
*backupFile.playlistBookmarks.orEmpty().toTypedArray()
)
Database.watchHistoryDao().insertAll(
*backupFile.watchHistory.orEmpty().toTypedArray()
)
Database.searchHistoryDao().insertAll(
*backupFile.searchHistory.orEmpty().toTypedArray()
)
Database.watchPositionDao().insertAll(
*backupFile.watchPositions.orEmpty().toTypedArray()
)
Database.localSubscriptionDao().insertAll(backupFile.localSubscriptions.orEmpty())
Database.customInstanceDao().insertAll(
*backupFile.customInstances.orEmpty().toTypedArray()
)
Database.playlistBookmarkDao().insertAll(
*backupFile.playlistBookmarks.orEmpty().toTypedArray()
)
backupFile.localPlaylists.orEmpty().forEach {
Database.localPlaylistsDao().createPlaylist(it.playlist)
val playlistId = Database.localPlaylistsDao().getAll().last().playlist.id
it.videos.forEach {
it.playlistId = playlistId
Database.localPlaylistsDao().addPlaylistVideo(it)
}
backupFile.localPlaylists.orEmpty().forEach {
Database.localPlaylistsDao().createPlaylist(it.playlist)
val playlistId = Database.localPlaylistsDao().getAll().last().playlist.id
it.videos.forEach {
it.playlistId = playlistId
Database.localPlaylistsDao().addPlaylistVideo(it)
}
restorePreferences(backupFile.preferences)
}
restorePreferences(context, backupFile.preferences)
}
/**
* Restore the shared preferences from a backup file
*/
private fun restorePreferences(preferences: List<PreferenceItem>?) {
private fun restorePreferences(context: Context, preferences: List<PreferenceItem>?) {
if (preferences == null) return
PreferenceManager.getDefaultSharedPreferences(context).edit(commit = true) {
// clear the previous settings

View File

@ -3,7 +3,6 @@ package com.github.libretube.helpers
import android.app.Activity
import android.net.Uri
import android.util.Log
import android.widget.Toast
import com.github.libretube.R
import com.github.libretube.api.JsonHelper
import com.github.libretube.api.PlaylistsHelper
@ -11,52 +10,42 @@ import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.obj.ImportPlaylist
import com.github.libretube.obj.ImportPlaylistFile
import com.github.libretube.obj.NewPipeSubscription
import com.github.libretube.obj.NewPipeSubscriptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import okio.use
class ImportHelper(
private val activity: Activity
) {
object ImportHelper {
/**
* Import subscriptions by a file uri
*/
fun importSubscriptions(uri: Uri?) {
if (uri == null) return
suspend fun importSubscriptions(activity: Activity, uri: Uri) {
try {
val applicationContext = activity.applicationContext
val channels = getChannelsFromUri(uri)
CoroutineScope(Dispatchers.IO).launch {
SubscriptionHelper.importSubscriptions(channels)
}.invokeOnCompletion {
applicationContext.toastFromMainThread(R.string.importsuccess)
}
SubscriptionHelper.importSubscriptions(getChannelsFromUri(activity, uri))
activity.toastFromMainDispatcher(R.string.importsuccess)
} catch (e: IllegalArgumentException) {
Log.e(TAG(), e.toString())
activity.toastFromMainThread(
activity.toastFromMainDispatcher(
activity.getString(R.string.unsupported_file_format) +
" (${activity.contentResolver.getType(uri)})"
)
} catch (e: Exception) {
Log.e(TAG(), e.toString())
Toast.makeText(activity, e.localizedMessage, Toast.LENGTH_SHORT).show()
e.localizedMessage?.let {
activity.toastFromMainDispatcher(it)
}
}
}
/**
* Get a list of channel IDs from a file [Uri]
*/
private fun getChannelsFromUri(uri: Uri): List<String> {
private fun getChannelsFromUri(activity: Activity, uri: Uri): List<String> {
return when (val fileType = activity.contentResolver.getType(uri)) {
"application/json", "application/*", "application/octet-stream" -> {
// NewPipe subscriptions format
@ -84,37 +73,31 @@ class ImportHelper(
/**
* Write the text to the document
*/
@OptIn(ExperimentalSerializationApi::class)
fun exportSubscriptions(uri: Uri?) {
if (uri == null) return
runBlocking(Dispatchers.IO) {
val token = PreferenceHelper.getToken()
val subs = if (token.isNotEmpty()) {
RetrofitInstance.authApi.subscriptions(token)
} else {
val subscriptions = Database.localSubscriptionDao().getAll().map { it.channelId }
RetrofitInstance.authApi.unauthenticatedSubscriptions(subscriptions)
}
val newPipeChannels = subs.map {
NewPipeSubscription(it.name, 0, "https://www.youtube.com${it.url}")
}
val newPipeSubscriptions = NewPipeSubscriptions(subscriptions = newPipeChannels)
activity.contentResolver.openOutputStream(uri)?.use {
JsonHelper.json.encodeToStream(newPipeSubscriptions, it)
}
activity.toastFromMainThread(R.string.exportsuccess)
suspend fun exportSubscriptions(activity: Activity, uri: Uri) {
val token = PreferenceHelper.getToken()
val subs = if (token.isNotEmpty()) {
RetrofitInstance.authApi.subscriptions(token)
} else {
val subscriptions = Database.localSubscriptionDao().getAll().map { it.channelId }
RetrofitInstance.authApi.unauthenticatedSubscriptions(subscriptions)
}
val newPipeChannels = subs.map {
NewPipeSubscription(it.name, 0, "https://www.youtube.com${it.url}")
}
val newPipeSubscriptions = NewPipeSubscriptions(subscriptions = newPipeChannels)
activity.contentResolver.openOutputStream(uri)?.use {
JsonHelper.json.encodeToStream(newPipeSubscriptions, it)
}
activity.toastFromMainDispatcher(R.string.exportsuccess)
}
/**
* Import Playlists
*/
@OptIn(ExperimentalSerializationApi::class)
fun importPlaylists(uri: Uri?) {
if (uri == null) return
suspend fun importPlaylists(activity: Activity, uri: Uri) {
val importPlaylists = mutableListOf<ImportPlaylist>()
when (val fileType = activity.contentResolver.getType(uri)) {
@ -139,7 +122,7 @@ class ImportHelper(
importPlaylists.addAll(playlistFile?.playlists.orEmpty())
}
else -> {
activity.applicationContext.toastFromMainThread("Unsupported file type $fileType")
activity.toastFromMainDispatcher("Unsupported file type $fileType")
return
}
}
@ -148,15 +131,13 @@ class ImportHelper(
importPlaylists.forEach { playlist ->
playlist.videos = playlist.videos.map { it.takeLast(11) }
}
CoroutineScope(Dispatchers.IO).launch {
try {
PlaylistsHelper.importPlaylists(importPlaylists)
activity.applicationContext.toastFromMainThread(R.string.success)
} catch (e: Exception) {
Log.e(TAG(), e.toString())
e.localizedMessage?.let {
activity.applicationContext.toastFromMainThread(it)
}
try {
PlaylistsHelper.importPlaylists(importPlaylists)
activity.toastFromMainDispatcher(R.string.success)
} catch (e: Exception) {
Log.e(TAG(), e.toString())
e.localizedMessage?.let {
activity.toastFromMainDispatcher(it)
}
}
}
@ -164,18 +145,14 @@ class ImportHelper(
/**
* Export Playlists
*/
fun exportPlaylists(uri: Uri?) {
if (uri == null) return
suspend fun exportPlaylists(activity: Activity, uri: Uri) {
val playlists = PlaylistsHelper.exportPlaylists()
val playlistFile = ImportPlaylistFile("Piped", 1, playlists)
runBlocking {
val playlists = PlaylistsHelper.exportPlaylists()
val playlistFile = ImportPlaylistFile("Piped", 1, playlists)
activity.contentResolver.openOutputStream(uri)?.use {
JsonHelper.json.encodeToStream(playlistFile, it)
}
activity.toastFromMainThread(R.string.exportsuccess)
activity.contentResolver.openOutputStream(uri)?.use {
JsonHelper.json.encodeToStream(playlistFile, it)
}
activity.toastFromMainDispatcher(R.string.exportsuccess)
}
}

View File

@ -1,10 +1,9 @@
package com.github.libretube.ui.preferences
import android.net.Uri
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import com.github.libretube.R
import com.github.libretube.helpers.BackupHelper
@ -12,68 +11,69 @@ import com.github.libretube.helpers.ImportHelper
import com.github.libretube.obj.BackupFile
import com.github.libretube.ui.base.BasePreferenceFragment
import com.github.libretube.ui.dialogs.BackupDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class BackupRestoreSettings : BasePreferenceFragment() {
private val backupDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH:mm:ss")
private var backupFile = BackupFile()
override val titleResourceId: Int = R.string.backup_restore
// backup and restore database
private lateinit var getBackupFile: ActivityResultLauncher<String>
private lateinit var createBackupFile: ActivityResultLauncher<String>
private var backupFile = BackupFile()
private val getBackupFile = registerForActivityResult(ActivityResultContracts.GetContent()) {
it?.let {
lifecycleScope.launch(Dispatchers.IO) {
BackupHelper.restoreAdvancedBackup(requireContext(), it)
}
}
}
private val createBackupFile = registerForActivityResult(CreateDocument(JSON)) {
it?.let {
lifecycleScope.launch(Dispatchers.IO) {
BackupHelper.createAdvancedBackup(requireContext(), it, backupFile)
}
}
}
/**
* result listeners for importing and exporting subscriptions
*/
private lateinit var getSubscriptionsFile: ActivityResultLauncher<String>
private lateinit var createSubscriptionsFile: ActivityResultLauncher<String>
private val getSubscriptionsFile = registerForActivityResult(
ActivityResultContracts.GetContent()
) {
it?.let {
lifecycleScope.launch(Dispatchers.IO) {
ImportHelper.importSubscriptions(requireActivity(), it)
}
}
}
private val createSubscriptionsFile = registerForActivityResult(CreateDocument(JSON)) {
it?.let {
lifecycleScope.launch(Dispatchers.IO) {
ImportHelper.exportSubscriptions(requireActivity(), it)
}
}
}
/**
* result listeners for importing and exporting playlists
*/
private lateinit var getPlaylistsFile: ActivityResultLauncher<String>
private lateinit var createPlaylistsFile: ActivityResultLauncher<String>
override fun onCreate(savedInstanceState: Bundle?) {
getSubscriptionsFile =
registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri ->
ImportHelper(requireActivity()).importSubscriptions(uri)
private val getPlaylistsFile = registerForActivityResult(ActivityResultContracts.GetContent()) {
it?.let {
lifecycleScope.launch(Dispatchers.IO) {
ImportHelper.importPlaylists(requireActivity(), it)
}
createSubscriptionsFile = registerForActivityResult(
CreateDocument("application/json")
) { uri ->
ImportHelper(requireActivity()).exportSubscriptions(uri)
}
getPlaylistsFile = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
ImportHelper(requireActivity()).importPlaylists(uri)
}
createPlaylistsFile = registerForActivityResult(
CreateDocument("application/json")
) { uri ->
ImportHelper(requireActivity()).exportPlaylists(uri)
}
getBackupFile =
registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri: Uri? ->
BackupHelper(requireContext()).restoreAdvancedBackup(uri)
}
private val createPlaylistsFile = registerForActivityResult(CreateDocument(JSON)) {
it?.let {
lifecycleScope.launch(Dispatchers.IO) {
ImportHelper.exportPlaylists(requireActivity(), it)
}
createBackupFile = registerForActivityResult(
CreateDocument("application/json")
) { uri: Uri? ->
BackupHelper(requireContext()).createAdvancedBackup(uri, backupFile)
}
super.onCreate(savedInstanceState)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -116,8 +116,12 @@ class BackupRestoreSettings : BasePreferenceFragment() {
val restoreAdvancedBackup = findPreference<Preference>("restore")
restoreAdvancedBackup?.setOnPreferenceClickListener {
getBackupFile.launch("application/json")
getBackupFile.launch(JSON)
true
}
}
companion object {
private const val JSON = "application/json"
}
}