6.0.13 commit

This commit is contained in:
Xilin Jia 2024-07-15 18:55:00 +01:00
parent b4b850baba
commit 91423f9267
84 changed files with 832 additions and 817 deletions

View File

@ -126,8 +126,8 @@ android {
buildConfig true
}
defaultConfig {
versionCode 3020212
versionName "6.0.12"
versionCode 3020213
versionName "6.0.13"
applicationId "ac.mdiq.podcini.R"
def commit = ""

View File

@ -1,9 +1,9 @@
package de.test.podcini.storage
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileStreaming
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileStreaming
import ac.mdiq.podcini.preferences.UserPreferences.isFollowQueue
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isFollowQueue
import ac.mdiq.podcini.storage.algorithms.AutoDownloads
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.downloadAlgorithm
import ac.mdiq.podcini.storage.model.Episode

View File

@ -11,6 +11,12 @@ import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule
import ac.mdiq.podcini.R
import ac.mdiq.podcini.playback.service.PlaybackService
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isFollowQueue
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPauseOnHeadsetDisconnect
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPersistNotify
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isUnpauseOnBluetoothReconnect
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isUnpauseOnHeadsetReconnect
import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.APCleanupAlgorithm
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.APNullCleanupAlgorithm
@ -18,8 +24,6 @@ import ac.mdiq.podcini.storage.algorithms.AutoCleanups.APQueueCleanupAlgorithm
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.build
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.ExceptFavoriteCleanupAlgorithm
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.EnqueueLocation
import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation
import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
import ac.mdiq.podcini.preferences.UserPreferences.init
@ -27,17 +31,14 @@ import ac.mdiq.podcini.preferences.UserPreferences.isAutoDelete
import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery
import ac.mdiq.podcini.preferences.UserPreferences.isFollowQueue
import ac.mdiq.podcini.preferences.UserPreferences.isPauseOnHeadsetDisconnect
import ac.mdiq.podcini.preferences.UserPreferences.isPersistNotify
import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnBluetoothReconnect
import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnHeadsetReconnect
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue
import ac.mdiq.podcini.preferences.UserPreferences.shouldPauseForFocusLoss
import ac.mdiq.podcini.preferences.UserPreferences.showNextChapterOnFullNotification
import ac.mdiq.podcini.preferences.UserPreferences.showPlaybackSpeedOnFullNotification
import ac.mdiq.podcini.preferences.UserPreferences.showSkipOnFullNotification
import ac.mdiq.podcini.storage.database.Queues
import ac.mdiq.podcini.storage.database.Queues.enqueueLocation
import de.test.podcini.EspressoTestUtils
import org.awaitility.Awaitility
import org.junit.Assert
@ -104,13 +105,13 @@ class PreferencesTest {
@Test
fun testEnqueueLocation() {
EspressoTestUtils.clickPreference(R.string.playback_pref)
doTestEnqueueLocation(R.string.enqueue_location_after_current, EnqueueLocation.AFTER_CURRENTLY_PLAYING)
doTestEnqueueLocation(R.string.enqueue_location_front, EnqueueLocation.FRONT)
doTestEnqueueLocation(R.string.enqueue_location_back, EnqueueLocation.BACK)
doTestEnqueueLocation(R.string.enqueue_location_random, EnqueueLocation.RANDOM)
doTestEnqueueLocation(R.string.enqueue_location_after_current, Queues.EnqueueLocation.AFTER_CURRENTLY_PLAYING)
doTestEnqueueLocation(R.string.enqueue_location_front, Queues.EnqueueLocation.FRONT)
doTestEnqueueLocation(R.string.enqueue_location_back, Queues.EnqueueLocation.BACK)
doTestEnqueueLocation(R.string.enqueue_location_random, Queues.EnqueueLocation.RANDOM)
}
private fun doTestEnqueueLocation(@StringRes optionResId: Int, expected: EnqueueLocation) {
private fun doTestEnqueueLocation(@StringRes optionResId: Int, expected: Queues.EnqueueLocation) {
EspressoTestUtils.clickPreference(R.string.pref_enqueue_location_title)
Espresso.onView(ViewMatchers.withText(optionResId)).perform(ViewActions.click())
Awaitility.await().atMost(1000, TimeUnit.MILLISECONDS)
@ -138,7 +139,7 @@ class PreferencesTest {
Espresso.onView(ViewMatchers.withText(R.string.pref_pauseOnHeadsetDisconnect_title))
.perform(ViewActions.click())
Awaitility.await().atMost(1000, TimeUnit.MILLISECONDS)
.until(UserPreferences::isPauseOnHeadsetDisconnect)
.until(PlaybackService::isPauseOnHeadsetDisconnect)
}
val unpauseOnHeadsetReconnect = isUnpauseOnHeadsetReconnect
Espresso.onView(ViewMatchers.withText(R.string.pref_unpauseOnHeadsetReconnect_title))
@ -158,7 +159,7 @@ class PreferencesTest {
Espresso.onView(ViewMatchers.withText(R.string.pref_pauseOnHeadsetDisconnect_title))
.perform(ViewActions.click())
Awaitility.await().atMost(1000, TimeUnit.MILLISECONDS)
.until(UserPreferences::isPauseOnHeadsetDisconnect)
.until(PlaybackService::isPauseOnHeadsetDisconnect)
}
val unpauseOnBluetoothReconnect = isUnpauseOnBluetoothReconnect
Espresso.onView(ViewMatchers.withText(R.string.pref_unpauseOnBluetoothReconnect_title))

View File

@ -27,7 +27,6 @@ class PodciniApp : Application() {
ClientConfig.applicationCallbacks = ApplicationCallbacksImpl()
Thread.setDefaultUncaughtExceptionHandler(CrashReportWriter())
// RxJavaErrorHandlerSetup.setupRxJavaErrorHandler()
if (BuildConfig.DEBUG) {
val builder: StrictMode.VmPolicy.Builder = StrictMode.VmPolicy.Builder()

View File

@ -1,8 +1,5 @@
package ac.mdiq.podcini.net.download
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction.NEVER
/** Utility class for Download Errors. */
/** Get machine-readable code. */
enum class DownloadError(@JvmField val code: Int) {

View File

@ -1,15 +1,14 @@
package ac.mdiq.podcini.net.download.service
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.utils.FileNameGenerator
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.utils.FilesUtils.feedfilePath
import ac.mdiq.podcini.storage.utils.FilesUtils.findUnusedFile
import ac.mdiq.podcini.storage.utils.FilesUtils.getFeedfileName
import ac.mdiq.podcini.storage.utils.FilesUtils.getMediafilePath
import ac.mdiq.podcini.storage.utils.FilesUtils.getMediafilename
import ac.mdiq.podcini.util.Logd
import android.webkit.URLUtil
import io.realm.kotlin.ext.isManaged
import org.apache.commons.io.FilenameUtils
import java.io.File
/**
@ -17,8 +16,6 @@ import java.io.File
*/
object DownloadRequestCreator {
private val TAG: String = DownloadRequestCreator::class.simpleName ?: "Anonymous"
private const val FEED_DOWNLOADPATH = "cache/"
private const val MEDIA_DOWNLOADPATH = "media/"
@JvmStatic
fun create(feed: Feed): DownloadRequest.Builder {
@ -53,83 +50,4 @@ object DownloadRequestCreator {
return DownloadRequest.Builder(dest.toString(), media).withAuthentication(username, password)
}
private fun findUnusedFile(dest: File): File? {
// find different name
var newDest: File? = null
for (i in 1 until Int.MAX_VALUE) {
val newName = (FilenameUtils.getBaseName(dest.name) + "-" + i + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(dest.name))
Logd(TAG, "Testing filename $newName")
newDest = File(dest.parent, newName)
if (!newDest.exists()) {
Logd(TAG, "File doesn't exist yet. Using $newName")
break
}
}
return newDest
}
private val feedfilePath: String
get() = UserPreferences.getDataFolder(FEED_DOWNLOADPATH).toString() + "/"
private fun getFeedfileName(feed: Feed): String {
var filename = feed.downloadUrl
if (!feed.title.isNullOrEmpty()) filename = feed.title
if (filename == null) return ""
return "feed-" + FileNameGenerator.generateFileName(filename) + feed.id
}
private fun getMediafilePath(media: EpisodeMedia): String {
val item = media.episode ?: return ""
Logd(TAG, "item managed: ${item.isManaged()}")
val title = item.feed?.title?:return ""
val mediaPath = (MEDIA_DOWNLOADPATH + FileNameGenerator.generateFileName(title))
return UserPreferences.getDataFolder(mediaPath).toString() + "/"
}
private fun getMediafilename(media: EpisodeMedia): String {
var titleBaseFilename = ""
// Try to generate the filename by the item title
if (media.episode?.title != null) {
val title = media.episode!!.title!!
titleBaseFilename = FileNameGenerator.generateFileName(title)
}
val urlBaseFilename = URLUtil.guessFileName(media.downloadUrl, null, media.mimeType)
var baseFilename: String
baseFilename = if (titleBaseFilename != "") titleBaseFilename else urlBaseFilename
val filenameMaxLength = 220
if (baseFilename.length > filenameMaxLength) baseFilename = baseFilename.substring(0, filenameMaxLength)
return (baseFilename + FilenameUtils.EXTENSION_SEPARATOR + media.id + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(urlBaseFilename))
}
fun getMediafilePath(item: Episode): String {
val title = item.feed?.title?:return ""
val mediaPath = (MEDIA_DOWNLOADPATH + FileNameGenerator.generateFileName(title))
return UserPreferences.getDataFolder(mediaPath).toString() + "/"
}
fun getMediafilename(item: Episode): String {
var titleBaseFilename = ""
// Try to generate the filename by the item title
if (item.title != null) {
val title = item.title!!
titleBaseFilename = FileNameGenerator.generateFileName(title)
}
// val urlBaseFilename = URLUtil.guessFileName(media.download_url, null, media.mime_type)
var baseFilename: String
baseFilename = if (titleBaseFilename != "") titleBaseFilename else "NoTitle"
val filenameMaxLength = 220
if (baseFilename.length > filenameMaxLength) baseFilename = baseFilename.substring(0, filenameMaxLength)
return (baseFilename + FilenameUtils.EXTENSION_SEPARATOR + "noid" + FilenameUtils.EXTENSION_SEPARATOR + "wav")
}
}

View File

@ -7,7 +7,10 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.net.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileEpisodeDownload
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.PREF_ENQUEUE_DOWNLOADED
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.LogsAndStats
import ac.mdiq.podcini.storage.database.Queues
@ -102,29 +105,35 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
private val constraints: Constraints
get() {
val constraints = Builder()
if (UserPreferences.isAllowMobileEpisodeDownload) constraints.setRequiredNetworkType(NetworkType.CONNECTED)
if (isAllowMobileEpisodeDownload) constraints.setRequiredNetworkType(NetworkType.CONNECTED)
else constraints.setRequiredNetworkType(NetworkType.UNMETERED)
return constraints.build()
}
@OptIn(UnstableApi::class) private fun getRequest(item: Episode): OneTimeWorkRequest.Builder {
@OptIn(UnstableApi::class)
private fun getRequest(item: Episode): OneTimeWorkRequest.Builder {
Logd(TAG, "starting getRequest")
val workRequest: OneTimeWorkRequest.Builder = OneTimeWorkRequest.Builder(EpisodeDownloadWorker::class.java)
.setInitialDelay(0L, TimeUnit.MILLISECONDS)
.addTag(WORK_TAG)
.addTag(WORK_TAG_EPISODE_URL + item.media!!.downloadUrl)
if (UserPreferences.enqueueDownloadedEpisodes()) {
if (enqueueDownloadedEpisodes()) {
runBlocking { Queues.addToQueueSync(false, item) }
workRequest.addTag(WORK_DATA_WAS_QUEUED)
}
workRequest.setInputData(Data.Builder().putLong(WORK_DATA_MEDIA_ID, item.media!!.id).build())
return workRequest
}
private fun enqueueDownloadedEpisodes(): Boolean {
return appPrefs.getBoolean(PREF_ENQUEUE_DOWNLOADED, true)
}
}
class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
private var downloader: Downloader? = null
private val isLastRunAttempt: Boolean
get() = runAttemptCount >= 2
@UnstableApi
override fun doWork(): Result {
@ -136,7 +145,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Log.e(TAG, "media is null for mediaId: $mediaId")
return Result.failure()
}
val request = create(media).build()
val progressUpdaterThread: Thread = object : Thread() {
override fun run() {
@ -168,7 +176,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
}
if (result == Result.failure() && downloader?.downloadRequest?.destination != null)
FileUtils.deleteQuietly(File(downloader!!.downloadRequest.destination!!))
progressUpdaterThread.interrupt()
try {
progressUpdaterThread.join()
@ -185,17 +192,15 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Logd(TAG, "Worker for " + media.downloadUrl + " returned.")
return result
}
override fun onStopped() {
super.onStopped()
downloader?.cancel()
}
override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
return Futures.immediateFuture(ForegroundInfo(R.id.notification_downloading, generateProgressNotification()))
}
@OptIn(UnstableApi::class) private fun performDownload(media: EpisodeMedia, request: DownloadRequest): Result {
@OptIn(UnstableApi::class)
private fun performDownload(media: EpisodeMedia, request: DownloadRequest): Result {
Logd(TAG, "starting performDownload")
val dest = File(request.destination)
if (!dest.exists()) {
@ -205,7 +210,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Log.e(TAG, "performDownload Unable to create file")
}
}
if (dest.exists()) {
try {
media.setfileUrlOrNull(request.destination)
@ -214,13 +218,11 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Log.e(TAG, "performDownload Exception in writeFileUrl: " + e.message)
}
}
downloader = DefaultDownloaderFactory().create(request)
if (downloader == null) {
Log.e(TAG, "performDownload Unable to create downloader")
return Result.failure()
}
try {
downloader!!.call()
} catch (e: Exception) {
@ -229,10 +231,8 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
sendErrorNotification(request.title?:"")
return Result.failure()
}
// This also happens when the worker was preempted, not just when the user cancelled it
if (downloader!!.cancelled) return Result.success()
val status = downloader!!.result
if (status.isSuccessful) {
val handler = MediaDownloadedHandler(applicationContext, downloader!!.result, request)
@ -240,14 +240,12 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
LogsAndStats.addDownloadStatus(handler.updatedStatus)
return Result.success()
}
if (status.reason == DownloadError.ERROR_HTTP_DATA_ERROR && status.reasonDetailed.toInt() == 416) {
Logd(TAG, "Requested invalid range, restarting download from the beginning")
if (downloader?.downloadRequest?.destination != null) FileUtils.deleteQuietly(File(downloader!!.downloadRequest.destination!!))
sendMessage(request.title?:"", false)
return retry3times()
}
Log.e(TAG, "Download failed ${request.title} ${status.reason}")
LogsAndStats.addDownloadStatus(status)
if (status.reason == DownloadError.ERROR_FORBIDDEN || status.reason == DownloadError.ERROR_NOT_FOUND
@ -260,7 +258,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
sendMessage(request.title?:"", false)
return retry3times()
}
private fun retry3times(): Result {
if (isLastRunAttempt) {
Log.e(TAG, "retry3times failure on isLastRunAttempt")
@ -268,10 +265,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
return Result.failure()
} else return Result.retry()
}
private val isLastRunAttempt: Boolean
get() = runAttemptCount >= 2
private fun sendMessage(episodeTitle: String, isImmediateFail: Boolean) {
var episodeTitle = episodeTitle
val retrying = !isLastRunAttempt && !isImmediateFail
@ -282,26 +275,22 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
episodeTitle), { ctx: Context -> MainActivityStarter(ctx).withDownloadLogsOpen().start() }, applicationContext.getString(
R.string.download_error_details)))
}
private fun getDownloadLogsIntent(context: Context): PendingIntent {
val intent = MainActivityStarter(context).withDownloadLogsOpen().getIntent()
return PendingIntent.getActivity(context, R.id.pending_intent_download_service_report, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
private fun getDownloadsIntent(context: Context): PendingIntent {
val intent = MainActivityStarter(context).withFragmentLoaded("DownloadsFragment").getIntent()
return PendingIntent.getActivity(context, R.id.pending_intent_download_service_notification, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
private fun sendErrorNotification(title: String) {
// TODO: need to get number of subscribers in SharedFlow
// if (EventBus.getDefault().hasSubscriberForEvent(FlowEvent.MessageEvent::class.java)) {
// sendMessage(title, false)
// return
// }
val builder = NotificationCompat.Builder(applicationContext, NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR)
builder.setTicker(applicationContext.getString(R.string.download_report_title))
.setContentTitle(applicationContext.getString(R.string.download_report_title))
@ -313,7 +302,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
val nm = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.notify(R.id.notification_download_report, builder.build())
}
private fun generateProgressNotification(): Notification {
val bigTextB = StringBuilder()
var progressCopy: Map<String, Int>
@ -326,7 +314,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
val bigText = bigTextB.toString().trim { it <= ' ' }
val contentText = if (progressCopy.size == 1) bigText
else applicationContext.resources.getQuantityString(R.plurals.downloads_left, progressCopy.size, progressCopy.size)
val builder = NotificationCompat.Builder(applicationContext, NotificationUtils.CHANNEL_ID_DOWNLOADING)
builder.setTicker(applicationContext.getString(R.string.download_notification_title_episodes))
.setContentTitle(applicationContext.getString(R.string.download_notification_title_episodes))
@ -344,7 +331,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
}
class MediaDownloadedHandler(private val context: Context, var updatedStatus: DownloadResult, private val request: DownloadRequest) : Runnable {
@UnstableApi override fun run() {
val media = Episodes.getEpisodeMedia(request.feedfileId)
if (media == null) {
@ -358,16 +344,11 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Logd(TAG, "media.episode.isNew: ${media.episode?.isNew} ${media.episode?.playState}")
media.setfileUrlOrNull(request.destination)
if (request.destination != null) media.size = File(request.destination).length()
media.checkEmbeddedPicture() // enforce check
// check if file has chapters
if (media.episode != null && media.episode!!.chapters.isEmpty())
media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context))
if (media.episode != null && media.episode!!.chapters.isEmpty()) media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context))
if (media.episode?.podcastIndexChapterUrl != null)
ChapterUtils.loadChaptersFromUrl(media.episode!!.podcastIndexChapterUrl!!, false)
// Get duration
var durationStr: String? = null
try {
@ -385,7 +366,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
}
val item = media.episode
item?.media = media
try {
// we've received the media, we don't want to autodownload it again
if (item != null) {
@ -404,7 +384,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Log.e(TAG, "ExecutionException in MediaHandlerThread: " + e.message)
updatedStatus = DownloadResult(media.id, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.message?:"")
}
if (item != null) {
val action = EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD)
.currentTimestamp()

View File

@ -7,6 +7,7 @@ import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.net.feed.parser.FeedHandler
import ac.mdiq.podcini.net.feed.parser.FeedHandlerResult
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileFeedRefresh
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.util.Logd
@ -14,6 +15,8 @@ import ac.mdiq.podcini.net.utils.NetworkUtils.isFeedRefreshAllowed
import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
import ac.mdiq.podcini.net.utils.NetworkUtils.isVpnOverWifi
import ac.mdiq.podcini.net.utils.NetworkUtils.networkAvailable
import ac.mdiq.podcini.preferences.UserPreferences.PREF_UPDATE_INTERVAL
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
import ac.mdiq.podcini.storage.database.Feeds
import ac.mdiq.podcini.storage.database.LogsAndStats
@ -53,13 +56,20 @@ import java.util.concurrent.TimeUnit
import javax.xml.parsers.ParserConfigurationException
object FeedUpdateManager {
private val TAG: String = FeedUpdateManager::class.simpleName ?: "Anonymous"
const val WORK_TAG_FEED_UPDATE: String = "feedUpdate"
private const val WORK_ID_FEED_UPDATE = "ac.mdiq.podcini.service.download.FeedUpdateWorker"
private const val WORK_ID_FEED_UPDATE_MANUAL = "feedUpdateManual"
const val EXTRA_FEED_ID: String = "feed_id"
const val EXTRA_NEXT_PAGE: String = "next_page"
const val EXTRA_EVEN_ON_MOBILE: String = "even_on_mobile"
private val TAG: String = FeedUpdateManager::class.simpleName ?: "Anonymous"
val updateInterval: Long
get() = appPrefs.getString(PREF_UPDATE_INTERVAL, "12")!!.toInt().toLong()
val isAutoUpdateDisabled: Boolean
get() = updateInterval == 0L
/**
* Start / restart periodic auto feed refresh
@ -67,12 +77,12 @@ object FeedUpdateManager {
*/
@JvmStatic
fun restartUpdateAlarm(context: Context, replace: Boolean) {
if (UserPreferences.isAutoUpdateDisabled) {
if (isAutoUpdateDisabled) {
WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE)
} else {
val workRequest: PeriodicWorkRequest = PeriodicWorkRequest.Builder(FeedUpdateWorker::class.java, UserPreferences.updateInterval, TimeUnit.HOURS)
val workRequest: PeriodicWorkRequest = PeriodicWorkRequest.Builder(FeedUpdateWorker::class.java, updateInterval, TimeUnit.HOURS)
.setConstraints(Builder()
.setRequiredNetworkType(if (UserPreferences.isAllowMobileFeedRefresh) NetworkType.CONNECTED else NetworkType.UNMETERED)
.setRequiredNetworkType(if (isAllowMobileFeedRefresh) NetworkType.CONNECTED else NetworkType.UNMETERED)
.build())
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_ID_FEED_UPDATE,
@ -116,7 +126,7 @@ object FeedUpdateManager {
.setTitle(R.string.feed_refresh_title)
.setPositiveButton(R.string.confirm_mobile_streaming_button_once) { _: DialogInterface?, _: Int -> runOnce(context, feed) }
.setNeutralButton(R.string.confirm_mobile_streaming_button_always) { _: DialogInterface?, _: Int ->
UserPreferences.isAllowMobileFeedRefresh = true
isAllowMobileFeedRefresh = true
runOnce(context, feed)
}
.setNegativeButton(R.string.no, null)

View File

@ -15,9 +15,11 @@ import ac.mdiq.podcini.net.sync.model.ISyncService
import ac.mdiq.podcini.net.sync.model.SyncServiceException
import ac.mdiq.podcini.net.sync.nextcloud.NextcloudSyncService
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueStorage
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileFor
import ac.mdiq.podcini.net.utils.NetworkUtils.setAllowMobileFor
import ac.mdiq.podcini.net.utils.UrlChecker.containsUrl
import ac.mdiq.podcini.preferences.UserPreferences.gpodnetNotificationsEnabled
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileSync
import ac.mdiq.podcini.preferences.UserPreferences.PREF_GPODNET_NOTIFICATIONS
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.Episodes.persistEpisodes
@ -27,16 +29,17 @@ import ac.mdiq.podcini.storage.database.Feeds.getFeedListDownloadUrls
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.ui.utils.NotificationUtils
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.*
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.annotation.OptIn
import androidx.collection.ArrayMap
@ -295,6 +298,11 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont
nm.cancel(R.id.notification_gpodnet_sync_autherror)
}
fun gpodnetNotificationsEnabled(): Boolean {
if (Build.VERSION.SDK_INT >= 26) return true // System handles notification preferences
return appPrefs.getBoolean(PREF_GPODNET_NOTIFICATIONS, true)
}
protected fun updateErrorNotification(exception: Exception) {
Logd(TAG, "Posting sync error notification")
val description = ("${applicationContext.getString(R.string.gpodnetsync_error_descr)}${exception.message}")
@ -347,6 +355,11 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont
internal fun setCurrentlyActive(active: Boolean) {
isCurrentlyActive = active
}
var isAllowMobileSync: Boolean
get() = isAllowMobileFor("sync")
set(allow) {
setAllowMobileFor("sync", allow)
}
private fun getWorkRequest(): OneTimeWorkRequest.Builder {
val constraints = Builder()

View File

@ -1,7 +1,8 @@
package ac.mdiq.podcini.net.sync
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.clearQueue
import ac.mdiq.podcini.preferences.UserPreferences.setGpodnetNotificationsEnabled
import ac.mdiq.podcini.preferences.UserPreferences.PREF_GPODNET_NOTIFICATIONS
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.util.config.ClientConfig
import android.content.Context
import android.content.SharedPreferences
@ -55,6 +56,10 @@ object SynchronizationCredentials {
preferences.edit().putInt(PREF_HOSTPORT, value).apply()
}
fun setGpodnetNotificationsEnabled() {
appPrefs.edit().putBoolean(PREF_GPODNET_NOTIFICATIONS, true).apply()
}
@Synchronized
fun clear(context: Context) {
username = null

View File

@ -1,10 +1,14 @@
package ac.mdiq.podcini.net.utils
import ac.mdiq.podcini.preferences.UserPreferences.PREF_AUTODL_SELECTED_NETWORKS
import ac.mdiq.podcini.preferences.UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER
import ac.mdiq.podcini.preferences.UserPreferences.PREF_MOBILE_UPDATE
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.wifi.WifiManager
import ac.mdiq.podcini.preferences.UserPreferences
import android.os.Build
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
@ -22,6 +26,39 @@ object NetworkUtils {
NetworkUtils.context = context
}
var isAllowMobileStreaming: Boolean
get() = isAllowMobileFor("streaming")
set(allow) {
setAllowMobileFor("streaming", allow)
}
var isAllowMobileAutoDownload: Boolean
get() = isAllowMobileFor("auto_download")
set(allow) {
setAllowMobileFor("auto_download", allow)
}
fun isAllowMobileFor(type: String): Boolean {
val defaultValue = HashSet<String>()
defaultValue.add("images")
val allowed = appPrefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue)
return allowed!!.contains(type)
}
fun setAllowMobileFor(type: String, allow: Boolean) {
val defaultValue = HashSet<String>()
defaultValue.add("images")
val getValueStringSet = appPrefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue)
val allowed: MutableSet<String> = HashSet(getValueStringSet!!)
if (allow) allowed.add(type)
else allowed.remove(type)
appPrefs.edit().putStringSet(PREF_MOBILE_UPDATE, allowed).apply()
}
val isEnableAutodownloadWifiFilter: Boolean
get() = Build.VERSION.SDK_INT < 29 && appPrefs.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false)
@JvmStatic
val isAutoDownloadAllowed: Boolean
get() {
@ -29,11 +66,11 @@ object NetworkUtils {
val networkInfo = cm.activeNetworkInfo ?: return false
return when (networkInfo.type) {
ConnectivityManager.TYPE_WIFI -> {
if (UserPreferences.isEnableAutodownloadWifiFilter) isInAllowedWifiNetwork
if (isEnableAutodownloadWifiFilter) isInAllowedWifiNetwork
else !isNetworkMetered
}
ConnectivityManager.TYPE_ETHERNET -> true
else -> UserPreferences.isAllowMobileAutoDownload || !isNetworkRestricted
else -> isAllowMobileAutoDownload || !isNetworkRestricted
}
}
@ -44,9 +81,21 @@ object NetworkUtils {
return info != null && info.isConnected
}
var isAllowMobileFeedRefresh: Boolean
get() = isAllowMobileFor("feed_refresh")
set(allow) {
setAllowMobileFor("feed_refresh", allow)
}
var isAllowMobileEpisodeDownload: Boolean
get() = isAllowMobileFor("episode_download")
set(allow) {
setAllowMobileFor("episode_download", allow)
}
@JvmStatic
val isEpisodeDownloadAllowed: Boolean
get() = UserPreferences.isAllowMobileEpisodeDownload || !isNetworkRestricted
get() = isAllowMobileEpisodeDownload || !isNetworkRestricted
@JvmStatic
val isEpisodeHeadDownloadAllowed: Boolean
@ -54,17 +103,23 @@ object NetworkUtils {
// that is probably not even considered a download by most users
get() = isImageAllowed
var isAllowMobileImages: Boolean
get() = isAllowMobileFor("images")
set(allow) {
setAllowMobileFor("images", allow)
}
@JvmStatic
val isImageAllowed: Boolean
get() = UserPreferences.isAllowMobileImages || !isNetworkRestricted
get() = isAllowMobileImages || !isNetworkRestricted
@JvmStatic
val isStreamingAllowed: Boolean
get() = UserPreferences.isAllowMobileStreaming || !isNetworkRestricted
get() = isAllowMobileStreaming || !isNetworkRestricted
@JvmStatic
val isFeedRefreshAllowed: Boolean
get() = UserPreferences.isAllowMobileFeedRefresh || !isNetworkRestricted
get() = isAllowMobileFeedRefresh || !isNetworkRestricted
@JvmStatic
val isNetworkRestricted: Boolean
@ -101,10 +156,16 @@ object NetworkUtils {
// }
}
val autodownloadSelectedNetworks: Array<String>
get() {
val selectedNetWorks = appPrefs.getString(PREF_AUTODL_SELECTED_NETWORKS, "")
return selectedNetWorks?.split(",")?.toTypedArray() ?: arrayOf()
}
private val isInAllowedWifiNetwork: Boolean
get() {
val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
val selectedNetworks = listOf(*UserPreferences.autodownloadSelectedNetworks)
val selectedNetworks = listOf(*autodownloadSelectedNetworks)
return selectedNetworks.contains(wm.connectionInfo.networkId.toString())
}

View File

@ -3,6 +3,10 @@ package ac.mdiq.podcini.playback.base
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.PREF_PLAYBACK_SPEED
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed
import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.FeedPreferences
import ac.mdiq.podcini.storage.model.Playable
@ -301,6 +305,17 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
@JvmField
val LONG_REWIND: Long = TimeUnit.SECONDS.toMillis(20)
val audioPlaybackSpeed: Float
get() {
try {
return appPrefs.getString(PREF_PLAYBACK_SPEED, "1.00")!!.toFloat()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
setPlaybackSpeed(1.0f)
return 1.0f
}
}
/**
* @param currentPosition current position in a media file in ms
* @param lastPlayedTime timestamp when was media paused
@ -334,8 +349,12 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
if (media.episode?.feed?.preferences != null) playbackSpeed = media.episode!!.feed!!.preferences!!.playSpeed
}
}
if (mediaType != null && playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = UserPreferences.getPlaybackSpeed(mediaType)
if (mediaType != null && playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = getPlaybackSpeed(mediaType)
return playbackSpeed
}
fun getPlaybackSpeed(mediaType: MediaType): Float {
return if (mediaType == MediaType.VIDEO) videoPlaybackSpeed else audioPlaybackSpeed
}
}
}

View File

@ -2,6 +2,7 @@ package ac.mdiq.podcini.playback.service
import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileStreaming
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.InTheatre
@ -22,43 +23,38 @@ import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo
import ac.mdiq.podcini.preferences.SleepTimerPreferences.isInTimeRange
import ac.mdiq.podcini.preferences.SleepTimerPreferences.timerMillis
import ac.mdiq.podcini.preferences.UserPreferences.PREF_FAVORITE_KEEPS_EPISODE
import ac.mdiq.podcini.preferences.UserPreferences.PREF_FOLLOW_QUEUE
import ac.mdiq.podcini.preferences.UserPreferences.PREF_HARDWARE_FORWARD_BUTTON
import ac.mdiq.podcini.preferences.UserPreferences.PREF_HARDWARE_PREVIOUS_BUTTON
import ac.mdiq.podcini.preferences.UserPreferences.PREF_PAUSE_ON_HEADSET_DISCONNECT
import ac.mdiq.podcini.preferences.UserPreferences.PREF_PERSISTENT_NOTIFICATION
import ac.mdiq.podcini.preferences.UserPreferences.PREF_SKIP_KEEPS_EPISODE
import ac.mdiq.podcini.preferences.UserPreferences.PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT
import ac.mdiq.podcini.preferences.UserPreferences.PREF_UNPAUSE_ON_HEADSET_RECONNECT
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
import ac.mdiq.podcini.preferences.UserPreferences.hardwareForwardButton
import ac.mdiq.podcini.preferences.UserPreferences.hardwarePreviousButton
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileStreaming
import ac.mdiq.podcini.preferences.UserPreferences.isFollowQueue
import ac.mdiq.podcini.preferences.UserPreferences.isPauseOnHeadsetDisconnect
import ac.mdiq.podcini.preferences.UserPreferences.isPersistNotify
import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnBluetoothReconnect
import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnHeadsetReconnect
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.preferences.UserPreferences.shouldAutoDeleteItem
import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue
import ac.mdiq.podcini.preferences.UserPreferences.shouldFavoriteKeepEpisode
import ac.mdiq.podcini.preferences.UserPreferences.shouldSkipKeepEpisode
import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.storage.database.Episodes.addToHistory
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaSync
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync
import ac.mdiq.podcini.storage.database.Episodes.persistEpisode
import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync
import ac.mdiq.podcini.storage.database.Queues.addToQueue
import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.model.CurrentState.Companion.NO_MEDIA_PLAYING
import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_OTHER
import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PAUSED
import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PLAYING
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.FeedPreferences
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting
import ac.mdiq.podcini.ui.utils.NotificationUtils
import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState
import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast
@ -82,6 +78,7 @@ import android.view.KeyEvent
import android.view.ViewConfiguration
import android.webkit.URLUtil
import android.widget.Toast
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player.STATE_ENDED
@ -331,6 +328,14 @@ class PlaybackService : MediaSessionService() {
}
}
fun shouldSkipKeepEpisode(): Boolean {
return appPrefs.getBoolean(PREF_SKIP_KEEPS_EPISODE, true)
}
fun shouldFavoriteKeepEpisode(): Boolean {
return appPrefs.getBoolean(PREF_FAVORITE_KEEPS_EPISODE, true)
}
override fun onPlaybackStart(playable: Playable, position: Int) {
Logd(TAG, "onPlaybackStart position: $position")
taskManager.startWidgetUpdater()
@ -1178,6 +1183,37 @@ class PlaybackService : MediaSessionService() {
var currentMediaType: MediaType? = MediaType.UNKNOWN
private set
/**
* @return `true` if notifications are persistent, `false` otherwise
*/
val isPersistNotify: Boolean
get() = appPrefs.getBoolean(PREF_PERSISTENT_NOTIFICATION, true)
val isPauseOnHeadsetDisconnect: Boolean
get() = appPrefs.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true)
val isUnpauseOnHeadsetReconnect: Boolean
get() = appPrefs.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true)
val isUnpauseOnBluetoothReconnect: Boolean
get() = appPrefs.getBoolean(PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT, false)
val hardwareForwardButton: Int
get() = appPrefs.getString(PREF_HARDWARE_FORWARD_BUTTON, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD.toString())!!.toInt()
val hardwarePreviousButton: Int
get() = appPrefs.getString(PREF_HARDWARE_PREVIOUS_BUTTON, KeyEvent.KEYCODE_MEDIA_REWIND.toString())!!.toInt()
/**
* Set to true to enable Continuous Playback
*/
@set:VisibleForTesting
var isFollowQueue: Boolean
get() = appPrefs.getBoolean(PREF_FOLLOW_QUEUE, true)
set(value) {
appPrefs.edit().putBoolean(PREF_FOLLOW_QUEUE, value).apply()
}
fun updateVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting) {
val playable = curMedia
if (playable is EpisodeMedia) {

View File

@ -1,12 +1,13 @@
package ac.mdiq.podcini.preferences
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
import ac.mdiq.podcini.preferences.UserPreferences.isAutoBackupOPML
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlReader
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlWriter
import ac.mdiq.podcini.preferences.UserPreferences.PREF_OPML_BACKUP
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlReader
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlWriter
import ac.mdiq.podcini.util.Logd
import android.app.backup.BackupAgentHelper
import android.app.backup.BackupDataInputStream
@ -29,6 +30,9 @@ import java.security.NoSuchAlgorithmException
class OpmlBackupAgent : BackupAgentHelper() {
val isAutoBackupOPML: Boolean
get() = appPrefs.getBoolean(PREF_OPML_BACKUP, true)
override fun onCreate() {
if (isAutoBackupOPML) {
Logd(TAG, "Backup enabled in preferences")

View File

@ -2,24 +2,16 @@ package ac.mdiq.podcini.preferences
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.ProxyConfig
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.storage.utils.FilesUtils
import ac.mdiq.podcini.storage.utils.FilesUtils.createNoMediaFile
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.util.Log
import android.view.KeyEvent
import androidx.annotation.VisibleForTesting
import androidx.preference.PreferenceManager
import org.json.JSONArray
import org.json.JSONException
import java.io.File
import java.io.IOException
import java.net.Proxy
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
/**
* Provides access to preferences set by the user in the settings screen. A
@ -30,7 +22,7 @@ import java.util.*
object UserPreferences {
private val TAG: String = UserPreferences::class.simpleName ?: "Anonymous"
private const val PREF_OPML_BACKUP = "prefOPMLBackup"
const val PREF_OPML_BACKUP = "prefOPMLBackup"
// User Interface
const val PREF_THEME: String = "prefTheme"
@ -40,39 +32,39 @@ object UserPreferences {
const val PREF_DRAWER_FEED_ORDER: String = "prefDrawerFeedOrder"
const val PREF_DRAWER_FEED_ORDER_DIRECTION: String = "prefDrawerFeedOrderDir"
const val PREF_FEED_GRID_LAYOUT: String = "prefFeedGridLayout"
const val PREF_DRAWER_FEED_COUNTER: String = "prefDrawerFeedIndicator"
// const val PREF_DRAWER_FEED_COUNTER: String = "prefDrawerFeedIndicator"
const val PREF_EXPANDED_NOTIFICATION: String = "prefExpandNotify"
private const val PREF_USE_EPISODE_COVER: String = "prefEpisodeCover"
const val PREF_USE_EPISODE_COVER: String = "prefEpisodeCover"
const val PREF_SHOW_TIME_LEFT: String = "showTimeLeft"
private const val PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"
const val PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"
const val PREF_FULL_NOTIFICATION_BUTTONS: String = "prefFullNotificationButtons"
private const val PREF_SHOW_DOWNLOAD_REPORT = "prefShowDownloadReport"
const val PREF_SHOW_DOWNLOAD_REPORT = "prefShowDownloadReport"
const val PREF_DEFAULT_PAGE: String = "prefDefaultPage"
private const val PREF_BACK_OPENS_DRAWER: String = "prefBackButtonOpensDrawer"
private const val PREF_QUEUE_KEEP_SORTED: String = "prefQueueKeepSorted"
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"
const val PREF_QUEUE_KEEP_SORTED: String = "prefQueueKeepSorted"
const val PREF_QUEUE_KEEP_SORTED_ORDER: String = "prefQueueKeepSortedOrder"
const val PREF_DOWNLOADS_SORTED_ORDER = "prefDownloadSortedOrder"
// private const val PREF_HISTORY_SORTED_ORDER = "prefHistorySortedOrder"
// Episodes
private const val PREF_SORT_ALL_EPISODES: String = "prefEpisodesSort"
private const val PREF_FILTER_ALL_EPISODES: String = "prefEpisodesFilter"
const val PREF_SORT_ALL_EPISODES: String = "prefEpisodesSort"
const val PREF_FILTER_ALL_EPISODES: String = "prefEpisodesFilter"
// Playback
private const val PREF_PAUSE_ON_HEADSET_DISCONNECT: String = "prefPauseOnHeadsetDisconnect"
const val PREF_PAUSE_ON_HEADSET_DISCONNECT: String = "prefPauseOnHeadsetDisconnect"
const val PREF_UNPAUSE_ON_HEADSET_RECONNECT: String = "prefUnpauseOnHeadsetReconnect"
const val PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT: String = "prefUnpauseOnBluetoothReconnect"
private const val PREF_HARDWARE_FORWARD_BUTTON: String = "prefHardwareForwardButton"
private const val PREF_HARDWARE_PREVIOUS_BUTTON: String = "prefHardwarePreviousButton"
const val PREF_HARDWARE_FORWARD_BUTTON: String = "prefHardwareForwardButton"
const val PREF_HARDWARE_PREVIOUS_BUTTON: String = "prefHardwarePreviousButton"
const val PREF_FOLLOW_QUEUE: String = "prefFollowQueue"
const val PREF_SKIP_KEEPS_EPISODE: String = "prefSkipKeepsEpisode"
const val PREF_REMOVDE_FROM_QUEUE_MARKED_PLAYED: String = "prefRemoveFromQueueMarkedPlayed"
private const val PREF_FAVORITE_KEEPS_EPISODE = "prefFavoriteKeepsEpisode"
const val PREF_FAVORITE_KEEPS_EPISODE = "prefFavoriteKeepsEpisode"
private const val PREF_AUTO_DELETE = "prefAutoDelete"
private const val PREF_AUTO_DELETE_LOCAL = "prefAutoDeleteLocal"
const val PREF_SMART_MARK_AS_PLAYED_SECS: String = "prefSmartMarkAsPlayedSecs"
private const val PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"
const val PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"
private const val PREF_FALLBACK_SPEED = "prefFallbackSpeed"
private const val PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS: String = "prefPauseForFocusLoss"
private const val PREF_TIME_RESPECTS_SPEED = "prefPlaybackTimeRespectsSpeed"
@ -80,16 +72,16 @@ object UserPreferences {
private const val PREF_SPEEDFORWRD_SPEED = "prefSpeedforwardSpeed"
// Network
private const val PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded"
const val PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded"
const val PREF_ENQUEUE_LOCATION: String = "prefEnqueueLocation"
const val PREF_UPDATE_INTERVAL: String = "prefAutoUpdateIntervall"
private const val PREF_MOBILE_UPDATE = "prefMobileUpdateTypes"
const val PREF_MOBILE_UPDATE = "prefMobileUpdateTypes"
const val PREF_EPISODE_CLEANUP: String = "prefEpisodeCleanup"
const val PREF_EPISODE_CACHE_SIZE: String = "prefEpisodeCacheSize"
const val PREF_ENABLE_AUTODL: String = "prefEnableAutoDl"
const val PREF_ENABLE_AUTODL_ON_BATTERY: String = "prefEnableAutoDownloadOnBattery"
const val PREF_ENABLE_AUTODL_WIFI_FILTER: String = "prefEnableAutoDownloadWifiFilter"
private const val PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks"
const val PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks"
private const val PREF_PROXY_TYPE = "prefProxyType"
private const val PREF_PROXY_HOST = "prefProxyHost"
private const val PREF_PROXY_PORT = "prefProxyPort"
@ -97,19 +89,19 @@ object UserPreferences {
private const val PREF_PROXY_PASSWORD = "prefProxyPassword"
// Services
private const val PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications"
const val PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications"
// Other
private const val PREF_DATA_FOLDER = "prefDataFolder"
// const val PREF_DATA_FOLDER = "prefDataFolder"
const val PREF_DELETE_REMOVES_FROM_QUEUE: String = "prefDeleteRemovesFromQueue"
// Mediaplayer
private const val PREF_PLAYBACK_SPEED = "prefPlaybackSpeed"
const val PREF_PLAYBACK_SPEED = "prefPlaybackSpeed"
private const val PREF_VIDEO_PLAYBACK_SPEED = "prefVideoPlaybackSpeed"
private const val PREF_PLAYBACK_SKIP_SILENCE: String = "prefSkipSilence"
private const val PREF_FAST_FORWARD_SECS = "prefFastForwardSecs"
private const val PREF_REWIND_SECS = "prefRewindSecs"
private const val PREF_QUEUE_LOCKED = "prefQueueLocked"
const val PREF_QUEUE_LOCKED = "prefQueueLocked"
private const val PREF_VIDEO_MODE = "prefVideoPlaybackMode"
// Experimental
@ -141,7 +133,6 @@ object UserPreferences {
private lateinit var context: Context
lateinit var appPrefs: SharedPreferences
var theme: ThemePreference
get() = when (appPrefs.getString(PREF_THEME, "system")) {
"0" -> ThemePreference.LIGHT
@ -190,103 +181,12 @@ object UserPreferences {
.apply()
}
val isAutoBackupOPML: Boolean
get() = appPrefs.getBoolean(PREF_OPML_BACKUP, true)
val feedOrderBy: Int
get() {
val value = appPrefs.getString(PREF_DRAWER_FEED_ORDER, "" + FEED_ORDER_UNPLAYED)
return value!!.toInt()
}
val feedOrderDir: Int
get() {
val value = appPrefs.getInt(PREF_DRAWER_FEED_ORDER_DIRECTION, 0)
return value
}
val useGridLayout: Boolean
get() = appPrefs.getBoolean(PREF_FEED_GRID_LAYOUT, false)
/**
* @return `true` if episodes should use their own cover, `false` otherwise
*/
val useEpisodeCoverSetting: Boolean
get() = appPrefs.getBoolean(PREF_USE_EPISODE_COVER, true)
/**
* @return `true` if notifications are persistent, `false` otherwise
*/
val isPersistNotify: Boolean
get() = appPrefs.getBoolean(PREF_PERSISTENT_NOTIFICATION, true)
/**
* Used for migration of the preference to system notification channels.
*/
val showDownloadReportRaw: Boolean
get() = appPrefs.getBoolean(PREF_SHOW_DOWNLOAD_REPORT, true)
var enqueueLocation: EnqueueLocation
get() {
val valStr = appPrefs.getString(PREF_ENQUEUE_LOCATION, EnqueueLocation.BACK.name)
try {
return EnqueueLocation.valueOf(valStr!!)
} catch (t: Throwable) {
// should never happen but just in case
Log.e(TAG, "getEnqueueLocation: invalid value '$valStr' Use default.", t)
return EnqueueLocation.BACK
}
}
set(location) {
appPrefs.edit().putString(PREF_ENQUEUE_LOCATION, location.name).apply()
}
val isPauseOnHeadsetDisconnect: Boolean
get() = appPrefs.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true)
val isUnpauseOnHeadsetReconnect: Boolean
get() = appPrefs.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true)
val isUnpauseOnBluetoothReconnect: Boolean
get() = appPrefs.getBoolean(PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT, false)
val hardwareForwardButton: Int
get() = appPrefs.getString(PREF_HARDWARE_FORWARD_BUTTON, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD.toString())!!.toInt()
val hardwarePreviousButton: Int
get() = appPrefs.getString(PREF_HARDWARE_PREVIOUS_BUTTON, KeyEvent.KEYCODE_MEDIA_REWIND.toString())!!.toInt()
/**
* Set to true to enable Continuous Playback
*/
@set:VisibleForTesting
var isFollowQueue: Boolean
get() = appPrefs.getBoolean(PREF_FOLLOW_QUEUE, true)
set(value) {
appPrefs.edit().putBoolean(PREF_FOLLOW_QUEUE, value).apply()
}
val isAutoDelete: Boolean
get() = appPrefs.getBoolean(PREF_AUTO_DELETE, false)
val isAutoDeleteLocal: Boolean
get() = appPrefs.getBoolean(PREF_AUTO_DELETE_LOCAL, false)
val smartMarkAsPlayedSecs: Int
get() = appPrefs.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")!!.toInt()
private val audioPlaybackSpeed: Float
get() {
try {
return appPrefs.getString(PREF_PLAYBACK_SPEED, "1.00")!!.toFloat()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
setPlaybackSpeed(1.0f)
return 1.0f
}
}
val videoPlayMode: Int
get() {
try {
@ -320,61 +220,6 @@ object UserPreferences {
appPrefs.edit().putBoolean(PREF_PLAYBACK_SKIP_SILENCE, skipSilence).apply()
}
var playbackSpeedArray: List<Float>
get() = readPlaybackSpeedArray(appPrefs.getString(PREF_PLAYBACK_SPEED_ARRAY, null))
set(speeds) {
val format = DecimalFormatSymbols(Locale.US)
format.decimalSeparator = '.'
val speedFormat = DecimalFormat("0.00", format)
val jsonArray = JSONArray()
for (speed in speeds) {
jsonArray.put(speedFormat.format(speed.toDouble()))
}
appPrefs.edit().putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString()).apply()
}
val updateInterval: Long
get() = appPrefs.getString(PREF_UPDATE_INTERVAL, "12")!!.toInt().toLong()
val isAutoUpdateDisabled: Boolean
get() = updateInterval == 0L
var isAllowMobileFeedRefresh: Boolean
get() = isAllowMobileFor("feed_refresh")
set(allow) {
setAllowMobileFor("feed_refresh", allow)
}
var isAllowMobileSync: Boolean
get() = isAllowMobileFor("sync")
set(allow) {
setAllowMobileFor("sync", allow)
}
var isAllowMobileEpisodeDownload: Boolean
get() = isAllowMobileFor("episode_download")
set(allow) {
setAllowMobileFor("episode_download", allow)
}
var isAllowMobileAutoDownload: Boolean
get() = isAllowMobileFor("auto_download")
set(allow) {
setAllowMobileFor("auto_download", allow)
}
var isAllowMobileStreaming: Boolean
get() = isAllowMobileFor("streaming")
set(allow) {
setAllowMobileFor("streaming", allow)
}
var isAllowMobileImages: Boolean
get() = isAllowMobileFor("images")
set(allow) {
setAllowMobileFor("images", allow)
}
/**
* Returns the capacity of the episode cache. This method will return the
* negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to
@ -393,9 +238,6 @@ object UserPreferences {
val isEnableAutodownloadOnBattery: Boolean
get() = appPrefs.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true)
val isEnableAutodownloadWifiFilter: Boolean
get() = Build.VERSION.SDK_INT < 29 && appPrefs.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false)
var speedforwardSpeed: Float
get() {
try {
@ -436,12 +278,6 @@ object UserPreferences {
appPrefs.edit().putInt(PREF_REWIND_SECS, secs).apply()
}
val autodownloadSelectedNetworks: Array<String>
get() {
val selectedNetWorks = appPrefs.getString(PREF_AUTODL_SELECTED_NETWORKS, "")
return selectedNetWorks?.split(",")?.toTypedArray() ?: arrayOf()
}
var proxyConfig: ProxyConfig
get() {
val type = Proxy.Type.valueOf(appPrefs.getString(PREF_PROXY_TYPE, Proxy.Type.DIRECT.name)!!)
@ -469,24 +305,6 @@ object UserPreferences {
editor.apply()
}
var isQueueLocked: Boolean
get() = appPrefs.getBoolean(PREF_QUEUE_LOCKED, false)
set(locked) {
appPrefs.edit().putBoolean(PREF_QUEUE_LOCKED, locked).apply()
}
/**
* Used for migration of the preference to system notification channels.
*/
val gpodnetNotificationsEnabledRaw: Boolean
get() = appPrefs.getBoolean(PREF_GPODNET_NOTIFICATIONS, true)
var episodeCleanupValue: Int
get() = appPrefs.getString(PREF_EPISODE_CLEANUP, "" + EPISODE_CLEANUP_NULL)!!.toInt()
set(episodeCleanupValue) {
appPrefs.edit().putString(PREF_EPISODE_CLEANUP, episodeCleanupValue.toString()).apply()
}
var defaultPage: String?
get() = appPrefs.getString(PREF_DEFAULT_PAGE, "SubscriptionsFragment")
set(defaultPage) {
@ -499,61 +317,6 @@ object UserPreferences {
appPrefs.edit().putBoolean(PREF_STREAM_OVER_DOWNLOAD, stream).apply()
}
var isQueueKeepSorted: Boolean
/**
* Returns if the queue is in keep sorted mode.
* @see .queueKeepSortedOrder
*/
get() = appPrefs.getBoolean(PREF_QUEUE_KEEP_SORTED, false)
/**
* Enables/disables the keep sorted mode of the queue.
* @see .queueKeepSortedOrder
*/
set(keepSorted) {
appPrefs.edit().putBoolean(PREF_QUEUE_KEEP_SORTED, keepSorted).apply()
}
var queueKeepSortedOrder: EpisodeSortOrder?
/**
* Returns the sort order for the queue keep sorted mode.
* Note: This value is stored independently from the keep sorted state.
* @see .isQueueKeepSorted
*/
get() {
val sortOrderStr = appPrefs.getString(PREF_QUEUE_KEEP_SORTED_ORDER, "use-default")
return EpisodeSortOrder.parseWithDefault(sortOrderStr, EpisodeSortOrder.DATE_NEW_OLD)
}
/**
* Sets the sort order for the queue keep sorted mode.
* @see .setQueueKeepSorted
*/
set(sortOrder) {
if (sortOrder == null) return
appPrefs.edit().putString(PREF_QUEUE_KEEP_SORTED_ORDER, sortOrder.name).apply()
}
// the sort order for the downloads.
var downloadsSortedOrder: EpisodeSortOrder?
get() {
val sortOrderStr = appPrefs.getString(PREF_DOWNLOADS_SORTED_ORDER, "" + EpisodeSortOrder.DATE_NEW_OLD.code)
return EpisodeSortOrder.fromCodeString(sortOrderStr)
}
set(sortOrder) {
appPrefs.edit().putString(PREF_DOWNLOADS_SORTED_ORDER, "" + sortOrder!!.code).apply()
}
var allEpisodesSortOrder: EpisodeSortOrder?
get() = EpisodeSortOrder.fromCodeString(appPrefs.getString(PREF_SORT_ALL_EPISODES, "" + EpisodeSortOrder.DATE_NEW_OLD.code))
set(s) {
appPrefs.edit().putString(PREF_SORT_ALL_EPISODES, "" + s!!.code).apply()
}
var prefFilterAllEpisodes: String
get() = appPrefs.getString(PREF_FILTER_ALL_EPISODES, "")?:""
set(filter) {
appPrefs.edit().putString(PREF_FILTER_ALL_EPISODES, filter).apply()
}
/**
* Sets up the UserPreferences class.
* @throws IllegalArgumentException if context is null
@ -561,6 +324,7 @@ object UserPreferences {
fun init(context: Context) {
Logd(TAG, "Creating new instance of UserPreferences")
UserPreferences.context = context.applicationContext
FilesUtils.context = context.applicationContext
appPrefs = PreferenceManager.getDefaultSharedPreferences(context)
createNoMediaFile()
@ -606,19 +370,6 @@ object UserPreferences {
return appPrefs.getBoolean(PREF_SHOW_TIME_LEFT, false)
}
fun setFeedOrder(selected: String, dir: Int) {
appPrefs.edit()
.putString(PREF_DRAWER_FEED_ORDER, selected)
.apply()
appPrefs.edit()
.putInt(PREF_DRAWER_FEED_ORDER_DIRECTION, dir)
.apply()
}
fun enqueueDownloadedEpisodes(): Boolean {
return appPrefs.getBoolean(PREF_ENQUEUE_DOWNLOADED, true)
}
/**
* Sets the preference for whether we show the remain time, if not show the duration. This will
* send out events so the current playing screen, queue and the episode list would refresh
@ -628,49 +379,15 @@ object UserPreferences {
appPrefs.edit().putBoolean(PREF_SHOW_TIME_LEFT, showRemain!!).apply()
}
fun shouldSkipKeepEpisode(): Boolean {
return appPrefs.getBoolean(PREF_SKIP_KEEPS_EPISODE, true)
}
fun shouldRemoveFromQueuesMarkPlayed(): Boolean {
return appPrefs.getBoolean(PREF_REMOVDE_FROM_QUEUE_MARKED_PLAYED, true)
}
fun shouldFavoriteKeepEpisode(): Boolean {
return appPrefs.getBoolean(PREF_FAVORITE_KEEPS_EPISODE, true)
}
fun shouldDeleteRemoveFromQueue(): Boolean {
return appPrefs.getBoolean(PREF_DELETE_REMOVES_FROM_QUEUE, false)
}
fun getPlaybackSpeed(mediaType: MediaType): Float {
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)
}
private fun isAllowMobileFor(type: String): Boolean {
val defaultValue = HashSet<String>()
defaultValue.add("images")
val allowed = appPrefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue)
return allowed!!.contains(type)
}
private fun setAllowMobileFor(type: String, allow: Boolean) {
val defaultValue = HashSet<String>()
defaultValue.add("images")
val getValueStringSet = appPrefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue)
val allowed: MutableSet<String> = HashSet(getValueStringSet!!)
if (allow) allowed.add(type)
else allowed.remove(type)
appPrefs.edit().putStringSet(PREF_MOBILE_UPDATE, allowed).apply()
}
fun backButtonOpensDrawer(): Boolean {
return appPrefs.getBoolean(PREF_BACK_OPENS_DRAWER, false)
}
@ -687,101 +404,7 @@ object UserPreferences {
appPrefs.edit().putString(PREF_VIDEO_MODE, mode.toString()).apply()
}
fun setAutodownloadSelectedNetworks(value: Array<String?>?) {
appPrefs.edit().putString(PREF_AUTODL_SELECTED_NETWORKS, value!!.joinToString()).apply()
}
fun gpodnetNotificationsEnabled(): Boolean {
if (Build.VERSION.SDK_INT >= 26) return true // System handles notification preferences
return appPrefs.getBoolean(PREF_GPODNET_NOTIFICATIONS, true)
}
fun setGpodnetNotificationsEnabled() {
appPrefs.edit().putBoolean(PREF_GPODNET_NOTIFICATIONS, true).apply()
}
private fun readPlaybackSpeedArray(valueFromPrefs: String?): List<Float> {
if (valueFromPrefs != null) {
try {
val jsonArray = JSONArray(valueFromPrefs)
val selectedSpeeds: MutableList<Float> = ArrayList()
for (i in 0 until jsonArray.length()) {
selectedSpeeds.add(jsonArray.getDouble(i).toFloat())
}
return selectedSpeeds
} catch (e: JSONException) {
Log.e(TAG, "Got JSON error when trying to get speeds from JSONArray")
e.printStackTrace()
}
}
// If this preference hasn't been set yet, return the default options
return mutableListOf(1.0f, 1.25f, 1.5f)
}
/**
* Return the folder where the app stores all of its data. This method will
* return the standard data folder if none has been set by the user.
* @param type The name of the folder inside the data folder. May be null
* when accessing the root of the data folder.
* @return The data folder that has been requested or null if the folder could not be created.
*/
fun getDataFolder(type: String?): File? {
var dataFolder = getTypeDir(appPrefs.getString(PREF_DATA_FOLDER, null), type)
if (dataFolder == null || !dataFolder.canWrite()) {
Logd(TAG, "User data folder not writable or not set. Trying default.")
dataFolder = context.getExternalFilesDir(type)
}
if (dataFolder == null || !dataFolder.canWrite()) {
Logd(TAG, "Default data folder not available or not writable. Falling back to internal memory.")
dataFolder = getTypeDir(context.filesDir.absolutePath, type)
}
return dataFolder
}
private fun getTypeDir(baseDirPath: String?, type: String?): File? {
if (baseDirPath == null) return null
val baseDir = File(baseDirPath)
val typeDir = if (type == null) baseDir else File(baseDir, type)
if (!typeDir.exists()) {
if (!baseDir.canWrite()) {
Log.e(TAG, "Base dir is not writable " + baseDir.absolutePath)
return null
}
if (!typeDir.mkdirs()) {
Log.e(TAG, "Could not create type dir " + typeDir.absolutePath)
return null
}
}
return typeDir
}
fun setDataFolder(dir: String) {
Logd(TAG, "setDataFolder(dir: $dir)")
appPrefs.edit().putString(PREF_DATA_FOLDER, dir).apply()
}
/**
* Create a .nomedia file to prevent scanning by the media scanner.
*/
private fun createNoMediaFile() {
val f = File(context.getExternalFilesDir(null), ".nomedia")
if (!f.exists()) {
try {
f.createNewFile()
} catch (e: IOException) {
Log.e(TAG, "Could not create .nomedia file")
e.printStackTrace()
}
Logd(TAG, ".nomedia file created")
}
}
enum class ThemePreference {
LIGHT, DARK, BLACK, SYSTEM
}
enum class EnqueueLocation {
BACK, FRONT, AFTER_CURRENTLY_PLAYING, RANDOM
}
}

View File

@ -13,12 +13,13 @@ import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.utils.NetworkUtils.autodownloadSelectedNetworks
import ac.mdiq.podcini.net.utils.NetworkUtils.isEnableAutodownloadWifiFilter
import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.autodownloadSelectedNetworks
import ac.mdiq.podcini.preferences.UserPreferences.PREF_AUTODL_SELECTED_NETWORKS
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadWifiFilter
import ac.mdiq.podcini.preferences.UserPreferences.setAutodownloadSelectedNetworks
import ac.mdiq.podcini.util.Logd
import java.util.*
@ -126,6 +127,10 @@ class AutoDownloadPreferencesFragment : PreferenceFragmentCompat() {
}
}
fun setAutodownloadSelectedNetworks(value: Array<String?>?) {
appPrefs.edit().putString(PREF_AUTODL_SELECTED_NETWORKS, value!!.joinToString()).apply()
}
private fun clearAutodownloadSelectedNetworsPreference() {
if (selectedNetworks != null) {
val prefScreen = preferenceScreen

View File

@ -4,16 +4,15 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ChooseDataFolderDialogBinding
import ac.mdiq.podcini.databinding.ChooseDataFolderDialogEntryBinding
import ac.mdiq.podcini.databinding.ProxySettingsBinding
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
import ac.mdiq.podcini.net.download.service.PodciniHttpClient
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.newBuilder
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.reinit
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.getDataFolder
import ac.mdiq.podcini.preferences.UserPreferences.proxyConfig
import ac.mdiq.podcini.preferences.UserPreferences.setDataFolder
import ac.mdiq.podcini.storage.model.ProxyConfig
import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
import ac.mdiq.podcini.storage.utils.StorageUtils.getFreeSpaceAvailable
import ac.mdiq.podcini.storage.utils.StorageUtils.getTotalSpaceAvailable
import ac.mdiq.podcini.ui.activity.PreferenceActivity
@ -79,7 +78,7 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
override fun onResume() {
super.onResume()
setDataFolderText()
// setDataFolderText()
}
private fun setupNetworkScreen() {
@ -93,13 +92,13 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
dialog.show()
true
}
findPreference<Preference>(PREF_CHOOSE_DATA_DIR)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
ChooseDataFolderDialog.showDialog(requireContext()) { path: String? ->
setDataFolder(path!!)
setDataFolderText()
}
true
}
// findPreference<Preference>(PREF_CHOOSE_DATA_DIR)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
// ChooseDataFolderDialog.showDialog(requireContext()) { path: String? ->
// setDataFolder(path!!)
//// setDataFolderText()
// }
// true
// }
findPreference<Preference>(PREF_AUTO_DELETE_LOCAL)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
if (blockAutoDeleteLocal && newValue as Boolean) {
showAutoDeleteEnableDialog()
@ -108,10 +107,10 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
}
}
private fun setDataFolderText() {
val f = getDataFolder(null)
if (f != null) findPreference<Preference>(PREF_CHOOSE_DATA_DIR)!!.summary = f.absolutePath
}
// private fun setDataFolderText() {
// val f = getDataFolder(null)
// if (f != null) findPreference<Preference>(PREF_CHOOSE_DATA_DIR)!!.summary = f.absolutePath
// }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
if (UserPreferences.PREF_UPDATE_INTERVAL == key) restartUpdateAlarm(requireContext(), true)
@ -444,6 +443,6 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
private const val PREF_SCREEN_AUTODL = "prefAutoDownloadSettings"
private const val PREF_AUTO_DELETE_LOCAL = "prefAutoDeleteLocal"
private const val PREF_PROXY = "prefProxy"
private const val PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"
// private const val PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"
}
}

View File

@ -9,7 +9,6 @@ import ac.mdiq.podcini.net.sync.model.EpisodeAction.Companion.readFromJsonObject
import ac.mdiq.podcini.net.sync.model.SyncServiceException
import ac.mdiq.podcini.preferences.ExportWriter
import ac.mdiq.podcini.preferences.OpmlTransporter.*
import ac.mdiq.podcini.preferences.UserPreferences.getDataFolder
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
@ -21,6 +20,7 @@ import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.storage.utils.FileNameGenerator.generateFileName
import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
import ac.mdiq.podcini.ui.activity.OpmlImportActivity
import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.util.Logd
@ -102,10 +102,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
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())
}
result: ActivityResult -> this.exportMediaFilesResult(result)
}
private var progressDialog: ProgressDialog? = null
@ -194,7 +191,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
showExportSuccessSnackbar(fileUri, exportType.contentType)
}
} catch (e: Exception) {
showExportErrorDialog(e)
showTransportErrorDialog(e)
} finally {
progressDialog!!.dismiss()
}
@ -208,7 +205,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
showExportSuccessSnackbar(output.uri, exportType.contentType)
}
} catch (e: Exception) {
showExportErrorDialog(e)
showTransportErrorDialog(e)
} finally {
progressDialog!!.dismiss()
}
@ -290,7 +287,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
builder.show()
}
private fun showDatabaseImportSuccessDialog() {
private fun showImportSuccessDialog() {
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(R.string.successful_import_label)
builder.setMessage(R.string.import_ok)
@ -305,11 +302,11 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
.show()
}
private fun showExportErrorDialog(error: Throwable) {
private fun showTransportErrorDialog(error: Throwable) {
progressDialog!!.dismiss()
val alert = MaterialAlertDialogBuilder(requireContext())
alert.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
alert.setTitle(R.string.export_error_label)
alert.setTitle(R.string.import_export_error_label)
alert.setMessage(error.message)
alert.show()
}
@ -371,17 +368,17 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
reader.close()
}
withContext(Dispatchers.Main) {
showDatabaseImportSuccessDialog()
showImportSuccessDialog()
progressDialog!!.dismiss()
}
} catch (e: Throwable) {
showExportErrorDialog(e)
showTransportErrorDialog(e)
}
}
} else {
val context = requireContext()
val message = context.getString(R.string.import_file_type_toast) + ".json"
showExportErrorDialog(Throwable(message))
showTransportErrorDialog(Throwable(message))
}
}
}
@ -403,17 +400,17 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
DatabaseTransporter.importBackup(uri, requireContext())
}
withContext(Dispatchers.Main) {
showDatabaseImportSuccessDialog()
showImportSuccessDialog()
progressDialog!!.dismiss()
}
} catch (e: Throwable) {
showExportErrorDialog(e)
showTransportErrorDialog(e)
}
}
} else {
val context = requireContext()
val message = context.getString(R.string.import_file_type_toast) + ".realm"
showExportErrorDialog(Throwable(message))
showTransportErrorDialog(Throwable(message))
}
}
}
@ -444,17 +441,17 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
PreferencesTransporter.importBackup(uri, requireContext())
}
withContext(Dispatchers.Main) {
showDatabaseImportSuccessDialog()
showImportSuccessDialog()
progressDialog!!.dismiss()
}
} catch (e: Throwable) {
showExportErrorDialog(e)
showTransportErrorDialog(e)
}
}
} else {
val context = requireContext()
val message = context.getString(R.string.import_directory_toast) + "Podcini-Prefs"
showExportErrorDialog(Throwable(message))
showTransportErrorDialog(Throwable(message))
}
}
@ -469,20 +466,38 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
MediaFilesTransporter.importBackup(uri, requireContext())
}
withContext(Dispatchers.Main) {
showDatabaseImportSuccessDialog()
showImportSuccessDialog()
progressDialog!!.dismiss()
}
} catch (e: Throwable) {
showExportErrorDialog(e)
showTransportErrorDialog(e)
}
}
} else {
val context = requireContext()
val message = context.getString(R.string.import_directory_toast) + "Podcini-MediaFiles"
showExportErrorDialog(Throwable(message))
showTransportErrorDialog(Throwable(message))
}
}
private fun exportMediaFilesResult(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) {
MediaFilesTransporter.exportToDocument(uri, requireContext())
}
withContext(Dispatchers.Main) {
showExportSuccessSnackbar(uri, null)
progressDialog!!.dismiss()
}
} catch (e: Throwable) {
showTransportErrorDialog(e)
}
}
}
private fun backupDatabaseResult(uri: Uri?) {
if (uri == null) return
@ -497,7 +512,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
progressDialog!!.dismiss()
}
} catch (e: Throwable) {
showExportErrorDialog(e)
showTransportErrorDialog(e)
}
}
}

