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.Handler
import android.os.Looper import android.os.Looper
import android.widget.Toast import android.widget.Toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
fun Context.toastFromMainThread(text: String) { fun Context.toastFromMainThread(text: String) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
@ -18,3 +20,11 @@ fun Context.toastFromMainThread(text: String) {
fun Context.toastFromMainThread(stringId: Int) { fun Context.toastFromMainThread(stringId: Int) {
toastFromMainThread(getString(stringId)) 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.extensions.TAG
import com.github.libretube.obj.BackupFile import com.github.libretube.obj.BackupFile
import com.github.libretube.obj.PreferenceItem import com.github.libretube.obj.PreferenceItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.booleanOrNull import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
@ -24,20 +22,18 @@ import kotlinx.serialization.json.longOrNull
/** /**
* Backup and restore the preferences * 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 * Write a [BackupFile] containing the database content as well as the preferences
*/ */
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
fun createAdvancedBackup(uri: Uri?, backupFile: BackupFile) { fun createAdvancedBackup(context: Context, uri: Uri, backupFile: BackupFile) {
uri?.let { try {
try { context.contentResolver.openOutputStream(uri)?.use { outputStream ->
context.contentResolver.openOutputStream(it)?.use { outputStream -> JsonHelper.json.encodeToStream(backupFile, outputStream)
JsonHelper.json.encodeToStream(backupFile, outputStream)
}
} catch (e: Exception) {
Log.e(TAG(), "Error while writing backup: $e")
} }
} 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] * Restore data from a [BackupFile]
*/ */
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
fun restoreAdvancedBackup(uri: Uri?) { suspend fun restoreAdvancedBackup(context: Context, uri: Uri) {
val backupFile = uri?.let { val backupFile = context.contentResolver.openInputStream(uri)?.use {
context.contentResolver.openInputStream(it)?.use { inputStream -> JsonHelper.json.decodeFromStream<BackupFile>(it)
JsonHelper.json.decodeFromStream<BackupFile>(inputStream)
}
} ?: return } ?: return
runBlocking(Dispatchers.IO) { Database.watchHistoryDao().insertAll(
Database.watchHistoryDao().insertAll( *backupFile.watchHistory.orEmpty().toTypedArray()
*backupFile.watchHistory.orEmpty().toTypedArray() )
) Database.searchHistoryDao().insertAll(
Database.searchHistoryDao().insertAll( *backupFile.searchHistory.orEmpty().toTypedArray()
*backupFile.searchHistory.orEmpty().toTypedArray() )
) Database.watchPositionDao().insertAll(
Database.watchPositionDao().insertAll( *backupFile.watchPositions.orEmpty().toTypedArray()
*backupFile.watchPositions.orEmpty().toTypedArray() )
) Database.localSubscriptionDao().insertAll(backupFile.localSubscriptions.orEmpty())
Database.localSubscriptionDao().insertAll(backupFile.localSubscriptions.orEmpty()) Database.customInstanceDao().insertAll(
Database.customInstanceDao().insertAll( *backupFile.customInstances.orEmpty().toTypedArray()
*backupFile.customInstances.orEmpty().toTypedArray() )
) Database.playlistBookmarkDao().insertAll(
Database.playlistBookmarkDao().insertAll( *backupFile.playlistBookmarks.orEmpty().toTypedArray()
*backupFile.playlistBookmarks.orEmpty().toTypedArray() )
)
backupFile.localPlaylists.orEmpty().forEach { backupFile.localPlaylists.orEmpty().forEach {
Database.localPlaylistsDao().createPlaylist(it.playlist) Database.localPlaylistsDao().createPlaylist(it.playlist)
val playlistId = Database.localPlaylistsDao().getAll().last().playlist.id val playlistId = Database.localPlaylistsDao().getAll().last().playlist.id
it.videos.forEach { it.videos.forEach {
it.playlistId = playlistId it.playlistId = playlistId
Database.localPlaylistsDao().addPlaylistVideo(it) Database.localPlaylistsDao().addPlaylistVideo(it)
}
} }
restorePreferences(backupFile.preferences)
} }
restorePreferences(context, backupFile.preferences)
} }
/** /**
* Restore the shared preferences from a backup file * 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 if (preferences == null) return
PreferenceManager.getDefaultSharedPreferences(context).edit(commit = true) { PreferenceManager.getDefaultSharedPreferences(context).edit(commit = true) {
// clear the previous settings // clear the previous settings

View File

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

View File

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