6.0.10 commit
This commit is contained in:
parent
3f07f253b9
commit
43054ec52e
|
@ -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
|
||||
* Settings/Preferences can now be exported and imported
|
||||
* 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
|
||||
|
||||
For more details of the changes, see the [Changelog](changelog.md)
|
||||
|
|
|
@ -126,8 +126,8 @@ android {
|
|||
buildConfig true
|
||||
}
|
||||
defaultConfig {
|
||||
versionCode 3020209
|
||||
versionName "6.0.9"
|
||||
versionCode 3020210
|
||||
versionName "6.0.10"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
|
|
@ -54,7 +54,6 @@ object UserPreferences {
|
|||
private const val PREF_QUEUE_KEEP_SORTED_ORDER: String = "prefQueueKeepSortedOrder"
|
||||
private const val PREF_DOWNLOADS_SORTED_ORDER = "prefDownloadSortedOrder"
|
||||
private const val PREF_HISTORY_SORTED_ORDER = "prefHistorySortedOrder"
|
||||
private const val PREF_INBOX_SORTED_ORDER = "prefInboxSortedOrder"
|
||||
|
||||
// Episodes
|
||||
private const val PREF_SORT_ALL_EPISODES: String = "prefEpisodesSort"
|
||||
|
@ -585,14 +584,17 @@ object UserPreferences {
|
|||
return !feed.isLocalFeed || isAutoDeleteLocal
|
||||
}
|
||||
|
||||
// only used in test
|
||||
fun showSkipOnFullNotification(): Boolean {
|
||||
return showButtonOnFullNotification(NOTIFICATION_BUTTON_SKIP)
|
||||
}
|
||||
|
||||
// only used in test
|
||||
fun showNextChapterOnFullNotification(): Boolean {
|
||||
return showButtonOnFullNotification(NOTIFICATION_BUTTON_NEXT_CHAPTER)
|
||||
}
|
||||
|
||||
// only used in test
|
||||
fun showPlaybackSpeedOnFullNotification(): Boolean {
|
||||
return showButtonOnFullNotification(NOTIFICATION_BUTTON_PLAYBACK_SPEED)
|
||||
}
|
||||
|
@ -646,6 +648,7 @@ object UserPreferences {
|
|||
return if (mediaType == MediaType.VIDEO) videoPlaybackSpeed else audioPlaybackSpeed
|
||||
}
|
||||
|
||||
// only used in test
|
||||
fun shouldPauseForFocusLoss(): Boolean {
|
||||
return appPrefs.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
|
@ -157,6 +167,14 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
exportPreferences()
|
||||
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 {
|
||||
openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher, FavoritesWriter())
|
||||
true
|
||||
|
@ -222,6 +240,31 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
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() {
|
||||
backupDatabaseLauncher.launch(dateStampFilename(DATABASE_EXPORT_FILENAME))
|
||||
}
|
||||
|
@ -314,7 +357,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
|
||||
private fun restoreProgressResult(result: ActivityResult) {
|
||||
if (result.resultCode != RESULT_OK || result.data?.data == null) return
|
||||
val uri = result.data!!.data!!
|
||||
val uri = result.data!!.data
|
||||
uri?.let {
|
||||
if (isJsonFile(uri)) {
|
||||
progressDialog!!.show()
|
||||
|
@ -379,25 +422,67 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
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) {
|
||||
if (result.resultCode != RESULT_OK || result.data?.data == null) return
|
||||
val uri = result.data!!.data!!
|
||||
progressDialog!!.show()
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
PreferencesTransporter.importBackup(uri, requireContext())
|
||||
if (isPrefDir(uri)) {
|
||||
progressDialog!!.show()
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
PreferencesTransporter.importBackup(uri, requireContext())
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
showDatabaseImportSuccessDialog()
|
||||
progressDialog!!.dismiss()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
showExportErrorDialog(e)
|
||||
}
|
||||
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-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?) {
|
||||
if (uri == null) return
|
||||
progressDialog!!.show()
|
||||
|
@ -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 {
|
||||
private val TAG: String = DatabaseTransporter::class.simpleName ?: "Anonymous"
|
||||
@Throws(IOException::class)
|
||||
|
@ -898,6 +1073,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
private const val PREF_HTML_EXPORT = "prefHtmlExport"
|
||||
private const val PREF_PREFERENCES_IMPORT = "prefPrefImport"
|
||||
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_EXPORT = "prefDatabaseExport"
|
||||
private const val PREF_FAVORITE_EXPORT = "prefFavoritesExport"
|
||||
|
|
|
@ -599,6 +599,7 @@
|
|||
<!-- import and export -->
|
||||
<string name="import_export_summary">Move subscriptions and queue to another device</string>
|
||||
<string name="preferences">Preferences</string>
|
||||
<string name="media_files">Media files</string>
|
||||
<string name="database">Database</string>
|
||||
<string name="opml">OPML</string>
|
||||
<string name="html">HTML</string>
|
||||
|
@ -606,8 +607,10 @@
|
|||
<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_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_import_summary">Import Podcini preferences from another device</string>
|
||||
<string name="preferences_export_summary">Export Podcini preferences</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_import_summary">Import Podcini database from another device</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="opml_export_label">OPML 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_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="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_import_label">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="import_directory_toast">Only accepting directory name including: </string>
|
||||
<string name="import_file_type_toast">Only accepting file extension: </string>
|
||||
<string name="please_wait">Please wait…</string>
|
||||
<string name="export_error_label">Export error</string>
|
||||
|
|
|
@ -16,6 +16,19 @@
|
|||
android:summary="@string/database_import_summary"/>
|
||||
</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">
|
||||
<Preference
|
||||
android:key="prefPrefExport"
|
||||
|
|
21
changelog.md
21
changelog.md
|
@ -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
|
||||
|
||||
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
|
||||
* removed the need for support libraries and the need for the jetifier
|
||||
* 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
|
||||
* 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
|
||||
* 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
|
||||
|
||||
* fixed threading issue of downloading multiple episodes
|
||||
|
|
|
@ -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"
|
|
@ -13,10 +13,11 @@ the following can be imported to it from Settings -> Import/Export:
|
|||
* preferences files
|
||||
* OPML file
|
||||
* 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
|
||||
|
||||
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.
|
||||
|
||||
|
|
Loading…
Reference in New Issue