View File

@ -1,8 +1,10 @@
package ac.mdiq.podcini.storage.algorithms
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.EPISODE_CLEANUP_NULL
import ac.mdiq.podcini.preferences.UserPreferences.PREF_EPISODE_CLEANUP
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
import ac.mdiq.podcini.preferences.UserPreferences.episodeCleanupValue
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaOfEpisode
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
@ -22,6 +24,12 @@ import java.util.concurrent.ExecutionException
object AutoCleanups {
var episodeCleanupValue: Int
get() = appPrefs.getString(PREF_EPISODE_CLEANUP, "" + EPISODE_CLEANUP_NULL)!!.toInt()
set(episodeCleanupValue) {
appPrefs.edit().putString(PREF_EPISODE_CLEANUP, episodeCleanupValue.toString()).apply()
}
/**
* Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller
* 'playbackCompletionDate'-value will be deleted first.

View File

@ -9,8 +9,9 @@ import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.ACTION_SHUTDOWN_PLAYBACK_SERVICE
import ac.mdiq.podcini.preferences.UserPreferences.PREF_REMOVDE_FROM_QUEUE_MARKED_PLAYED
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue
import ac.mdiq.podcini.preferences.UserPreferences.shouldRemoveFromQueuesMarkPlayed
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesSync
import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync
import ac.mdiq.podcini.storage.database.RealmDB.realm
@ -84,12 +85,6 @@ object Episodes {
return if (episode != null) realm.copyFromRealm(episode) else null
}
fun getEpisodeByTitle(title: String): Episode? {
Logd(TAG, "getEpisodeByTitle called $title ")
val episode = realm.query(Episode::class).query("title == $0", title).first().find()
return if (episode != null) realm.copyFromRealm(episode) else null
}
fun getEpisodeMedia(mediaId: Long): EpisodeMedia? {
Logd(TAG, "getEpisodeMedia called $mediaId")
val media = realm.query(EpisodeMedia::class).query("id == $0", mediaId).first().find()
@ -290,4 +285,8 @@ object Episodes {
EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(result))
return result
}
private fun shouldRemoveFromQueuesMarkPlayed(): Boolean {
return appPrefs.getBoolean(PREF_REMOVDE_FROM_QUEUE_MARKED_PLAYED, true)
}
}

View File

@ -3,21 +3,18 @@ package ac.mdiq.podcini.storage.database
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences.EnqueueLocation
import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation
import ac.mdiq.podcini.preferences.UserPreferences.isQueueKeepSorted
import ac.mdiq.podcini.preferences.UserPreferences.queueKeepSortedOrder
import ac.mdiq.podcini.preferences.UserPreferences.PREF_ENQUEUE_LOCATION
import ac.mdiq.podcini.preferences.UserPreferences.PREF_QUEUE_KEEP_SORTED
import ac.mdiq.podcini.preferences.UserPreferences.PREF_QUEUE_KEEP_SORTED_ORDER
import ac.mdiq.podcini.preferences.UserPreferences.PREF_QUEUE_LOCKED
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.PlayQueue
import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
@ -32,6 +29,10 @@ import java.util.*
object Queues {
private val TAG: String = Queues::class.simpleName ?: "Anonymous"
enum class EnqueueLocation {
BACK, FRONT, AFTER_CURRENTLY_PLAYING, RANDOM
}
fun getInQueueEpisodeIds(): Set<Long> {
Logd(TAG, "getQueueIDList() called")
val queues = realm.query(PlayQueue::class).find()
@ -273,6 +274,60 @@ object Queues {
upsertBlk(curQueue) {}
}
var isQueueLocked: Boolean
get() = appPrefs.getBoolean(PREF_QUEUE_LOCKED, false)
set(locked) {
appPrefs.edit().putBoolean(PREF_QUEUE_LOCKED, locked).apply()
}
var isQueueKeepSorted: Boolean
/**
* Returns if the queue is in keep sorted mode.
* @see .queueKeepSortedOrder
*/
get() = appPrefs.getBoolean(PREF_QUEUE_KEEP_SORTED, false)
/**
* Enables/disables the keep sorted mode of the queue.
* @see .queueKeepSortedOrder
*/
set(keepSorted) {
appPrefs.edit().putBoolean(PREF_QUEUE_KEEP_SORTED, keepSorted).apply()
}
var queueKeepSortedOrder: EpisodeSortOrder?
/**
* Returns the sort order for the queue keep sorted mode.
* Note: This value is stored independently from the keep sorted state.
* @see .isQueueKeepSorted
*/
get() {
val sortOrderStr = appPrefs.getString(PREF_QUEUE_KEEP_SORTED_ORDER, "use-default")
return EpisodeSortOrder.parseWithDefault(sortOrderStr, EpisodeSortOrder.DATE_NEW_OLD)
}
/**
* Sets the sort order for the queue keep sorted mode.
* @see .setQueueKeepSorted
*/
set(sortOrder) {
if (sortOrder == null) return
appPrefs.edit().putString(PREF_QUEUE_KEEP_SORTED_ORDER, sortOrder.name).apply()
}
var enqueueLocation: EnqueueLocation
get() {
val valStr = appPrefs.getString(PREF_ENQUEUE_LOCATION, EnqueueLocation.BACK.name)
try {
return EnqueueLocation.valueOf(valStr!!)
} catch (t: Throwable) {
// should never happen but just in case
Log.e(TAG, "getEnqueueLocation: invalid value '$valStr' Use default.", t)
return EnqueueLocation.BACK
}
}
set(location) {
appPrefs.edit().putString(PREF_ENQUEUE_LOCATION, location.name).apply()
}
class EnqueuePositionCalculator(private val enqueueLocation: EnqueueLocation) {
/**
* Determine the position (0-based) that the item(s) should be inserted to the named queue.

View File

@ -1,12 +1,14 @@
package ac.mdiq.podcini.storage.utils
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.PREF_SMART_MARK_AS_PLAYED_SECS
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Playable
import org.apache.commons.lang3.StringUtils
object EpisodeUtil {
private val TAG: String = EpisodeUtil::class.simpleName ?: "Anonymous"
val smartMarkAsPlayedSecs: Int
get() = appPrefs.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")!!.toInt()
@JvmStatic
fun indexOfItemWithId(episodes: List<Episode?>, id: Long): Int {
@ -43,7 +45,6 @@ object EpisodeUtil {
@JvmStatic
fun hasAlmostEnded(media: Playable): Boolean {
val smartMarkAsPlayedSecs = UserPreferences.smartMarkAsPlayedSecs
return media.getDuration() > 0 && media.getPosition() >= media.getDuration() - smartMarkAsPlayedSecs * 1000
}
}

View File

@ -13,10 +13,7 @@ object FileNameGenerator {
const val MAX_FILENAME_LENGTH: Int = 242 // limited by CircleCI
private const val MD5_HEX_LENGTH = 32
private val validChars = ("abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789"
+ " _-").toCharArray()
private val validChars = ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 _-").toCharArray()
/**
* This method will return a new string that doesn't contain any illegal

View File

@ -0,0 +1,153 @@
package ac.mdiq.podcini.storage.utils
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.util.Log
import android.webkit.URLUtil
import io.realm.kotlin.ext.isManaged
import org.apache.commons.io.FilenameUtils
import java.io.File
import java.io.IOException
object FilesUtils {
private val TAG: String = FilesUtils::class.simpleName ?: "Anonymous"
private const val FEED_DOWNLOADPATH = "cache/"
private const val MEDIA_DOWNLOADPATH = "media/"
lateinit var context: Context
fun findUnusedFile(dest: File): File? {
// find different name
var newDest: File? = null
for (i in 1 until Int.MAX_VALUE) {
val newName = (FilenameUtils.getBaseName(dest.name) + "-" + i + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(dest.name))
Logd(TAG, "Testing filename $newName")
newDest = File(dest.parent, newName)
if (!newDest.exists()) {
Logd(TAG, "File doesn't exist yet. Using $newName")
break
}
}
return newDest
}
val feedfilePath: String
get() = getDataFolder(FEED_DOWNLOADPATH).toString() + "/"
fun getFeedfileName(feed: Feed): String {
var filename = feed.downloadUrl
if (!feed.title.isNullOrEmpty()) filename = feed.title
if (filename == null) return ""
return "feed-" + FileNameGenerator.generateFileName(filename) + feed.id
}
fun getMediafilePath(media: EpisodeMedia): String {
val item = media.episode ?: return ""
Logd(TAG, "item managed: ${item.isManaged()}")
val title = item.feed?.title?:return ""
val mediaPath = (MEDIA_DOWNLOADPATH + FileNameGenerator.generateFileName(title))
return getDataFolder(mediaPath).toString() + "/"
}
fun getMediafilename(media: EpisodeMedia): String {
var titleBaseFilename = ""
// Try to generate the filename by the item title
if (media.episode?.title != null) {
val title = media.episode!!.title!!
titleBaseFilename = FileNameGenerator.generateFileName(title)
}
val urlBaseFilename = URLUtil.guessFileName(media.downloadUrl, null, media.mimeType)
var baseFilename: String
baseFilename = if (titleBaseFilename != "") titleBaseFilename else urlBaseFilename
val filenameMaxLength = 220
if (baseFilename.length > filenameMaxLength) baseFilename = baseFilename.substring(0, filenameMaxLength)
return (baseFilename + FilenameUtils.EXTENSION_SEPARATOR + media.id + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(urlBaseFilename))
}
fun getMediafilePath(item: Episode): String {
val title = item.feed?.title?:return ""
val mediaPath = (MEDIA_DOWNLOADPATH + FileNameGenerator.generateFileName(title))
return getDataFolder(mediaPath).toString() + "/"
}
fun getMediafilename(item: Episode): String {
var titleBaseFilename = ""
// Try to generate the filename by the item title
if (item.title != null) {
val title = item.title!!
titleBaseFilename = FileNameGenerator.generateFileName(title)
}
var baseFilename: String
baseFilename = if (titleBaseFilename != "") titleBaseFilename else "NoTitle"
val filenameMaxLength = 220
if (baseFilename.length > filenameMaxLength) baseFilename = baseFilename.substring(0, filenameMaxLength)
return (baseFilename + FilenameUtils.EXTENSION_SEPARATOR + "noid" + FilenameUtils.EXTENSION_SEPARATOR + "wav")
}
fun getTypeDir(baseDirPath: String?, type: String?): File? {
if (baseDirPath == null) return null
val baseDir = File(baseDirPath)
val typeDir = if (type == null) baseDir else File(baseDir, type)
if (!typeDir.exists()) {
if (!baseDir.canWrite()) {
Log.e(TAG, "Base dir is not writable " + baseDir.absolutePath)
return null
}
if (!typeDir.mkdirs()) {
Log.e(TAG, "Could not create type dir " + typeDir.absolutePath)
return null
}
}
return typeDir
}
/**
* Return the folder where the app stores all of its data. This method will
* return the standard data folder if none has been set by the user.
* @param type The name of the folder inside the data folder. May be null
* when accessing the root of the data folder.
* @return The data folder that has been requested or null if the folder could not be created.
*/
fun getDataFolder(type: String?): File? {
var dataFolder = getTypeDir(null, type)
if (dataFolder == null || !dataFolder.canWrite()) {
Logd(TAG, "User data folder not writable or not set. Trying default.")
dataFolder = context.getExternalFilesDir(type)
}
if (dataFolder == null || !dataFolder.canWrite()) {
Logd(TAG, "Default data folder not available or not writable. Falling back to internal memory.")
dataFolder = getTypeDir(context.filesDir.absolutePath, type)
}
return dataFolder
}
/**
* Create a .nomedia file to prevent scanning by the media scanner.
*/
fun createNoMediaFile() {
val f = File(context.getExternalFilesDir(null), ".nomedia")
if (!f.exists()) {
try {
f.createNewFile()
} catch (e: IOException) {
Log.e(TAG, "Could not create .nomedia file")
e.printStackTrace()
}
Logd(TAG, ".nomedia file created")
}
}
}

View File

@ -4,17 +4,25 @@ import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.PREF_USE_EPISODE_COVER
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
/**
* Utility class to use the appropriate image resource based on [UserPreferences].
*/
object ImageResourceUtils {
/**
* @return `true` if episodes should use their own cover, `false` otherwise
*/
val useEpisodeCoverSetting: Boolean
get() = appPrefs.getBoolean(PREF_USE_EPISODE_COVER, true)
/**
* returns the image location, does prefer the episode cover if available and enabled in settings.
*/
@JvmStatic
fun getEpisodeListImageLocation(playable: Playable): String? {
return if (UserPreferences.useEpisodeCoverSetting) playable.getImageLocation()
return if (useEpisodeCoverSetting) playable.getImageLocation()
else getFallbackImageLocation(playable)
}
@ -23,7 +31,7 @@ object ImageResourceUtils {
*/
@JvmStatic
fun getEpisodeListImageLocation(episode: Episode): String? {
return if (UserPreferences.useEpisodeCoverSetting) episode.imageLocation
return if (useEpisodeCoverSetting) episode.imageLocation
else getFallbackImageLocation(episode)
}

View File

@ -2,6 +2,7 @@ package ac.mdiq.podcini.storage.utils
import android.os.StatFs
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
/**
* Utility functions for handling storage errors
@ -13,7 +14,7 @@ object StorageUtils {
@JvmStatic
val freeSpaceAvailable: Long
get() {
val dataFolder = UserPreferences.getDataFolder(null)
val dataFolder = getDataFolder(null)
return if (dataFolder != null) getFreeSpaceAvailable(dataFolder.absolutePath) else 0
}

View File

@ -8,6 +8,12 @@ import android.view.View
import androidx.media3.common.util.UnstableApi
class DeleteActionButton(item: Episode) : EpisodeActionButton(item) {
override val visibility: Int
get() {
if (item.media != null && (item.media!!.downloaded || item.feed?.isLocalFeed == true)) return View.VISIBLE
return View.INVISIBLE
}
override fun getLabel(): Int {
return R.string.delete_label
}
@ -17,10 +23,4 @@ class DeleteActionButton(item: Episode) : EpisodeActionButton(item) {
@UnstableApi override fun onClick(context: Context) {
deleteEpisodesWarnLocal(context, listOf(item))
}
override val visibility: Int
get() {
if (item.media != null && (item.media!!.downloaded || item.feed?.isLocalFeed == true)) return View.VISIBLE
return View.INVISIBLE
}
}

View File

@ -15,14 +15,16 @@ import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
class DownloadActionButton(item: Episode) : EpisodeActionButton(item) {
override val visibility: Int
get() = if (item.feed?.isLocalFeed == true) View.INVISIBLE else View.VISIBLE
override fun getLabel(): Int {
return R.string.download_label
}
override fun getDrawable(): Int {
return R.drawable.ic_download
}
override val visibility: Int
get() = if (item.feed?.isLocalFeed == true) View.INVISIBLE else View.VISIBLE
override fun onClick(context: Context) {
val media = item.media

View File

@ -13,17 +13,17 @@ import androidx.media3.common.util.UnstableApi
abstract class EpisodeActionButton internal constructor(@JvmField var item: Episode) {
val TAG = this::class.simpleName ?: "ItemActionButton"
open val visibility: Int
get() = View.VISIBLE
var processing: Float = -1f
abstract fun getLabel(): Int
abstract fun getDrawable(): Int
abstract fun onClick(context: Context)
open val visibility: Int
get() = View.VISIBLE
var processing: Float = -1f
fun configure(button: View, icon: ImageView, context: Context) {
button.visibility = visibility
button.contentDescription = context.getString(getLabel())

View File

@ -8,16 +8,19 @@ import ac.mdiq.podcini.storage.model.Episode
import androidx.media3.common.util.UnstableApi
class MarkAsPlayedActionButton(item: Episode) : EpisodeActionButton(item) {
override val visibility: Int
get() = if (item.isPlayed()) View.INVISIBLE else View.VISIBLE
override fun getLabel(): Int {
return (if (item.media != null) R.string.mark_read_label else R.string.mark_read_no_media_label)
}
override fun getDrawable(): Int {
return R.drawable.ic_check
}
@UnstableApi override fun onClick(context: Context) {
if (!item.isPlayed()) setPlayState(Episode.PLAYED, true, item)
}
override val visibility: Int
get() = if (item.isPlayed()) View.INVISIBLE else View.VISIBLE
}

View File

@ -1,10 +1,10 @@
package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileStreaming
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.preferences.UsageStatistics
import ac.mdiq.podcini.preferences.UsageStatistics.logAction
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileStreaming
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.storage.model.Playable

View File

@ -1,18 +1,18 @@
package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.getMediafilePath
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.getMediafilename
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
import ac.mdiq.podcini.storage.database.Episodes.persistEpisode
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.utils.FilesUtils.getMediafilePath
import ac.mdiq.podcini.storage.utils.FilesUtils.getMediafilename
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.tts
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.ttsReady
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.ttsWorking
import ac.mdiq.podcini.util.AudioMediaOperation.mergeAudios
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
import ac.mdiq.podcini.storage.database.Episodes.persistEpisode
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
import android.content.Context
@ -25,7 +25,10 @@ import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.text.HtmlCompat
import androidx.media3.common.util.UnstableApi
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import net.dankito.readability4j.Readability4J
import java.io.File
import java.util.*
@ -35,7 +38,9 @@ import kotlin.math.min
class TTSActionButton(item: Episode) : EpisodeActionButton(item) {
private var readerText: String? = null
// private val ioScope = CoroutineScope(Dispatchers.IO)
override val visibility: Int
get() = if (item.link.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
override fun getLabel(): Int {
return R.string.TTS_label
@ -151,7 +156,4 @@ class TTSActionButton(item: Episode) : EpisodeActionButton(item) {
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
}
}
override val visibility: Int
get() = if (item.link.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
}

View File

@ -7,16 +7,18 @@ import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.storage.model.Episode
class VisitWebsiteActionButton(item: Episode) : EpisodeActionButton(item) {
override val visibility: Int
get() = if (item.link.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
override fun getLabel(): Int {
return R.string.visit_website_label
}
override fun getDrawable(): Int {
return R.drawable.ic_web
}
override fun onClick(context: Context) {
if (!item.link.isNullOrEmpty()) openInBrowser(context, item.link!!)
}
override val visibility: Int
get() = if (item.link.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
}

View File

@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.BugReportBinding
import ac.mdiq.podcini.preferences.ThemeSwitcher.getTheme
import ac.mdiq.podcini.preferences.UserPreferences.getDataFolder
import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.error.CrashReportWriter

View File

@ -4,13 +4,13 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SortDialogBinding
import ac.mdiq.podcini.databinding.SortDialogItemActiveBinding
import ac.mdiq.podcini.databinding.SortDialogItemBinding
import ac.mdiq.podcini.preferences.UserPreferences.feedOrderBy
import ac.mdiq.podcini.preferences.UserPreferences.feedOrderDir
import ac.mdiq.podcini.preferences.UserPreferences.setFeedOrder
import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER
import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER_DIRECTION
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.model.FeedSortOrder
import ac.mdiq.podcini.storage.model.FeedSortOrder.Companion.getSortOrder
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion.feedOrderBy
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion.feedOrderDir
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
@ -134,4 +134,13 @@ open class FeedSortDialogNew : BottomSheetDialogFragment() {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
private fun setFeedOrder(selected: String, dir: Int) {
appPrefs.edit()
.putString(PREF_DRAWER_FEED_ORDER, selected)
.apply()
appPrefs.edit()
.putInt(PREF_DRAWER_FEED_ORDER_DIRECTION, dir)
.apply()
}
}

View File

@ -9,15 +9,14 @@ import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.PREF_PLAYBACK_SPEED_ARRAY
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.preferences.UserPreferences.playbackSpeedArray
import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed
import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
import ac.mdiq.podcini.ui.utils.ItemOffsetDecoration
import ac.mdiq.podcini.ui.view.PlaybackSpeedSeekBar
import ac.mdiq.podcini.util.Logd
@ -42,6 +41,9 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONException
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
@ -177,6 +179,37 @@ import java.util.*
}
}
var playbackSpeedArray: List<Float>
get() = readPlaybackSpeedArray(appPrefs.getString(PREF_PLAYBACK_SPEED_ARRAY, null))
set(speeds) {
val format = DecimalFormatSymbols(Locale.US)
format.decimalSeparator = '.'
val speedFormat = DecimalFormat("0.00", format)
val jsonArray = JSONArray()
for (speed in speeds) {
jsonArray.put(speedFormat.format(speed.toDouble()))
}
appPrefs.edit().putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString()).apply()
}
private fun readPlaybackSpeedArray(valueFromPrefs: String?): List<Float> {
if (valueFromPrefs != null) {
try {
val jsonArray = JSONArray(valueFromPrefs)
val selectedSpeeds: MutableList<Float> = ArrayList()
for (i in 0 until jsonArray.length()) {
selectedSpeeds.add(jsonArray.getDouble(i).toFloat())
}
return selectedSpeeds
} catch (e: JSONException) {
Log.e(TAG, "Got JSON error when trying to get speeds from JSONArray")
e.printStackTrace()
}
}
// If this preference hasn't been set yet, return the default options
return mutableListOf(1.0f, 1.25f, 1.5f)
}
inner class SpeedSelectionAdapter : RecyclerView.Adapter<SpeedSelectionAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val chip = Chip(context)

View File

@ -1,8 +1,9 @@
package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.preferences.UserPreferences.allEpisodesSortOrder
import ac.mdiq.podcini.preferences.UserPreferences.prefFilterAllEpisodes
import ac.mdiq.podcini.preferences.UserPreferences.PREF_FILTER_ALL_EPISODES
import ac.mdiq.podcini.preferences.UserPreferences.PREF_SORT_ALL_EPISODES
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount
import ac.mdiq.podcini.storage.model.Episode
@ -12,7 +13,6 @@ import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.dialog.EpisodeFilterDialog
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
@ -35,7 +35,7 @@ import kotlin.math.min
*/
@UnstableApi class AllEpisodesFragment : BaseEpisodesFragment() {
var allEpisodes: List<Episode> = listOf()
private var allEpisodes: List<Episode> = listOf()
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val root = super.onCreateView(inflater, container, savedInstanceState)
@ -181,8 +181,19 @@ import kotlin.math.min
}
}
}
companion object {
val TAG = AllEpisodesFragment::class.simpleName ?: "Anonymous"
const val PREF_NAME: String = "PrefAllEpisodesFragment"
var allEpisodesSortOrder: EpisodeSortOrder?
get() = EpisodeSortOrder.fromCodeString(appPrefs.getString(PREF_SORT_ALL_EPISODES, "" + EpisodeSortOrder.DATE_NEW_OLD.code))
set(s) {
appPrefs.edit().putString(PREF_SORT_ALL_EPISODES, "" + s!!.code).apply()
}
var prefFilterAllEpisodes: String
get() = appPrefs.getString(PREF_FILTER_ALL_EPISODES, "")?:""
set(filter) {
appPrefs.edit().putString(PREF_FILTER_ALL_EPISODES, filter).apply()
}
}
}

