6.0.10 commit

This commit is contained in:
Xilin Jia 2024-07-13 21:19:33 +01:00
parent 3f07f253b9
commit 43054ec52e
9 changed files with 246 additions and 19 deletions

View File

@ -120,6 +120,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* Disabled `usesCleartextTraffic`, so that all content transmission is more private and secure * Disabled `usesCleartextTraffic`, so that all content transmission is more private and secure
* Settings/Preferences can now be exported and imported * Settings/Preferences can now be exported and imported
* Play history/progress can be separately exported/imported as Json files * Play history/progress can be separately exported/imported as Json files
* downloaded media files can be exported/imported
* There is a setting to disable/enable auto backup OPML files to Google * There is a setting to disable/enable auto backup OPML files to Google
For more details of the changes, see the [Changelog](changelog.md) For more details of the changes, see the [Changelog](changelog.md)

View File

@ -126,8 +126,8 @@ android {
buildConfig true buildConfig true
} }
defaultConfig { defaultConfig {
versionCode 3020209 versionCode 3020210
versionName "6.0.9" versionName "6.0.10"
applicationId "ac.mdiq.podcini.R" applicationId "ac.mdiq.podcini.R"
def commit = "" def commit = ""

View File

@ -54,7 +54,6 @@ object UserPreferences {
private const val PREF_QUEUE_KEEP_SORTED_ORDER: String = "prefQueueKeepSortedOrder" private const val PREF_QUEUE_KEEP_SORTED_ORDER: String = "prefQueueKeepSortedOrder"
private const val PREF_DOWNLOADS_SORTED_ORDER = "prefDownloadSortedOrder" private const val PREF_DOWNLOADS_SORTED_ORDER = "prefDownloadSortedOrder"
private const val PREF_HISTORY_SORTED_ORDER = "prefHistorySortedOrder" private const val PREF_HISTORY_SORTED_ORDER = "prefHistorySortedOrder"
private const val PREF_INBOX_SORTED_ORDER = "prefInboxSortedOrder"
// Episodes // Episodes
private const val PREF_SORT_ALL_EPISODES: String = "prefEpisodesSort" private const val PREF_SORT_ALL_EPISODES: String = "prefEpisodesSort"
@ -585,14 +584,17 @@ object UserPreferences {
return !feed.isLocalFeed || isAutoDeleteLocal return !feed.isLocalFeed || isAutoDeleteLocal
} }
// only used in test
fun showSkipOnFullNotification(): Boolean { fun showSkipOnFullNotification(): Boolean {
return showButtonOnFullNotification(NOTIFICATION_BUTTON_SKIP) return showButtonOnFullNotification(NOTIFICATION_BUTTON_SKIP)
} }
// only used in test
fun showNextChapterOnFullNotification(): Boolean { fun showNextChapterOnFullNotification(): Boolean {
return showButtonOnFullNotification(NOTIFICATION_BUTTON_NEXT_CHAPTER) return showButtonOnFullNotification(NOTIFICATION_BUTTON_NEXT_CHAPTER)
} }
// only used in test
fun showPlaybackSpeedOnFullNotification(): Boolean { fun showPlaybackSpeedOnFullNotification(): Boolean {
return showButtonOnFullNotification(NOTIFICATION_BUTTON_PLAYBACK_SPEED) return showButtonOnFullNotification(NOTIFICATION_BUTTON_PLAYBACK_SPEED)
} }
@ -646,6 +648,7 @@ object UserPreferences {
return if (mediaType == MediaType.VIDEO) videoPlaybackSpeed else audioPlaybackSpeed return if (mediaType == MediaType.VIDEO) videoPlaybackSpeed else audioPlaybackSpeed
} }
// only used in test
fun shouldPauseForFocusLoss(): Boolean { fun shouldPauseForFocusLoss(): Boolean {
return appPrefs.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true) return appPrefs.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true)
} }

View File

