Skip to content

Commit

Permalink
6.0.10 commit
Browse files Browse the repository at this point in the history
  • Loading branch information
XilinJia committed Jul 13, 2024
1 parent 3f07f25 commit 43054ec
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 18 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
}
} 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)
}
} 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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand Down
11 changes: 9 additions & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -599,15 +599,18 @@
<!-- 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>
<string name="progress">Progress</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_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>
Expand All @@ -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&#8230;</string>
<string name="export_error_label">Export error</string>
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/res/xml/preferences_import_export.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading

0 comments on commit 43054ec

Please sign in to comment.