View File

@ -6,14 +6,15 @@ import ac.mdiq.podcini.databinding.SimpleListFragmentBinding
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.net.feed.FeedUpdateManager
import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.PREF_DOWNLOADS_SORTED_ORDER
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler
import ac.mdiq.podcini.ui.actions.actionbutton.DeleteActionButton
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
@ -24,7 +25,6 @@ import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
import ac.mdiq.podcini.ui.utils.LiftOnScrollListener
import ac.mdiq.podcini.ui.view.EpisodesRecyclerView
@ -327,7 +327,7 @@ import java.util.*
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {
val sortOrder: EpisodeSortOrder? = UserPreferences.downloadsSortedOrder
val sortOrder: EpisodeSortOrder? = downloadsSortedOrder
val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.DOWNLOADED), sortOrder)
if (runningDownloads.isEmpty()) episodes = downloadedItems.toMutableList()
else {
@ -412,7 +412,7 @@ import java.util.*
class DownloadsSortDialog : EpisodeSortDialog() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sortOrder = UserPreferences.downloadsSortedOrder
sortOrder = downloadsSortedOrder
}
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
@ -426,7 +426,7 @@ import java.util.*
override fun onSelectionChanged() {
super.onSelectionChanged()
UserPreferences.downloadsSortedOrder = sortOrder
downloadsSortedOrder = sortOrder
EventFlow.postEvent(FlowEvent.DownloadLogEvent())
}
}
@ -436,5 +436,15 @@ import java.util.*
const val ARG_SHOW_LOGS: String = "show_logs"
private const val KEY_UP_ARROW = "up_arrow"
// the sort order for the downloads.
var downloadsSortedOrder: EpisodeSortOrder?
get() {
val sortOrderStr = appPrefs.getString(PREF_DOWNLOADS_SORTED_ORDER, "" + EpisodeSortOrder.DATE_NEW_OLD.code)
return EpisodeSortOrder.fromCodeString(sortOrderStr)
}
set(sortOrder) {
appPrefs.edit().putString(PREF_DOWNLOADS_SORTED_ORDER, "" + sortOrder!!.code).apply()
}
}
}