@ -97,6 +97,16 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
} }
} }
private val restoreMediaFilesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult -> this.restoreMediaFilesResult(result) }
private val backupMediaFilesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
val data: Uri? = it.data?.data
if (data != null) MediaFilesTransporter.exportToDocument(data, requireContext())
}
}
private var progressDialog: ProgressDialog? = null private var progressDialog: ProgressDialog? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -157,6 +167,14 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
exportPreferences() exportPreferences()
true true
} }
findPreference<Preference>(PREF_MEDIAFILES_IMPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
importMediaFiles()
true
}
findPreference<Preference>(PREF_MEDIAFILES_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
exportMediaFiles()
true
}
findPreference<Preference>(PREF_FAVORITE_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { findPreference<Preference>(PREF_FAVORITE_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher, FavoritesWriter()) openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher, FavoritesWriter())
true true
@ -222,6 +240,31 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
builder.show() builder.show()
} }
private fun exportMediaFiles() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addCategory(Intent.CATEGORY_DEFAULT)
backupMediaFilesLauncher.launch(intent)
}
private fun importMediaFiles() {
val builder = MaterialAlertDialogBuilder(requireActivity())
builder.setTitle(R.string.media_files_import_label)
builder.setMessage(R.string.media_files_import_notice)
// add a button
builder.setNegativeButton(R.string.no, null)
builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addCategory(Intent.CATEGORY_DEFAULT)
restoreMediaFilesLauncher.launch(intent)
}
// create and show the alert dialog
builder.show()
}
private fun exportDatabase() { private fun exportDatabase() {
backupDatabaseLauncher.launch(dateStampFilename(DATABASE_EXPORT_FILENAME)) backupDatabaseLauncher.launch(dateStampFilename(DATABASE_EXPORT_FILENAME))
} }
@ -314,7 +357,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
private fun restoreProgressResult(result: ActivityResult) { private fun restoreProgressResult(result: ActivityResult) {
if (result.resultCode != RESULT_OK || result.data?.data == null) return if (result.resultCode != RESULT_OK || result.data?.data == null) return
val uri = result.data!!.data!! val uri = result.data!!.data
uri?.let { uri?.let {
if (isJsonFile(uri)) { if (isJsonFile(uri)) {
progressDialog!!.show() progressDialog!!.show()
@ -379,9 +422,20 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
return fileName.endsWith(".realm", ignoreCase = true) return fileName.endsWith(".realm", ignoreCase = true)
} }
private fun isPrefDir(uri: Uri): Boolean {
val fileName = uri.lastPathSegment ?: return false
return fileName.contains("Podcini-Prefs", ignoreCase = true)
}
private fun isMediaFilesDir(uri: Uri): Boolean {
val fileName = uri.lastPathSegment ?: return false
return fileName.contains("Podcini-MediaFiles", ignoreCase = true)
}
private fun restorePreferencesResult(result: ActivityResult) { private fun restorePreferencesResult(result: ActivityResult) {
if (result.resultCode != RESULT_OK || result.data?.data == null) return if (result.resultCode != RESULT_OK || result.data?.data == null) return
val uri = result.data!!.data!! val uri = result.data!!.data!!
if (isPrefDir(uri)) {
progressDialog!!.show() progressDialog!!.show()
lifecycleScope.launch { lifecycleScope.launch {
try { try {
@ -396,7 +450,38 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
showExportErrorDialog(e) showExportErrorDialog(e)
} }
} }
} else {
val context = requireContext()
val message = context.getString(R.string.import_directory_toast) + "Podcini-Prefs"
showExportErrorDialog(Throwable(message))
} }
}
private fun restoreMediaFilesResult(result: ActivityResult) {
if (result.resultCode != RESULT_OK || result.data?.data == null) return
val uri = result.data!!.data!!
if (isMediaFilesDir(uri)) {
progressDialog!!.show()
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {
MediaFilesTransporter.importBackup(uri, requireContext())
}
withContext(Dispatchers.Main) {
showDatabaseImportSuccessDialog()
progressDialog!!.dismiss()
}
} catch (e: Throwable) {
showExportErrorDialog(e)
}
}
} else {
val context = requireContext()
val message = context.getString(R.string.import_directory_toast) + "Podcini-MediaFiles"
showExportErrorDialog(Throwable(message))
}
}
private fun backupDatabaseResult(uri: Uri?) { private fun backupDatabaseResult(uri: Uri?) {
if (uri == null) return if (uri == null) return
@ -621,6 +706,96 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
} }
} }
object MediaFilesTransporter {
private val TAG: String = MediaFilesTransporter::class.simpleName ?: "Anonymous"
@Throws(IOException::class)
fun exportToDocument(uri: Uri, context: Context) {
try {
val mediaDir = context.getExternalFilesDir("media") ?: return
val chosenDir = DocumentFile.fromTreeUri(context, uri) ?: throw IOException("Destination directory is not valid")
val exportSubDir = chosenDir.createDirectory("Podcini-MediaFiles") ?: throw IOException("Error creating subdirectory Podcini-Prefs")
mediaDir.listFiles()?.forEach { file ->
copyRecursive(context, file, mediaDir, exportSubDir)
}
} catch (e: IOException) {
Log.e(TAG, Log.getStackTraceString(e))
throw e
} finally { }
}
private fun copyRecursive(context: Context, srcFile: File, srcRootDir: File, destRootDir: DocumentFile) {
val relativePath = srcFile.absolutePath.substring(srcRootDir.absolutePath.length+1)
if (srcFile.isDirectory) {
val dirFiles = srcFile.listFiles()
if (!dirFiles.isNullOrEmpty()) {
val destDir = destRootDir.findFile(relativePath) ?: destRootDir.createDirectory(relativePath) ?: return
dirFiles.forEach { file ->
copyRecursive(context, file, srcFile, destDir)
}
}
} else {
val destFile = destRootDir.createFile("application/octet-stream", relativePath) ?: return
copyFile(srcFile, destFile, context)
}
}
private fun copyFile(sourceFile: File, destFile: DocumentFile, context: Context) {
try {
val outputStream = context.contentResolver.openOutputStream(destFile.uri) ?: return
val inputStream = FileInputStream(sourceFile)
copyStream(inputStream, outputStream)
inputStream.close()
outputStream.close()
} catch (e: IOException) {
Log.e("Error", "Error copying file: $e")
throw e
}
}
private fun copyRecursive(context: Context, srcFile: DocumentFile, srcRootDir: DocumentFile, destRootDir: File) {
val relativePath = srcFile.uri.path?.substring(srcRootDir.uri.path!!.length) ?: return
val destFile = File(destRootDir, relativePath)
if (srcFile.isDirectory) {
if (!destFile.exists()) destFile.mkdirs()
srcFile.listFiles().forEach { file ->
copyRecursive(context, file, srcFile, destFile)
}
} else {
if (!destFile.exists()) copyFile(srcFile, destFile, context)
}
}
private fun copyFile(sourceFile: DocumentFile, destFile: File, context: Context) {
try {
val inputStream = context.contentResolver.openInputStream(sourceFile.uri) ?: return
val outputStream = FileOutputStream(destFile)
copyStream(inputStream, outputStream)
inputStream.close()
outputStream.close()
} catch (e: IOException) {
Log.e("Error", "Error copying file: $e")
throw e
}
}
private fun copyStream(inputStream: InputStream, outputStream: OutputStream) {
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
}
@Throws(IOException::class)
fun importBackup(uri: Uri, context: Context) {
try {
val exportedDir = DocumentFile.fromTreeUri(context, uri) ?: throw IOException("Backup directory is not valid")
if (exportedDir.name?.contains("Podcini-MediaFiles") != true) return
val mediaDir = context.getExternalFilesDir("media") ?: return
exportedDir.listFiles().forEach { file ->
copyRecursive(context, file, exportedDir, mediaDir)
}
} catch (e: IOException) {
Log.e(TAG, Log.getStackTraceString(e))
throw e
} finally { }
}
}
object DatabaseTransporter { object DatabaseTransporter {
private val TAG: String = DatabaseTransporter::class.simpleName ?: "Anonymous" private val TAG: String = DatabaseTransporter::class.simpleName ?: "Anonymous"
@Throws(IOException::class) @Throws(IOException::class)
@ -898,6 +1073,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
private const val PREF_HTML_EXPORT = "prefHtmlExport" private const val PREF_HTML_EXPORT = "prefHtmlExport"
private const val PREF_PREFERENCES_IMPORT = "prefPrefImport" private const val PREF_PREFERENCES_IMPORT = "prefPrefImport"
private const val PREF_PREFERENCES_EXPORT = "prefPrefExport" private const val PREF_PREFERENCES_EXPORT = "prefPrefExport"
private const val PREF_MEDIAFILES_IMPORT = "prefMediaFilesImport"
private const val PREF_MEDIAFILES_EXPORT = "prefMediaFilesExport"
private const val PREF_DATABASE_IMPORT = "prefDatabaseImport" private const val PREF_DATABASE_IMPORT = "prefDatabaseImport"
private const val PREF_DATABASE_EXPORT = "prefDatabaseExport" private const val PREF_DATABASE_EXPORT = "prefDatabaseExport"
private const val PREF_FAVORITE_EXPORT = "prefFavoritesExport" private const val PREF_FAVORITE_EXPORT = "prefFavoritesExport"

View File

@ -599,6 +599,7 @@
<!-- import and export --> <!-- import and export -->
<string name="import_export_summary">Move subscriptions and queue to another device</string> <string name="import_export_summary">Move subscriptions and queue to another device</string>
<string name="preferences">Preferences</string> <string name="preferences">Preferences</string>
<string name="media_files">Media files</string>
<string name="database">Database</string> <string name="database">Database</string>
<string name="opml">OPML</string> <string name="opml">OPML</string>
<string name="html">HTML</string> <string name="html">HTML</string>
@ -606,8 +607,10 @@
<string name="html_export_summary">Show your subscriptions to a friend</string> <string name="html_export_summary">Show your subscriptions to a friend</string>
<string name="opml_export_summary">Transfer your subscriptions to another podcast app</string> <string name="opml_export_summary">Transfer your subscriptions to another podcast app</string>
<string name="opml_import_summary">Import your subscriptions from another podcast app</string> <string name="opml_import_summary">Import your subscriptions from another podcast app</string>
<string name="preferences_export_summary">Transfer Podcini preferences to Podcini on another device</string> <string name="preferences_export_summary">Export Podcini preferences</string>
<string name="preferences_import_summary">Import Podcini preferences from another device</string> <string name="preferences_import_summary">Import Podcini preferences</string>
<string name="media_files_export_summary">Export Podcini media files</string>
<string name="media_files_import_summary">Import Podcini media files</string>
<string name="database_export_summary">Transfer subscriptions, listened episodes and queue to Podcini on another device</string> <string name="database_export_summary">Transfer subscriptions, listened episodes and queue to Podcini on another device</string>
<string name="database_import_summary">Import Podcini database from another device</string> <string name="database_import_summary">Import Podcini database from another device</string>
<string name="opml_import_label">OPML import</string> <string name="opml_import_label">OPML import</string>
@ -623,13 +626,17 @@
<string name="progress_import_warning">Importing episodes progress will replace all of your current playing history. You should export your current progress as a backup. Do you want to replace\? If yes, in the next screen, choose the desired .json file. The process can take a couple minutes depending on size. Once completed, a popup of either success or failure will be shown.</string> <string name="progress_import_warning">Importing episodes progress will replace all of your current playing history. You should export your current progress as a backup. Do you want to replace\? If yes, in the next screen, choose the desired .json file. The process can take a couple minutes depending on size. Once completed, a popup of either success or failure will be shown.</string>
<string name="opml_export_label">OPML export</string> <string name="opml_export_label">OPML export</string>
<string name="html_export_label">HTML export</string> <string name="html_export_label">HTML export</string>
<string name="media_files_export_label">Media files export</string>
<string name="media_files_import_label">Media files import</string>
<string name="preferences_export_label">Preferences export</string> <string name="preferences_export_label">Preferences export</string>
<string name="preferences_import_label">Preferences import</string> <string name="preferences_import_label">Preferences import</string>
<string name="preferences_import_warning">Importing preferences will replace all of your current preferences. If confirmed, choose a previously exported directory with name containing \"Podcini-Prefs\"</string> <string name="preferences_import_warning">Importing preferences will replace all of your current preferences. If confirmed, choose a previously exported directory with name containing \"Podcini-Prefs\"</string>
<string name="media_files_import_notice">Choose a previously exported directory with name containing \"Podcini-MediaFiles\"</string>
<string name="database_export_label">Database export</string> <string name="database_export_label">Database export</string>
<string name="database_import_label">Database import</string> <string name="database_import_label">Database import</string>
<string name="realm_database_import_label">Realm database import</string> <string name="realm_database_import_label">Realm database import</string>
<string name="database_import_warning">Importing a database will replace all of your current subscriptions and playing history. You should export your current database as a backup. Do you want to replace\? If yes, in the next screen, choose a file with extension .realm</string> <string name="database_import_warning">Importing a database will replace all of your current subscriptions and playing history. You should export your current database as a backup. Do you want to replace\? If yes, in the next screen, choose a file with extension .realm</string>
<string name="import_directory_toast">Only accepting directory name including: </string>
<string name="import_file_type_toast">Only accepting file extension: </string> <string name="import_file_type_toast">Only accepting file extension: </string>
<string name="please_wait">Please wait&#8230;</string> <string name="please_wait">Please wait&#8230;</string>
<string name="export_error_label">Export error</string> <string name="export_error_label">Export error</string>

View File

@ -16,6 +16,19 @@
android:summary="@string/database_import_summary"/> android:summary="@string/database_import_summary"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/media_files">
<Preference
android:key="prefMediaFilesExport"
search:keywords="@string/import_export_search_keywords"
android:title="@string/media_files_export_label"
android:summary="@string/media_files_export_summary"/>
<Preference
android:key="prefMediaFilesImport"
search:keywords="@string/import_export_search_keywords"
android:title="@string/media_files_import_label"
android:summary="@string/media_files_import_summary"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/preferences"> <PreferenceCategory android:title="@string/preferences">
<Preference <Preference
android:key="prefPrefExport" android:key="prefPrefExport"

View File

@ -1,3 +1,15 @@
# 6.0.10
* for better migrating from version 5, added export/import of downloaded media files: inter-operable with Podcini 5.5.4
* importing media files is restricted to directory with name containing "Podcini-MediaFiles"
* importing preferences is restricted directory with name containing "Podcini-Prefs"
# 5.5.4
* this is an extra minor release for better migration to Podcini 6
* added export/import of downloaded media files which can then be imported in Podcini 6
* this release is not updated to F-Droid due to Podcini 6 listing in progress
# 6.0.9 # 6.0.9
This is the first release of Podcini.R version 6. If you have an older version installed, this release installs afresh in parallel with and is not compatible with older versions. Main changes are: This is the first release of Podcini.R version 6. If you have an older version installed, this release installs afresh in parallel with and is not compatible with older versions. Main changes are:
@ -55,7 +67,7 @@ This is the first release of Podcini.R version 6. If you have an older version i
* decade-old joanzapata iconify is replaced with mikepenz iconics * decade-old joanzapata iconify is replaced with mikepenz iconics
* removed the need for support libraries and the need for the jetifier * removed the need for support libraries and the need for the jetifier
* Java tools checkstyle and spotbus are removed * Java tools checkstyle and spotbus are removed
* the clumsy FeedDrawerItem class was removed and related compponents are based purely on feed objects * the clumsy FeedDrawerItem class was removed and related components are based purely on feed objects
* code is now built with Kotlin 2.0.0 * code is now built with Kotlin 2.0.0
* for more details, see the changelogs in pre-release versions * for more details, see the changelogs in pre-release versions
@ -83,6 +95,13 @@ This is the first release of Podcini.R version 6. If you have an older version i
* enabled selection of .json files when importing progress * enabled selection of .json files when importing progress
* in wifi sync and episode progress export/import, changed start position and added played duration for episodes (available from 5.5.3), this helps for the statistics view at the importer to correctly show imported progress without having to do "include marked played" * in wifi sync and episode progress export/import, changed start position and added played duration for episodes (available from 5.5.3), this helps for the statistics view at the importer to correctly show imported progress without having to do "include marked played"
## 5.5.3
* this is an extra minor release for better migration to Podcini 6
* in wifi sync and episode progress export/import, changed start position and added played duration for episodes
* this helps for the statistics view at the importer to correctly show imported progress without having to do "include marked played"
* this release is not updated to F-Droid due to Podcini 6 release in progress
## 6.0.5 ## 6.0.5
* fixed threading issue of downloading multiple episodes * fixed threading issue of downloading multiple episodes

View File

@ -0,0 +1,6 @@
Version 6.0.10 brings several changes:
* added export/import of downloaded media files: inter-operable with Podcini 5.5.4
* importing media files is restricted to directory with name containing "Podcini-MediaFiles"
* importing preferences is restricted directory with name containing "Podcini-Prefs"

View File

@ -13,10 +13,11 @@ the following can be imported to it from Settings -> Import/Export:
* preferences files * preferences files
* OPML file * OPML file
* json file of episodes progress * json file of episodes progress
* downloaded media files (5.5.4 only)
An OPML file should be imported before importing episodes progress, but you can always re-do any of the above An OPML file should be imported before importing episodes progress, but you can always re-do any of the above
These files can be best exported in version 5.5.3 in Settings -> Import/Export These files can be best exported in version 5.5.4 (or 5.5.3, or 5.5.2) in Settings -> Import/Export
Then your subscriptions, listening progress, favorites, and app preferences will be updated. Then your subscriptions, listening progress, favorites, and app preferences will be updated.