diff --git a/app/src/main/java/com/github/libretube/extensions/ToastFromMainThread.kt b/app/src/main/java/com/github/libretube/extensions/ToastFromMainThread.kt index 2be369899..22d6337ab 100644 --- a/app/src/main/java/com/github/libretube/extensions/ToastFromMainThread.kt +++ b/app/src/main/java/com/github/libretube/extensions/ToastFromMainThread.kt @@ -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)) +} diff --git a/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt b/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt index aaa852b55..b520066ff 100644 --- a/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt @@ -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(inputStream) - } + suspend fun restoreAdvancedBackup(context: Context, uri: Uri) { + val backupFile = context.contentResolver.openInputStream(uri)?.use { + JsonHelper.json.decodeFromStream(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?) { + private fun restorePreferences(context: Context, preferences: List?) { if (preferences == null) return PreferenceManager.getDefaultSharedPreferences(context).edit(commit = true) { // clear the previous settings diff --git a/app/src/main/java/com/github/libretube/helpers/ImportHelper.kt b/app/src/main/java/com/github/libretube/helpers/ImportHelper.kt index c61b70dab..6674f1bf5 100644 --- a/app/src/main/java/com/github/libretube/helpers/ImportHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/ImportHelper.kt @@ -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 { + private fun getChannelsFromUri(activity: Activity, uri: Uri): List { 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() 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) } } diff --git a/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt b/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt index 63020dc01..9deee4061 100644 --- a/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt +++ b/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt @@ -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 - private lateinit var createBackupFile: ActivityResultLauncher - 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 - private lateinit var createSubscriptionsFile: ActivityResultLauncher + 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 - private lateinit var createPlaylistsFile: ActivityResultLauncher - - 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("restore") restoreAdvancedBackup?.setOnPreferenceClickListener { - getBackupFile.launch("application/json") + getBackupFile.launch(JSON) true } } + + companion object { + private const val JSON = "application/json" + } }