View File

@ -10,16 +10,19 @@ import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.database.Queues.clearQueue
import ac.mdiq.podcini.storage.database.Queues.isQueueKeepSorted
import ac.mdiq.podcini.storage.database.Queues.isQueueLocked
import ac.mdiq.podcini.storage.database.Queues.moveInQueue
import ac.mdiq.podcini.storage.database.Queues.moveInQueueSync
import ac.mdiq.podcini.storage.database.Queues.queueKeepSortedOrder
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils
@ -30,7 +33,6 @@ import ac.mdiq.podcini.ui.adapter.SelectableAdapter
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
import ac.mdiq.podcini.ui.utils.LiftOnScrollListener
import ac.mdiq.podcini.ui.view.EpisodesRecyclerView
@ -393,8 +395,8 @@ import java.util.*
}
private fun refreshToolbarState() {
val keepSorted: Boolean = UserPreferences.isQueueKeepSorted
toolbar.menu?.findItem(R.id.queue_lock)?.setChecked(UserPreferences.isQueueLocked)
val keepSorted: Boolean = isQueueKeepSorted
toolbar.menu?.findItem(R.id.queue_lock)?.setChecked(isQueueLocked)
toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(!keepSorted)
}
@ -422,7 +424,7 @@ import java.util.*
}
@UnstableApi private fun toggleQueueLock() {
val isLocked: Boolean = UserPreferences.isQueueLocked
val isLocked: Boolean = isQueueLocked
if (isLocked) setQueueLocked(false)
else {
val shouldShowLockWarning: Boolean = prefs!!.getBoolean(PREF_SHOW_LOCK_WARNING, true)
@ -448,7 +450,7 @@ import java.util.*
}
@UnstableApi private fun setQueueLocked(locked: Boolean) {
UserPreferences.isQueueLocked = locked
isQueueLocked = locked
refreshToolbarState()
adapter?.updateDragDropEnabled()
@ -568,12 +570,12 @@ import java.util.*
class QueueSortDialog : EpisodeSortDialog() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
if (UserPreferences.isQueueKeepSorted) sortOrder = UserPreferences.queueKeepSortedOrder
if (isQueueKeepSorted) sortOrder = queueKeepSortedOrder
val view: View = super.onCreateView(inflater, container, savedInstanceState)!!
binding.keepSortedCheckbox.visibility = View.VISIBLE
binding.keepSortedCheckbox.setChecked(UserPreferences.isQueueKeepSorted)
binding.keepSortedCheckbox.setChecked(isQueueKeepSorted)
// Disable until something gets selected
binding.keepSortedCheckbox.setEnabled(UserPreferences.isQueueKeepSorted)
binding.keepSortedCheckbox.setEnabled(isQueueKeepSorted)
return view
}
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
@ -584,8 +586,8 @@ import java.util.*
super.onSelectionChanged()
binding.keepSortedCheckbox.setEnabled(sortOrder != EpisodeSortOrder.RANDOM)
if (sortOrder == EpisodeSortOrder.RANDOM) binding.keepSortedCheckbox.setChecked(false)
UserPreferences.isQueueKeepSorted = binding.keepSortedCheckbox.isChecked
UserPreferences.queueKeepSortedOrder = sortOrder
isQueueKeepSorted = binding.keepSortedCheckbox.isChecked
queueKeepSortedOrder = sortOrder
reorderQueue(sortOrder, true)
}
/**
@ -659,10 +661,10 @@ import java.util.*
private var dragDropEnabled: Boolean
init {
dragDropEnabled = !(UserPreferences.isQueueKeepSorted || UserPreferences.isQueueLocked)
dragDropEnabled = !(isQueueKeepSorted || isQueueLocked)
}
fun updateDragDropEnabled() {
dragDropEnabled = !(UserPreferences.isQueueKeepSorted || UserPreferences.isQueueLocked)
dragDropEnabled = !(isQueueKeepSorted || isQueueLocked)
notifyDataSetChanged()
}
@UnstableApi
@ -701,7 +703,7 @@ import java.util.*
if (!inActionMode()) {
// menu.findItem(R.id.multi_select).setVisible(true)
val keepSorted: Boolean = UserPreferences.isQueueKeepSorted
val keepSorted: Boolean = isQueueKeepSorted
if (getItem(0)?.id === longPressedItem?.id || keepSorted) menu.findItem(R.id.move_to_top_item).setVisible(false)
if (getItem(itemCount - 1)?.id === longPressedItem?.id || keepSorted) menu.findItem(R.id.move_to_bottom_item).setVisible(false)
} else {

View File

@ -19,12 +19,10 @@ import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
import ac.mdiq.podcini.ui.utils.LiftOnScrollListener
import ac.mdiq.podcini.ui.view.EpisodesRecyclerView
import ac.mdiq.podcini.ui.view.SquareImageView
import ac.mdiq.podcini.ui.view.viewholder.EpisodeViewHolder
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
@ -74,7 +72,7 @@ import java.lang.ref.WeakReference
private lateinit var emptyViewHandler: EmptyViewHandler
private lateinit var recyclerView: EpisodesRecyclerView
private lateinit var searchView: SearchView
private lateinit var speedDialBinding: MultiSelectSpeedDialBinding
private lateinit var sdBinding: MultiSelectSpeedDialBinding
private lateinit var chip: Chip
private lateinit var automaticSearchDebouncer: Handler
@ -95,7 +93,7 @@ import java.lang.ref.WeakReference
Logd(TAG, "fragment onCreateView")
setupToolbar(binding.toolbar)
speedDialBinding = MultiSelectSpeedDialBinding.bind(binding.root)
sdBinding = MultiSelectSpeedDialBinding.bind(binding.root)
progressBar = binding.progressBar
recyclerView = binding.recyclerView
recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool)
@ -104,7 +102,6 @@ import java.lang.ref.WeakReference
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
super.onCreateContextMenu(menu, v, menuInfo)
if (!inActionMode()) menu.findItem(R.id.multi_select).setVisible(true)
MenuItemUtils.setOnClickListeners(menu) { item: MenuItem -> this@SearchFragment.onContextItemSelected(item) }
}
}
@ -151,21 +148,20 @@ import java.lang.ref.WeakReference
}
}
})
speedDialBinding.fabSD.overlayLayout = speedDialBinding.fabSDOverlay
speedDialBinding.fabSD.inflate(R.menu.episodes_apply_action_speeddial)
speedDialBinding.fabSD.setOnChangeListener(object : SpeedDialView.OnChangeListener {
sdBinding.fabSD.overlayLayout = sdBinding.fabSDOverlay
sdBinding.fabSD.inflate(R.menu.episodes_apply_action_speeddial)
sdBinding.fabSD.setOnChangeListener(object : SpeedDialView.OnChangeListener {
override fun onMainActionSelected(): Boolean {
return false
}
override fun onToggleChanged(open: Boolean) {
if (open && adapter.selectedCount == 0) {
(activity as MainActivity).showSnackbarAbovePlayer(R.string.no_items_selected, Snackbar.LENGTH_SHORT)
speedDialBinding.fabSD.close()
sdBinding.fabSD.close()
}
}
})
speedDialBinding.fabSD.setOnActionSelectedListener { actionItem: SpeedDialActionItem ->
sdBinding.fabSD.setOnActionSelectedListener { actionItem: SpeedDialActionItem ->
EpisodeMultiSelectHandler(activity as MainActivity, actionItem.id)
.handleAction(adapter.selectedItems.filterIsInstance<Episode>())
adapter.endSelectMode()
@ -210,7 +206,6 @@ import java.lang.ref.WeakReference
searchWithProgressBar()
return true
}
@UnstableApi override fun onQueryTextChange(s: String): Boolean {
automaticSearchDebouncer.removeCallbacksAndMessages(null)
if (s.isEmpty() || s.endsWith(" ") || (lastQueryChange != 0L && System.currentTimeMillis() > lastQueryChange + SEARCH_DEBOUNCE_INTERVAL)) {
@ -227,7 +222,6 @@ import java.lang.ref.WeakReference
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
parentFragmentManager.popBackStack()
return true
@ -337,7 +331,6 @@ import java.lang.ref.WeakReference
if (requireArguments().getLong(ARG_FEED, 0) == 0L) {
if (results.second != null) adapterFeeds.updateData(results.second!!)
} else adapterFeeds.updateData(emptyList())
if (searchView.query.toString().isEmpty()) emptyViewHandler.setMessage(R.string.type_to_search)
else emptyViewHandler.setMessage(getString(R.string.no_results_for_query) + searchView.query)
}
@ -351,9 +344,10 @@ import java.lang.ref.WeakReference
val query = searchView.query.toString()
if (query.isEmpty()) return Pair<List<Episode>, List<Feed>>(emptyList(), emptyList())
val feed = requireArguments().getLong(ARG_FEED)
val items: List<Episode> = searchEpisodes(feed, query)
val feedID = requireArguments().getLong(ARG_FEED)
val items: List<Episode> = searchEpisodes(feedID, query)
val feeds: List<Feed> = searchFeeds(query)
Logd(TAG, "performSearch items: ${items.size} feeds: ${feeds.size}")
return Pair<List<Episode>, List<Feed>>(items, feeds)
}
@ -362,23 +356,23 @@ import java.lang.ref.WeakReference
val sb = StringBuilder()
for (i in queryWords.indices) {
sb.append("(")
.append("feedTitle TEXT ${queryWords[i]}")
.append("eigenTitle TEXT '${queryWords[i]}'")
.append(" OR ")
.append("customTitle TEXT ${queryWords[i]}")
.append("customTitle TEXT '${queryWords[i]}'")
.append(" OR ")
.append("author TEXT ${queryWords[i]}")
.append("author TEXT '${queryWords[i]}'")
.append(" OR ")
.append("description TEXT ${queryWords[i]}")
.append("description TEXT '${queryWords[i]}'")
.append(") ")
if (i != queryWords.size - 1) sb.append("AND ")
}
return sb.toString()
}
private fun searchFeeds(query: String): List<Feed> {
Logd(TAG, "searchFeeds called")
val queryString = prepareFeedQueryString(query)
Logd(TAG, "searchFeeds queryString: $queryString")
return realm.query(Feed::class).query(queryString).find()
}
@ -387,9 +381,9 @@ import java.lang.ref.WeakReference
val sb = StringBuilder()
for (i in queryWords.indices) {
sb.append("(")
.append("description TEXT ${queryWords[i]}")
.append("description TEXT '${queryWords[i]}'")
.append(" OR ")
.append("title TEXT ${queryWords[i]}" )
.append("title TEXT '${queryWords[i]}'" )
.append(") ")
if (i != queryWords.size - 1) sb.append("AND ")
}
@ -405,9 +399,10 @@ import java.lang.ref.WeakReference
*/
private fun searchEpisodes(feedID: Long, query: String): List<Episode> {
Logd(TAG, "searchEpisodes called")
val queryString = prepareEpisodeQueryString(query)
val idString = if (feedID != 0L) "(id = $feedID)" else ""
return realm.query(Episode::class).query("$idString AND $queryString").find()
var queryString = prepareEpisodeQueryString(query)
if (feedID != 0L) queryString = "(feedId == $feedID) AND $queryString"
Logd(TAG, "searchEpisodes queryString: $queryString")
return realm.query(Episode::class).query(queryString).find()
}
private fun showInputMethod(view: View) {
@ -431,14 +426,14 @@ import java.lang.ref.WeakReference
override fun onStartSelectMode() {
searchViewFocusOff()
// speedDialBinding.fabSD.removeActionItemById(R.id.remove_from_inbox_batch)
speedDialBinding.fabSD.removeActionItemById(R.id.remove_from_queue_batch)
speedDialBinding.fabSD.removeActionItemById(R.id.delete_batch)
speedDialBinding.fabSD.visibility = View.VISIBLE
sdBinding.fabSD.removeActionItemById(R.id.remove_from_queue_batch)
sdBinding.fabSD.removeActionItemById(R.id.delete_batch)
sdBinding.fabSD.visibility = View.VISIBLE
}
override fun onEndSelectMode() {
speedDialBinding.fabSD.close()
speedDialBinding.fabSD.visibility = View.GONE
sdBinding.fabSD.close()
sdBinding.fabSD.visibility = View.GONE
searchViewFocusOn()
}
@ -452,14 +447,13 @@ import java.lang.ref.WeakReference
searchView.requestFocus()
}
open class HorizontalFeedListAdapter(mainActivity: MainActivity) :
RecyclerView.Adapter<HorizontalFeedListAdapter.Holder>(), View.OnCreateContextMenuListener {
open class HorizontalFeedListAdapter(mainActivity: MainActivity)
: RecyclerView.Adapter<HorizontalFeedListAdapter.Holder>(), View.OnCreateContextMenuListener {
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
private val data: MutableList<Feed> = ArrayList()
private var dummyViews = 0
var longPressedItem: Feed? = null
@StringRes
private var endButtonText = 0
private var endButtonAction: Runnable? = null
@ -467,18 +461,15 @@ import java.lang.ref.WeakReference
fun setDummyViews(dummyViews: Int) {
this.dummyViews = dummyViews
}
fun updateData(newData: List<Feed>?) {
data.clear()
data.addAll(newData!!)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val convertView = View.inflate(mainActivityRef.get(), R.layout.horizontal_feed_item, null)
return Holder(convertView)
}
@UnstableApi override fun onBindViewHolder(holder: Holder, position: Int) {
if (position == itemCount - 1 && endButtonAction != null) {
holder.cardView.visibility = View.GONE
@ -497,49 +488,41 @@ import java.lang.ref.WeakReference
holder.imageView.setImageResource(R.color.medium_gray)
return
}
holder.itemView.alpha = 1.0f
val podcast: Feed = data[position]
holder.imageView.setContentDescription(podcast.title)
holder.imageView.setOnClickListener {
mainActivityRef.get()?.loadChildFragment(FeedEpisodesFragment.newInstance(podcast.id))
}
holder.imageView.setOnCreateContextMenuListener(this)
holder.imageView.setOnLongClickListener {
val currentItemPosition = holder.bindingAdapterPosition
longPressedItem = data[currentItemPosition]
false
}
holder.imageView.load(podcast.imageUrl) {
placeholder(R.color.light_gray)
error(R.mipmap.ic_launcher)
}
}
override fun getItemId(position: Int): Long {
if (position >= data.size) return RecyclerView.NO_ID // Dummy views
return data[position].id
}
override fun getItemCount(): Int {
return dummyViews + data.size + (if ((endButtonAction == null)) 0 else 1)
}
override fun onCreateContextMenu(contextMenu: ContextMenu, view: View, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
val inflater: MenuInflater = mainActivityRef.get()!!.menuInflater
if (longPressedItem == null) return
inflater.inflate(R.menu.nav_feed_context, contextMenu)
contextMenu.setHeaderTitle(longPressedItem!!.title)
}
fun setEndButton(@StringRes text: Int, action: Runnable?) {
endButtonAction = action
endButtonText = text
notifyDataSetChanged()
}
class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val binding = HorizontalFeedItemBinding.bind(itemView)
var imageView: SquareImageView = binding.discoveryCover
@ -584,9 +567,9 @@ import java.lang.ref.WeakReference
/**
* Create a new SearchFragment that searches one specific feed.
*/
fun newInstance(feed: Long, feedTitle: String?): SearchFragment {
fun newInstance(feedId: Long, feedTitle: String?): SearchFragment {
val fragment = newInstance()
fragment.requireArguments().putLong(ARG_FEED, feed)
fragment.requireArguments().putLong(ARG_FEED, feedId)
fragment.requireArguments().putString(ARG_FEED_NAME, feedTitle)
return fragment
}

View File

@ -4,9 +4,11 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.*
import ac.mdiq.podcini.net.feed.FeedUpdateManager
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.feedOrderBy
import ac.mdiq.podcini.preferences.UserPreferences.feedOrderDir
import ac.mdiq.podcini.preferences.UserPreferences.useGridLayout
import ac.mdiq.podcini.preferences.UserPreferences.FEED_ORDER_UNPLAYED
import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER
import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER_DIRECTION
import ac.mdiq.podcini.preferences.UserPreferences.PREF_FEED_GRID_LAYOUT
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
import ac.mdiq.podcini.storage.database.Feeds.getTags
import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences
@ -88,6 +90,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
private val tags: MutableList<String> = mutableListOf()
private var useGrid: Boolean? = null
val useGridLayout: Boolean
get() = appPrefs.getBoolean(PREF_FEED_GRID_LAYOUT, false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -878,6 +882,18 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
private var prevFeedUpdatingEvent: FlowEvent.FeedUpdatingEvent? = null
val feedOrderBy: Int
get() {
val value = appPrefs.getString(PREF_DRAWER_FEED_ORDER, "" + FEED_ORDER_UNPLAYED)
return value!!.toInt()
}
val feedOrderDir: Int
get() {
val value = appPrefs.getInt(PREF_DRAWER_FEED_ORDER_DIRECTION, 0)
return value
}
fun newInstance(folderTitle: String?): SubscriptionsFragment {
val fragment = SubscriptionsFragment()
val args = Bundle()

View File

@ -1,12 +1,13 @@
package ac.mdiq.podcini.ui.utils
import ac.mdiq.podcini.R
import ac.mdiq.podcini.preferences.UserPreferences.PREF_GPODNET_NOTIFICATIONS
import ac.mdiq.podcini.preferences.UserPreferences.PREF_SHOW_DOWNLOAD_REPORT
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import android.content.Context
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationChannelGroupCompat
import androidx.core.app.NotificationManagerCompat
import ac.mdiq.podcini.preferences.UserPreferences.gpodnetNotificationsEnabledRaw
import ac.mdiq.podcini.preferences.UserPreferences.showDownloadReportRaw
object NotificationUtils {
const val CHANNEL_ID_USER_ACTION: String = "user_action"
@ -19,6 +20,18 @@ object NotificationUtils {
const val GROUP_ID_ERRORS: String = "group_errors"
const val GROUP_ID_NEWS: String = "group_news"
/**
* Used for migration of the preference to system notification channels.
*/
val showDownloadReportRaw: Boolean
get() = appPrefs.getBoolean(PREF_SHOW_DOWNLOAD_REPORT, true)
/**
* Used for migration of the preference to system notification channels.
*/
val gpodnetNotificationsEnabledRaw: Boolean
get() = appPrefs.getBoolean(PREF_GPODNET_NOTIFICATIONS, true)
fun createChannels(context: Context) {
val mNotificationManager = NotificationManagerCompat.from(context)

View File

@ -1,7 +1,7 @@
package ac.mdiq.podcini.util.error
import ac.mdiq.podcini.BuildConfig
import ac.mdiq.podcini.preferences.UserPreferences.getDataFolder
import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
import android.os.Build
import android.util.Log
import org.apache.commons.io.IOUtils

View File

@ -578,7 +578,7 @@
<string name="database_import_label">استيراد قاعدة البيانات</string>
<string name="database_import_warning">استيراد قاعدة بيانات سيمسح كل اشتراكاتك الحالية وسجل الاستماع. الأفضل أن تصدر قاعدة البيانات الحالية لأرشيف. هل تريد تبديل البيانات؟</string>
<string name="please_wait">يرجى الانتظار…</string>
<string name="export_error_label">حدث خطأ أثناء التصدير</string>
<string name="import_export_error_label">حدث خطأ أثناء التصدير</string>
<string name="export_success_title">تم التصدير بنجاح</string>
<string name="opml_import_ask_read_permission">الوصول الى مساحة التخزين الخارجية مطلوب لقراءة ملف الـ OPML</string>
<string name="successful_import_label">استيراد ناجح</string>

View File

@ -459,7 +459,7 @@
<string name="database_import_label">Enporzhiañ ar stlennvon</string>
<string name="database_import_warning">Enporzhiañ ur stlennvon a amsavo ho holl goumanantoù ha roll istor lenn. Erbediñ a reomp ezporzhiañ ho stlennvon en a-raok. Fellout a ra deoc\'h enporzhiañ?</string>
<string name="please_wait">Gortozit un tamm…</string>
<string name="export_error_label">Fazi ezporzhiañ</string>
<string name="import_export_error_label">Fazi ezporzhiañ</string>
<string name="export_success_title">Ezporzhiet gant berzh</string>
<string name="opml_import_ask_read_permission">Ret eo aotren haeziñ ar c\'hadaviñ diavaez evit lenn ar restr OPML</string>
<string name="successful_import_label">Enporzhiet gant berzh</string>

View File

@ -521,7 +521,7 @@
<string name="database_import_label">Importa base de dades</string>
<string name="database_import_warning">Importar una base de dades reemplaçarà totes les teues subscripcions i històric de reproducció. Deuries exportar la teua base de dades com a còpia de seguretat. Vols reemplaçar\?</string>
<string name="please_wait">Per favor, espera…</string>
<string name="export_error_label">Error d\'exportació</string>
<string name="import_export_error_label">Error d\'exportació</string>
<string name="export_success_title">Exportació correcta</string>
<string name="opml_import_ask_read_permission">Per llegir arxius OPML és necessari accés a la memòria externa</string>
<string name="successful_import_label">Importació correcta</string>

View File

@ -573,7 +573,7 @@
<string name="database_import_label">Import databáze</string>
<string name="database_import_warning">Importem databáze nahradíte všechny své odběry a historii poslechu. Doporučujeme nejdříve exportovat současnou databázi pro případnou obnovu. Opravdu chcete databázi nahradit\?</string>
<string name="please_wait">Čekejte prosím…</string>
<string name="export_error_label">Chyba exportu</string>
<string name="import_export_error_label">Chyba exportu</string>
<string name="export_success_title">Export úspěšný</string>
<string name="opml_import_ask_read_permission">Pro přečtení OPML souboru je vyžadován přístup k externímu úložišti</string>
<string name="successful_import_label">Import úspěšný</string>

View File

@ -545,7 +545,7 @@
<string name="database_import_label">Importér database</string>
<string name="database_import_warning">Import af database vil overskrive alle dine nuværende abonnementer og din afspilningshistorik. Du bør eksportere din nuværende database som en sikkerhedskopi først. Vil du overskrive\?</string>
<string name="please_wait">Vent…</string>
<string name="export_error_label">Eksportfejl</string>
<string name="import_export_error_label">Eksportfejl</string>
<string name="export_success_title">Eksport lykkedes</string>
<string name="opml_import_ask_read_permission">Adgang til eksternt lager er påkrævet for at læse OPML-filen</string>
<string name="successful_import_label">Importeret</string>

View File

@ -549,7 +549,7 @@
<string name="database_import_label">Datenbank importieren</string>
<string name="database_import_warning">Das Importieren einer Datenbank ersetzt alle Abonnements und abgespielte Episoden. Ein Export als Backup wird empfohlen. Möchtest du die Datenbank wirklich ersetzen\?</string>
<string name="please_wait">Bitte warten…</string>
<string name="export_error_label">Exportfehler</string>
<string name="import_export_error_label">Exportfehler</string>
<string name="export_success_title">Export erfolgreich</string>
<string name="opml_import_ask_read_permission">Zugriff auf externen Speicher wird benötigt, um die OPML-Datei zu lesen</string>
<string name="successful_import_label">Import erfolgreich</string>

View File

@ -562,7 +562,7 @@
<string name="database_import_label">Importar base de datos</string>
<string name="database_import_warning">La importación de una base de datos reemplazará todas las suscripciones actuales y el historial de reproducción. Debes exportar la base de datos actual como una copia de seguridad. ¿Quieres reemplazarla\?</string>
<string name="please_wait">Por favor espera…</string>
<string name="export_error_label">Error en la exportación</string>
<string name="import_export_error_label">Error en la exportación</string>
<string name="export_success_title">Exportación exitosa</string>
<string name="opml_import_ask_read_permission">Necesita acceso al almacenamiento externo para leer archivos OPML</string>
<string name="successful_import_label">Importación exitosa</string>

View File

@ -403,7 +403,7 @@
<string name="database_import_label">Andmebaasi import</string>
<string name="database_import_warning">Andmebaasi importimine asendab kõik sinu praegused tellimused ja kuulamiste ajaloo. Peaksid oma praeguse andmebaasi varundamise eesmärgil enne eksportima. Kas soovid andmed asendada\?</string>
<string name="please_wait">Palun oota…</string>
<string name="export_error_label">Viga eksportimisel</string>
<string name="import_export_error_label">Viga eksportimisel</string>
<string name="export_success_title">Eksportimine edukas</string>
<string name="opml_import_ask_read_permission">OPML faili lugemiseks on vajalik ligipääs välisele salvestusruumile</string>
<string name="successful_import_label">Importimine oli edukas</string>

View File

@ -488,7 +488,7 @@
<string name="database_import_label">Ekarri datu basea</string>
<string name="database_import_warning">Datu-base bat inportatzeak egungo harpidetza guztiak eta erreprodukzio-historia ordezkatuko ditu. Egungo datu-basea esportatu behar duzu segurtasun-kopia gisa. Ordezkatu nahi duzu?</string>
<string name="please_wait">Itxaron mesedez…</string>
<string name="export_error_label">Errorea esportatzean</string>
<string name="import_export_error_label">Errorea esportatzean</string>
<string name="export_success_title">Esportatzea ongi burutu da</string>
<string name="opml_import_ask_read_permission">Kanpo biltegiratzerako sarbidea behar duzu OPML fitxategia irakurtzeko</string>
<string name="successful_import_label">Inportate arrakastatsua</string>

View File

@ -524,7 +524,7 @@
<string name="database_import_label">وارد کردن پایگاه داده</string>
<string name="database_import_warning">وارد کردن یک پایگاه داده باعث جایگزین تمام اشتراکهای فعلی و سابقه پخش شما خواهد شد. شما باید پایگاه داده فعلی خود را به عنوان پشتیبان خارج کنید. آیا شما میخواهید جایگزین کنید؟</string>
<string name="please_wait">صبر کنید لطفا …</string>
<string name="export_error_label">خطا در خارج کردن</string>
<string name="import_export_error_label">خطا در خارج کردن</string>
<string name="export_success_title">خارج کردن موفقیت آمیز</string>
<string name="opml_import_ask_read_permission">برای خواندن پروندهٔ OPML نباز به دسترسی حافظهٔ خارجی است</string>
<string name="successful_import_label">وارد کردن موفقیت آمیز</string>

View File

@ -506,7 +506,7 @@
<string name="database_import_label">Tietokannan tuonti</string>
<string name="database_import_warning">Tietokannan tuonti korvaa kaikki nykyiset tilauksesi ja toistohistoriasi. Vie nykyinen tietokanta halutessasi varmuuskopioksi ennen jatkamista. Haluatko korvata\?</string>
<string name="please_wait">Ole hyvä ja odota…</string>
<string name="export_error_label">Vientivirhe</string>
<string name="import_export_error_label">Vientivirhe</string>
<string name="export_success_title">Vienti onnistunut</string>
<string name="opml_import_ask_read_permission">Pääsy ulkoiseen solvellukseen tarvitaan OPML tiedoston lukemiseen</string>
<string name="successful_import_label">Tuonti onnistui</string>

View File

@ -562,7 +562,7 @@
<string name="database_import_label">Importer la base de données</string>
<string name="database_import_warning">Importer une base de données remplacera tout vos abonnements et votre historique de lecture. Il est conseillé d\'exporter votre base de données actuelle pour avoir une sauvegarde. Confirmez-vous l\'import \?</string>
<string name="please_wait">Merci de patienter…</string>
<string name="export_error_label">Erreur d\'exportation</string>
<string name="import_export_error_label">Erreur d\'exportation</string>
<string name="export_success_title">Export réussi</string>
<string name="opml_import_ask_read_permission">L\'accès au stockage externe est requis pour lire le fichier OPML</string>
<string name="successful_import_label">Import réussi</string>

View File

@ -545,7 +545,7 @@
<string name="database_import_label">Importar base de datos</string>
<string name="database_import_warning">Ao importar a base de datos substituirás todas as subscricións actuais e historial de reprodución. Deberías exportar a base de datos actual como copia de apoio. Desexas substituíla?</string>
<string name="please_wait">Agarda…</string>
<string name="export_error_label">Fallo ao exportar</string>
<string name="import_export_error_label">Fallo ao exportar</string>
<string name="export_success_title">Exportado con éxito</string>
<string name="opml_import_ask_read_permission">Precísase acceso ao almacenamento externo para ler o ficheiro OPML</string>
<string name="successful_import_label">Importación correcta</string>

View File

@ -420,7 +420,7 @@
<string name="database_import_label">Adatbázis importálása</string>
<string name="database_import_warning">Az adatbázis importálása lecseréli a jelenlegi feliratkozásait és lejátszási előzményeit. Célszerű biztonsági mentésként exportálni a jelenlegi adatbázist. Biztos, hogy lecseréli\?</string>
<string name="please_wait">Várjon…</string>
<string name="export_error_label">Exportálási hiba</string>
<string name="import_export_error_label">Exportálási hiba</string>
<string name="export_success_title">Exportálás sikeres</string>
<string name="opml_import_ask_read_permission">A külső tároló elérése szükséges az OPML fájl olvasásához</string>
<string name="successful_import_label">Importálás sikeres</string>

View File

@ -429,7 +429,7 @@
<string name="database_import_label">Import Database</string>
<string name="database_import_warning">Mengimpor database akan menggantikan seluruh langganan dan riwayat pemutaran Anda. Anda sebaiknya mengekspor database yang sekarang Anda miliki sebagai cadangan. Tetap lanjutkan?</string>
<string name="please_wait">Mohon tunggu…</string>
<string name="export_error_label">Kesalahan ekspor</string>
<string name="import_export_error_label">Kesalahan ekspor</string>
<string name="export_success_title">Ekspor berhasil</string>
<string name="opml_import_ask_read_permission">Akses ke penyimpanan eksternal diperlukan untuk membaca berkas OPML</string>
<string name="successful_import_label">Impor sukses!</string>

View File

@ -563,7 +563,7 @@
<string name="database_import_label">Importa database</string>
<string name="database_import_warning">L\'importazione di un database sostituirà tutte le iscrizioni attuali e la cronologia di riproduzione. Dovresti eseguire un backup del tuo database attuale. Vuoi proseguire\?</string>
<string name="please_wait">Attendi…</string>
<string name="export_error_label">Errore di esportazione</string>
<string name="import_export_error_label">Errore di esportazione</string>
<string name="export_success_title">Esportazione eseguita</string>
<string name="opml_import_ask_read_permission">Per leggere il file OPML è necessario l\'accesso alla memoria esterna</string>
<string name="successful_import_label">Importazione eseguita</string>

View File

@ -573,7 +573,7 @@
<string name="database_import_label">ייבוא מסד נתונים</string>
<string name="database_import_warning">ייבוא מסד נתונים יחליף את כל המינויים הנוכחיים שלך לרבות היסטוריית הנגינה. עליך תחילה לייצא את מסד הנתונים הנוכחי שלך כגיבוי. להמשיך בהחלפה?</string>
<string name="please_wait">נא להמתין…</string>
<string name="export_error_label">שגיאת ייצוא</string>
<string name="import_export_error_label">שגיאת ייצוא</string>
<string name="export_success_title">הייצוא הצליח</string>
<string name="opml_import_ask_read_permission">נדרשת גישה לאחסון חיצוני כדי לקרוא את קובץ ה־OPML</string>
<string name="successful_import_label">הייבוא הצליח</string>

View File

@ -383,7 +383,7 @@
<string name="database_import_label">データベースのインポート</string>
<string name="database_import_warning">データベースをインポートすると、現在の購読と再生履歴がすべて置き換えられます。現在のデータベースをバックアップとしてエクスポートする必要があります。それでも置き換えしますか?</string>
<string name="please_wait">しばらくお待ちください…</string>
<string name="export_error_label">エクスポートエラー</string>
<string name="import_export_error_label">エクスポートエラー</string>
<string name="export_success_title">エクスポートしました</string>
<string name="opml_import_ask_read_permission">OPML ファイルを読み込むために、外部ストレージへのアクセスが必要です</string>
<string name="successful_import_label">インポートしました</string>

View File

@ -503,7 +503,7 @@
<string name="database_import_label">데이터베이스 가져오기</string>
<string name="database_import_warning">데이터베이스 가져오기를 하면 모든 구독과 재생 히스토리가 가져온 내용으로 바뀝니다. 백업을 하려면 현재 데이터베이스를 내보내기 해야 합니다. 정말로 데이터베이스를 바꾸시겠습니까\?</string>
<string name="please_wait">기다리십시오…</string>
<string name="export_error_label">내보내기 오류</string>
<string name="import_export_error_label">내보내기 오류</string>
<string name="export_success_title">내보내기 성공</string>
<string name="opml_import_ask_read_permission">OPML 파일을 읽으려면 외부 저장소 접근이 필요합니다</string>
<string name="successful_import_label">가져오기 성공</string>

View File

@ -395,7 +395,7 @@
<string name="database_import_label">Duomenų bazės importas</string>
<string name="database_import_warning">Importuojant duomenų bazę bus pakeistos visos dabartinės jūsų prenumeratos ir atkūrimo istorija. Rekomenduojame eksportuoti dabartinę duomenų bazę kaip atsarginę kopiją. Ar norite pakeisti\?</string>
<string name="please_wait">Prašome luktelėti…</string>
<string name="export_error_label">Eksporto klaida</string>
<string name="import_export_error_label">Eksporto klaida</string>
<string name="export_success_title">Sėkmingai eksportuota</string>
<string name="opml_import_ask_read_permission">Norint nuskaityti OPML failą reikalinga prieiga prie išorinės laikmenos</string>
<string name="successful_import_label">Sėkmingai importuota</string>

View File

@ -512,7 +512,7 @@
<string name="database_import_label">Database-importering</string>
<string name="database_import_warning">Å importere en database vil erstatte all dine abonnementer og avspillingshistorie. Det er anbefalt å eksportere din eksisterende database som backup. Vil du importere nå\?</string>
<string name="please_wait">Vennligst vent…</string>
<string name="export_error_label">Feil ved eksportering</string>
<string name="import_export_error_label">Feil ved eksportering</string>
<string name="export_success_title">Eksportering vellykket!</string>
<string name="opml_import_ask_read_permission">Tilgang til ekstern lagring er nødvendig for å lese OPML filen</string>
<string name="successful_import_label">Eksportering vellykket</string>

View File

@ -480,7 +480,7 @@
<string name="database_import_label">Database importeren</string>
<string name="database_import_warning">Bij het importeren van een database worden alle huidige abonnementen en de afspeelgeschiedenis vervangen. Exporteer ter back-up de huidige database. Doorgaan\?</string>
<string name="please_wait">Even geduld…</string>
<string name="export_error_label">Exportfout</string>
<string name="import_export_error_label">Exportfout</string>
<string name="export_success_title">Geëxporteerd</string>
<string name="opml_import_ask_read_permission">Toegang tot externe locaties is nodig om het OPML-bestand te kunnen lezen</string>
<string name="successful_import_label">Importeren voltooid</string>

View File

@ -526,7 +526,7 @@
<string name="database_import_label">Import bazy danych</string>
<string name="database_import_warning">Import bazy danych nadpisze wszystkie twoje aktualne subskrypcje i historię odtworzeń. Zalecany jest eksport aktualnej bazy danych jako kopia zapasowa. Czy chcesz zamienić\?</string>
<string name="please_wait">Proszę czekać…</string>
<string name="export_error_label">Błąd eksportu</string>
<string name="import_export_error_label">Błąd eksportu</string>
<string name="export_success_title">Export zakończony powodzeniem</string>
<string name="opml_import_ask_read_permission">Dostęp do zewnętrznej pamięci jest potrzebny do odczytywania plików OPML</string>
<string name="successful_import_label">Import udany</string>

View File

@ -519,7 +519,7 @@
<string name="database_import_label">Importação do banco de dados</string>
<string name="database_import_warning">A importação de um banco de dados substituirá todas as suas assinaturas atuais e histórico de reprodução. Você deve exportar seu banco de dados atual como um backup. Você quer substituir\?</string>
<string name="please_wait">Por favor aguarde…</string>
<string name="export_error_label">Erro na exportação</string>
<string name="import_export_error_label">Erro na exportação</string>
<string name="export_success_title">Exportado com sucesso</string>
<string name="opml_import_ask_read_permission">É necessário acesso ao armazenamento externo para ler o arquivo OPML</string>
<string name="successful_import_label">Importação bem sucedida</string>

View File

@ -562,7 +562,7 @@
<string name="database_import_label">Importar base de dados</string>
<string name="database_import_warning">A importação de uma base de dados substitui as subscrições atuais e o histórico de reprodução. Deve efetuar um backup da base de dados atual. Substituir\?</string>
<string name="please_wait">Aguarde…</string>
<string name="export_error_label">Erro de exportação</string>
<string name="import_export_error_label">Erro de exportação</string>
<string name="export_success_title">Exportação efetuada</string>
<string name="opml_import_ask_read_permission">Requer acesso ao armazenamento externo para ler o ficheiro OPML</string>
<string name="successful_import_label">Importação bem sucedida</string>

View File

@ -559,7 +559,7 @@
<string name="database_import_label">import bază de date</string>
<string name="database_import_warning">Importarea unei baze de date va înlocui abonamentele tale și istoricul fișierelor redate. Ar trebui să exporți baza de date curentă ca un fișier de rezervă. Doreșți să înlocuieșți\?</string>
<string name="please_wait">Vă rugăm așteptați…</string>
<string name="export_error_label">Eroare exportare</string>
<string name="import_export_error_label">Eroare exportare</string>
<string name="export_success_title">Exportarea a reușit</string>
<string name="opml_import_ask_read_permission">Accesarea memoriei externe este necesară pentru a citi fișierul OPML</string>
<string name="successful_import_label">Importul a reușit</string>

View File

@ -545,7 +545,7 @@
<string name="database_import_label">Импорт базы данных</string>
<string name="database_import_warning">Импорт базы данных приведет к замене всех текущих подписок и истории воспроизведения. Следует экспортировать текущую базу данных, чтобы иметь резервную копию. Хотите заменить\?</string>
<string name="please_wait">Подождите…</string>
<string name="export_error_label">Ошибка экспорта</string>
<string name="import_export_error_label">Ошибка экспорта</string>
<string name="export_success_title">Экспорт выполнен</string>
<string name="opml_import_ask_read_permission">Для чтения файла OPML необходим доступ к внешнему хранилищу</string>
<string name="successful_import_label">Импорт успешен</string>

View File

@ -573,7 +573,7 @@
<string name="database_import_label">Import databázy</string>
<string name="database_import_warning">Import databázy prepíše všetky vaše aktuálne odbery a históriu prehrávania. Aktuálnu databázu by ste mali exportovať ako zálohu. Chcete ju prepísať\?</string>
<string name="please_wait">Prosím čakajte…</string>
<string name="export_error_label">Chyba exportu</string>
<string name="import_export_error_label">Chyba exportu</string>
<string name="export_success_title">Export bol úspešný</string>
<string name="opml_import_ask_read_permission">Na načítanie súboru OPML je potrebný prístup k externému úložisku</string>
<string name="successful_import_label">Import úspešný</string>

View File

@ -382,7 +382,7 @@
<string name="database_import_label">Uvoz podatkovne baze</string>
<string name="database_import_warning">Uvoz podatkovne baze bo nadomestil vse vaše trenutne naročnine in zgodovino predvajanja. Priporočamo, da svojo trenutno zbirko podatkov izvozite kot varnostno kopijo. Ali želite vseeno zamenjati\?</string>
<string name="please_wait">Prosimo počakajte…</string>
<string name="export_error_label">Napaka pri izvozu</string>
<string name="import_export_error_label">Napaka pri izvozu</string>
<string name="export_success_title">Izvoz uspešen</string>
<string name="opml_import_ask_read_permission">Za branje datoteke OPML je potreben dostop do zunanjega pomnilnika</string>
<string name="successful_import_label">Uvoz uspešen</string>

View File

@ -545,7 +545,7 @@
<string name="database_import_label">Databasimport</string>
<string name="database_import_warning">Importering av en databas kommer att ersätta alla dina befintliga prenumerationer och spelhistoriken. Du bör exportera din nuvarande databas som en backup. Vill du ersätta den\?</string>
<string name="please_wait">Vänta…</string>
<string name="export_error_label">Exporteringsfel</string>
<string name="import_export_error_label">Exporteringsfel</string>
<string name="export_success_title">Exporten lyckades</string>
<string name="opml_import_ask_read_permission">Tillgång till extern lagring krävs för att läsa OPML-filen</string>
<string name="successful_import_label">Importering lyckades</string>

View File

@ -522,7 +522,7 @@
<string name="database_import_label">Veri tabanını içe aktarma</string>
<string name="database_import_warning">Bir veritabanını içe aktarmak, mevcut tüm aboneliklerinizi ve çalma geçmişinizi değiştirecektir. Mevcut veritabanınızı yedek olarak dışa aktarmalısınız. Yerini değiştirmek istiyor musunuz\?</string>
<string name="please_wait">Lütfen bekleyin…</string>
<string name="export_error_label">Dışa aktarma hatası</string>
<string name="import_export_error_label">Dışa aktarma hatası</string>
<string name="export_success_title">Dışa aktarma başarılı</string>
<string name="opml_import_ask_read_permission">OPML dosyasını okumak için harici depolama alanına erişim gereklidir</string>
<string name="successful_import_label">İçe aktarma başarılı</string>

View File

@ -573,7 +573,7 @@
<string name="database_import_label">Імпортувати базу даних</string>
<string name="database_import_warning">Імпорт бази даних замінить усі ваші поточні підписки та історію відтворення. Ви повинні експортувати свою поточну базу даних як резервну копію. Бажаєте замінити\?</string>
<string name="please_wait">Будь ласка, зачекайте…</string>
<string name="export_error_label">Помилка експорту</string>
<string name="import_export_error_label">Помилка експорту</string>
<string name="export_success_title">Успішний експорт</string>
<string name="opml_import_ask_read_permission">Щоб прочитати файл OPML потрібен доступ до зовнішнього носія</string>
<string name="successful_import_label">Імпорт - успішний</string>

View File

@ -532,7 +532,7 @@
<string name="database_import_label">数据库导入</string>
<string name="database_import_warning">导入数据库将替换所有当前订阅和播放历史记录。您应该将当前数据库导出为备份。您要替换吗?</string>
<string name="please_wait">请等待…</string>
<string name="export_error_label">导出出错</string>
<string name="import_export_error_label">导出出错</string>
<string name="export_success_title">成功导出</string>
<string name="opml_import_ask_read_permission">读取 OPML 文件需要访问外部存储的权限</string>
<string name="successful_import_label">导入成功</string>

View File

@ -311,7 +311,7 @@
<string name="database_import_label">資料庫匯入</string>
<string name="database_import_warning">匯入的資料將取代您目前的訂閱清單與播放歷史紀錄,您最好先匯出當前的資料庫以便備份。要取代目前的資料嗎?</string>
<string name="please_wait">請稍候…</string>
<string name="export_error_label">匯出錯誤</string>
<string name="import_export_error_label">匯出錯誤</string>
<string name="export_success_title">匯出成功</string>
<string name="opml_import_ask_read_permission">讀取 OPML 檔需要存取外部儲存空間的權限</string>
<string name="successful_import_label">匯入完畢</string>

View File

@ -639,7 +639,7 @@
<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>
<string name="import_export_error_label">Import/Export error</string>
<string name="export_success_title">Export successful</string>
<string name="opml_import_ask_read_permission">Access to external storage is required to read the OPML file</string>
<string name="successful_import_label">Import successful</string>

View File

@ -3,9 +3,9 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch">
<Preference
android:title="@string/choose_data_directory"
android:key="prefChooseDataDir"/>
<!-- <Preference-->
<!-- android:title="@string/choose_data_directory"-->
<!-- android:key="prefChooseDataDir"/>-->
<PreferenceCategory android:title="@string/automation">
<ac.mdiq.podcini.preferences.MaterialListPreference

View File

@ -4,7 +4,6 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation
import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue
import ac.mdiq.podcini.storage.database.Episodes.addToHistory
import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes
@ -14,8 +13,10 @@ import ac.mdiq.podcini.storage.database.Episodes.getEpisodeMedia
import ac.mdiq.podcini.storage.database.Episodes.persistEpisode
import ac.mdiq.podcini.storage.database.Episodes.persistEpisodeMedia
import ac.mdiq.podcini.storage.database.Feeds.deleteFeedSync
import ac.mdiq.podcini.storage.database.Queues
import ac.mdiq.podcini.storage.database.Queues.addToQueue
import ac.mdiq.podcini.storage.database.Queues.clearQueue
import ac.mdiq.podcini.storage.database.Queues.enqueueLocation
import ac.mdiq.podcini.storage.database.Queues.moveInQueue
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
import ac.mdiq.podcini.storage.model.Episode
@ -540,7 +541,7 @@ class DbWriterTest {
@Throws(Exception::class)
private fun queueTestSetupMultipleItems(numItems: Int): Feed {
enqueueLocation = UserPreferences.EnqueueLocation.BACK
enqueueLocation = Queues.EnqueueLocation.BACK
val feed = Feed("url", null, "title")
feed.episodes.clear()
for (i in 0 until numItems) {

View File

@ -3,8 +3,8 @@ package ac.mdiq.podcini.storage
import ac.mdiq.podcini.feed.FeedMother.anyFeed
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
import ac.mdiq.podcini.preferences.UserPreferences.EnqueueLocation
import ac.mdiq.podcini.storage.database.Queues
import ac.mdiq.podcini.storage.database.Queues.EnqueueLocation
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Playable
@ -74,7 +74,7 @@ open class BasicTest {
var idsExpected: List<Long?>? = null
@Parameterized.Parameter(2)
var options: EnqueueLocation? = null
var options: Queues.EnqueueLocation? = null
@Parameterized.Parameter(3)
var curQueue: List<Episode?>? = null

View File

@ -14,7 +14,7 @@ buildscript {
}
plugins {
id 'io.realm.kotlin' version '2.0.0' apply false
id 'io.realm.kotlin' version '2.1.0' apply false
// id 'com.google.devtools.ksp' version '2.0.0-1.0.23' apply false
}

View File

@ -1,3 +1,17 @@
# 6.0.13
* removed from preferences "Choose data folder", it's not suitable for newer Androids
* some class restructuring, especially some functions moved out of Userpreferences
* fixed issue of early termination when exporting a large set of media files
* fixed the mal-functioning feeds and episodes search
* updated realm.kotlin to 2.1.0
# 5.5.5
* this is an extra minor release for better migration to Podcini 6
* fixed issue (in 5.5.4) of terminating pre-maturely when exporting a large set of media files
* this release is not updated to F-Droid due to Podcini 6 listing in progress
# 6.0.12
* re-enabled import of downloaded media files, which can be used to migrate from 5.5.4. Media files imported are restricted to existing feeds and episodes.

View File

@ -0,0 +1,8 @@
Version 6.0.13 brings several changes:
* removed from preferences "Choose data folder", it's not suitable for newer Androids
* some class restructuring, especially some functions moved out of Userpreferences
* fixed issue of early termination when exporting a large set of media files
* fixed the mal-functioning feeds and episodes search
* updated realm.kotlin to 2.1.0