diff --git a/app/build.gradle b/app/build.gradle index 234b2006..dd3ed12b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 = "" diff --git a/app/src/androidTest/kotlin/ac/test/podcini/storage/AutoDownloadTest.kt b/app/src/androidTest/kotlin/ac/test/podcini/storage/AutoDownloadTest.kt index e90d7017..9d2a4a0d 100644 --- a/app/src/androidTest/kotlin/ac/test/podcini/storage/AutoDownloadTest.kt +++ b/app/src/androidTest/kotlin/ac/test/podcini/storage/AutoDownloadTest.kt @@ -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 diff --git a/app/src/androidTest/kotlin/ac/test/podcini/ui/PreferencesTest.kt b/app/src/androidTest/kotlin/ac/test/podcini/ui/PreferencesTest.kt index f2a21bea..02661285 100644 --- a/app/src/androidTest/kotlin/ac/test/podcini/ui/PreferencesTest.kt +++ b/app/src/androidTest/kotlin/ac/test/podcini/ui/PreferencesTest.kt @@ -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)) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/PodciniApp.kt b/app/src/main/kotlin/ac/mdiq/podcini/PodciniApp.kt index f3e75a09..ce7f54f4 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/PodciniApp.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/PodciniApp.kt @@ -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() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/DownloadError.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/DownloadError.kt index 7f0090bd..82f33a8a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/DownloadError.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/DownloadError.kt @@ -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) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequestCreator.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequestCreator.kt index e53701bd..b543efc5 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequestCreator.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequestCreator.kt @@ -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") - } - } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt index 573d41bc..79f3dab6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt @@ -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 { 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 @@ -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() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt index e41b98f2..e14f0057 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt @@ -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) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt index 684bad04..618dd5e0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt @@ -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() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SynchronizationCredentials.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SynchronizationCredentials.kt index 6c7804d2..48017f57 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SynchronizationCredentials.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SynchronizationCredentials.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/utils/NetworkUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/utils/NetworkUtils.kt index e28b9e6f..622f5dab 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/utils/NetworkUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/utils/NetworkUtils.kt @@ -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() + defaultValue.add("images") + val allowed = appPrefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue) + return allowed!!.contains(type) + } + + fun setAllowMobileFor(type: String, allow: Boolean) { + val defaultValue = HashSet() + defaultValue.add("images") + val getValueStringSet = appPrefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue) + val allowed: MutableSet = 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 + 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()) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt index b0a0a12c..46a3513f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt @@ -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 + } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt index c400ce09..c648bb91 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -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) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlBackupAgent.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlBackupAgent.kt index f3ce4cf0..5256db0b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlBackupAgent.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlBackupAgent.kt @@ -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") diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt index 24764e0c..c9844b1e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt @@ -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 - 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 - 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() - 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() - defaultValue.add("images") - val getValueStringSet = appPrefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue) - val allowed: MutableSet = 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?) { - 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 { - if (valueFromPrefs != null) { - try { - val jsonArray = JSONArray(valueFromPrefs) - val selectedSpeeds: MutableList = 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 - } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AutoDownloadPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AutoDownloadPreferencesFragment.kt index e6e8dd86..69ac1fe0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AutoDownloadPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AutoDownloadPreferencesFragment.kt @@ -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?) { + appPrefs.edit().putString(PREF_AUTODL_SELECTED_NETWORKS, value!!.joinToString()).apply() + } + private fun clearAutodownloadSelectedNetworsPreference() { if (selectedNetworks != null) { val prefScreen = preferenceScreen diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt index 699a47eb..c44c05d7 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt @@ -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(PREF_CHOOSE_DATA_DIR)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - ChooseDataFolderDialog.showDialog(requireContext()) { path: String? -> - setDataFolder(path!!) - setDataFolderText() - } - true - } +// findPreference(PREF_CHOOSE_DATA_DIR)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { +// ChooseDataFolderDialog.showDialog(requireContext()) { path: String? -> +// setDataFolder(path!!) +//// setDataFolderText() +// } +// true +// } findPreference(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(PREF_CHOOSE_DATA_DIR)!!.summary = f.absolutePath - } +// private fun setDataFolderText() { +// val f = getDataFolder(null) +// if (f != null) findPreference(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" } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt index d5ec329c..636036b4 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt @@ -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) } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoCleanups.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoCleanups.kt index 7bda8f9c..e56fea54 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoCleanups.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoCleanups.kt @@ -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. diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt index 42be2f49..c02e848c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt @@ -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) + } } \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt index e3f22e22..559de613 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt @@ -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 { 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. diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodeUtil.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodeUtil.kt index ee47b982..82048356 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodeUtil.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodeUtil.kt @@ -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, 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 } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/FileNameGenerator.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/FileNameGenerator.kt index b6489b9b..fda7fe2c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/FileNameGenerator.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/FileNameGenerator.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/FilesUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/FilesUtils.kt new file mode 100644 index 00000000..6a643825 --- /dev/null +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/FilesUtils.kt @@ -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") + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt index 1f00202f..b3c15901 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt @@ -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) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/StorageUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/StorageUtils.kt index 21790d89..7d8f21bc 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/StorageUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/StorageUtils.kt @@ -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 } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/DeleteActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/DeleteActionButton.kt index 54a1a787..d2b7031f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/DeleteActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/DeleteActionButton.kt @@ -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 - } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/DownloadActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/DownloadActionButton.kt index f52cc597..16fdd167 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/DownloadActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/DownloadActionButton.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt index 670b592b..0b70d99b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt @@ -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()) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/MarkAsPlayedActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/MarkAsPlayedActionButton.kt index 17271be0..96f574e3 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/MarkAsPlayedActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/MarkAsPlayedActionButton.kt @@ -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 } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt index c7e90af2..79c2f9fd 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/TTSActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/TTSActionButton.kt index f4d813f9..6d391275 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/TTSActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/TTSActionButton.kt @@ -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 } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/VisitWebsiteActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/VisitWebsiteActionButton.kt index 5cb4128f..faaf63b4 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/VisitWebsiteActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/VisitWebsiteActionButton.kt @@ -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 } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/BugReportActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/BugReportActivity.kt index f7783b20..d6e300ce 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/BugReportActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/BugReportActivity.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedSortDialogNew.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedSortDialogNew.kt index 7b848384..41411cb4 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedSortDialogNew.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedSortDialogNew.kt @@ -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() + } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt index 8346d6c3..a2ef53db 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt @@ -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 + 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 { + if (valueFromPrefs != null) { + try { + val jsonArray = JSONArray(valueFromPrefs) + val selectedSpeeds: MutableList = 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() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val chip = Chip(context) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt index 2b6de5a4..4e546a78 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt @@ -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 = listOf() + private var allEpisodes: List = 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() + } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt index e2a92fc9..2309a6ab 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt @@ -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() + } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt index 610d2d39..f35ccb22 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt @@ -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 { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt index 891fc2f0..64387f00 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt @@ -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()) 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>(emptyList(), emptyList()) - val feed = requireArguments().getLong(ARG_FEED) - val items: List = searchEpisodes(feed, query) + val feedID = requireArguments().getLong(ARG_FEED) + val items: List = searchEpisodes(feedID, query) val feeds: List = searchFeeds(query) + Logd(TAG, "performSearch items: ${items.size} feeds: ${feeds.size}") return Pair, List>(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 { 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 { 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(), View.OnCreateContextMenuListener { + open class HorizontalFeedListAdapter(mainActivity: MainActivity) + : RecyclerView.Adapter(), View.OnCreateContextMenuListener { private val mainActivityRef: WeakReference = WeakReference(mainActivity) private val data: MutableList = 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?) { 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 } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt index c2307fa6..155f26e7 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt @@ -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 = 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() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/NotificationUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/NotificationUtils.kt index afcb9aec..e59ee297 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/NotificationUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/NotificationUtils.kt @@ -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) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/error/CrashReportWriter.kt b/app/src/main/kotlin/ac/mdiq/podcini/util/error/CrashReportWriter.kt index 2270af33..5d425cd1 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/util/error/CrashReportWriter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/util/error/CrashReportWriter.kt @@ -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 diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index d8c07d79..529f31f4 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -578,7 +578,7 @@ استيراد قاعدة البيانات استيراد قاعدة بيانات سيمسح كل اشتراكاتك الحالية وسجل الاستماع. الأفضل أن تصدر قاعدة البيانات الحالية لأرشيف. هل تريد تبديل البيانات؟ يرجى الانتظار… - حدث خطأ أثناء التصدير + حدث خطأ أثناء التصدير تم التصدير بنجاح الوصول الى مساحة التخزين الخارجية مطلوب لقراءة ملف الـ OPML استيراد ناجح diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 9d2574b3..a4bd906f 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -459,7 +459,7 @@ Enporzhiañ ar stlennvon 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ñ? Gortozit un tamm… - Fazi ezporzhiañ + Fazi ezporzhiañ Ezporzhiet gant berzh Ret eo aotren haeziñ ar c\'hadaviñ diavaez evit lenn ar restr OPML Enporzhiet gant berzh diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 9142d8cf..c7f4c002 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -521,7 +521,7 @@ Importa base de dades 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\? Per favor, espera… - Error d\'exportació + Error d\'exportació Exportació correcta Per llegir arxius OPML és necessari accés a la memòria externa Importació correcta diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 4e3b5c7a..0e09b4f7 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -573,7 +573,7 @@ Import databáze 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\? Čekejte prosím… - Chyba exportu + Chyba exportu Export úspěšný Pro přečtení OPML souboru je vyžadován přístup k externímu úložišti Import úspěšný diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 8c682faa..33a27feb 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -545,7 +545,7 @@ Importér database 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\? Vent… - Eksportfejl + Eksportfejl Eksport lykkedes Adgang til eksternt lager er påkrævet for at læse OPML-filen Importeret diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d6595e87..b8d3e0be 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -549,7 +549,7 @@ Datenbank importieren Das Importieren einer Datenbank ersetzt alle Abonnements und abgespielte Episoden. Ein Export als Backup wird empfohlen. Möchtest du die Datenbank wirklich ersetzen\? Bitte warten… - Exportfehler + Exportfehler Export erfolgreich Zugriff auf externen Speicher wird benötigt, um die OPML-Datei zu lesen Import erfolgreich diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 886146f5..8c644b9f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -562,7 +562,7 @@ Importar base de datos 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\? Por favor espera… - Error en la exportación + Error en la exportación Exportación exitosa Necesita acceso al almacenamiento externo para leer archivos OPML Importación exitosa diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index eafd702d..27cccfde 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -403,7 +403,7 @@ Andmebaasi import Andmebaasi importimine asendab kõik sinu praegused tellimused ja kuulamiste ajaloo. Peaksid oma praeguse andmebaasi varundamise eesmärgil enne eksportima. Kas soovid andmed asendada\? Palun oota… - Viga eksportimisel + Viga eksportimisel Eksportimine edukas OPML faili lugemiseks on vajalik ligipääs välisele salvestusruumile Importimine oli edukas diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 8b407f23..86036ed3 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -488,7 +488,7 @@ Ekarri datu basea Datu-base bat inportatzeak egungo harpidetza guztiak eta erreprodukzio-historia ordezkatuko ditu. Egungo datu-basea esportatu behar duzu segurtasun-kopia gisa. Ordezkatu nahi duzu? Itxaron mesedez… - Errorea esportatzean + Errorea esportatzean Esportatzea ongi burutu da Kanpo biltegiratzerako sarbidea behar duzu OPML fitxategia irakurtzeko Inportate arrakastatsua diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index c9a8f056..3971ab17 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -524,7 +524,7 @@ وارد کردن پایگاه داده وارد کردن یک پایگاه داده باعث جایگزین تمام اشتراکهای فعلی و سابقه پخش شما خواهد شد. شما باید پایگاه داده فعلی خود را به عنوان پشتیبان خارج کنید. آیا شما میخواهید جایگزین کنید؟ صبر کنید لطفا … - خطا در خارج کردن + خطا در خارج کردن خارج کردن موفقیت آمیز برای خواندن پروندهٔ OPML نباز به دسترسی حافظهٔ خارجی است وارد کردن موفقیت آمیز diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 2b78b6cf..e694e4de 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -506,7 +506,7 @@ Tietokannan tuonti Tietokannan tuonti korvaa kaikki nykyiset tilauksesi ja toistohistoriasi. Vie nykyinen tietokanta halutessasi varmuuskopioksi ennen jatkamista. Haluatko korvata\? Ole hyvä ja odota… - Vientivirhe + Vientivirhe Vienti onnistunut Pääsy ulkoiseen solvellukseen tarvitaan OPML tiedoston lukemiseen Tuonti onnistui diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 90f46be9..7f7709b1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -562,7 +562,7 @@ Importer la base de données 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 \? Merci de patienter… - Erreur d\'exportation + Erreur d\'exportation Export réussi L\'accès au stockage externe est requis pour lire le fichier OPML Import réussi diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 0bb4e7d2..cf8049be 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -545,7 +545,7 @@ Importar base de datos 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? Agarda… - Fallo ao exportar + Fallo ao exportar Exportado con éxito Precísase acceso ao almacenamento externo para ler o ficheiro OPML Importación correcta diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 8b022b5b..5c1f3aec 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -420,7 +420,7 @@ Adatbázis importálása 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\? Várjon… - Exportálási hiba + Exportálási hiba Exportálás sikeres A külső tároló elérése szükséges az OPML fájl olvasásához Importálás sikeres diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 1d94587a..6590e578 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -429,7 +429,7 @@ Import Database Mengimpor database akan menggantikan seluruh langganan dan riwayat pemutaran Anda. Anda sebaiknya mengekspor database yang sekarang Anda miliki sebagai cadangan. Tetap lanjutkan? Mohon tunggu… - Kesalahan ekspor + Kesalahan ekspor Ekspor berhasil Akses ke penyimpanan eksternal diperlukan untuk membaca berkas OPML Impor sukses! diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 884819d0..11f9de6c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -563,7 +563,7 @@ Importa database 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\? Attendi… - Errore di esportazione + Errore di esportazione Esportazione eseguita Per leggere il file OPML è necessario l\'accesso alla memoria esterna Importazione eseguita diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index e815f098..54b3acb1 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -573,7 +573,7 @@ ייבוא מסד נתונים ייבוא מסד נתונים יחליף את כל המינויים הנוכחיים שלך לרבות היסטוריית הנגינה. עליך תחילה לייצא את מסד הנתונים הנוכחי שלך כגיבוי. להמשיך בהחלפה? נא להמתין… - שגיאת ייצוא + שגיאת ייצוא הייצוא הצליח נדרשת גישה לאחסון חיצוני כדי לקרוא את קובץ ה־OPML הייבוא הצליח diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f7c6ff06..c3a685c7 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -383,7 +383,7 @@ データベースのインポート データベースをインポートすると、現在の購読と再生履歴がすべて置き換えられます。現在のデータベースをバックアップとしてエクスポートする必要があります。それでも置き換えしますか? しばらくお待ちください… - エクスポートエラー + エクスポートエラー エクスポートしました OPML ファイルを読み込むために、外部ストレージへのアクセスが必要です インポートしました diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index c2a39a45..1cb8adac 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -503,7 +503,7 @@ 데이터베이스 가져오기 데이터베이스 가져오기를 하면 모든 구독과 재생 히스토리가 가져온 내용으로 바뀝니다. 백업을 하려면 현재 데이터베이스를 내보내기 해야 합니다. 정말로 데이터베이스를 바꾸시겠습니까\? 기다리십시오… - 내보내기 오류 + 내보내기 오류 내보내기 성공 OPML 파일을 읽으려면 외부 저장소 접근이 필요합니다 가져오기 성공 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 1e9a952a..3b4d612d 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -395,7 +395,7 @@ Duomenų bazės importas 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\? Prašome luktelėti… - Eksporto klaida + Eksporto klaida Sėkmingai eksportuota Norint nuskaityti OPML failą reikalinga prieiga prie išorinės laikmenos Sėkmingai importuota diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index ebf89aca..f2918fd3 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -512,7 +512,7 @@ Database-importering Å importere en database vil erstatte all dine abonnementer og avspillingshistorie. Det er anbefalt å eksportere din eksisterende database som backup. Vil du importere nå\? Vennligst vent… - Feil ved eksportering + Feil ved eksportering Eksportering vellykket! Tilgang til ekstern lagring er nødvendig for å lese OPML filen Eksportering vellykket diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 20fb3956..7c128ef8 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -480,7 +480,7 @@ Database importeren Bij het importeren van een database worden alle huidige abonnementen en de afspeelgeschiedenis vervangen. Exporteer ter back-up de huidige database. Doorgaan\? Even geduld… - Exportfout + Exportfout Geëxporteerd Toegang tot externe locaties is nodig om het OPML-bestand te kunnen lezen Importeren voltooid diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d25011e3..4bdb0e2a 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -526,7 +526,7 @@ Import bazy danych Import bazy danych nadpisze wszystkie twoje aktualne subskrypcje i historię odtworzeń. Zalecany jest eksport aktualnej bazy danych jako kopia zapasowa. Czy chcesz zamienić\? Proszę czekać… - Błąd eksportu + Błąd eksportu Export zakończony powodzeniem Dostęp do zewnętrznej pamięci jest potrzebny do odczytywania plików OPML Import udany diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 97a21d04..2d22c43a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -519,7 +519,7 @@ Importação do banco de dados 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\? Por favor aguarde… - Erro na exportação + Erro na exportação Exportado com sucesso É necessário acesso ao armazenamento externo para ler o arquivo OPML Importação bem sucedida diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 640e13d8..24337d5f 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -562,7 +562,7 @@ Importar base de dados 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\? Aguarde… - Erro de exportação + Erro de exportação Exportação efetuada Requer acesso ao armazenamento externo para ler o ficheiro OPML Importação bem sucedida diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index d81f15f0..8095f317 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -559,7 +559,7 @@ import bază de date 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\? Vă rugăm așteptați… - Eroare exportare + Eroare exportare Exportarea a reușit Accesarea memoriei externe este necesară pentru a citi fișierul OPML Importul a reușit diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 84c2bd01..ad73899f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -545,7 +545,7 @@ Импорт базы данных Импорт базы данных приведет к замене всех текущих подписок и истории воспроизведения. Следует экспортировать текущую базу данных, чтобы иметь резервную копию. Хотите заменить\? Подождите… - Ошибка экспорта + Ошибка экспорта Экспорт выполнен Для чтения файла OPML необходим доступ к внешнему хранилищу Импорт успешен diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 59427482..6fb74552 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -573,7 +573,7 @@ Import databázy 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ť\? Prosím čakajte… - Chyba exportu + Chyba exportu Export bol úspešný Na načítanie súboru OPML je potrebný prístup k externému úložisku Import úspešný diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 6d824807..bc6ece94 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -382,7 +382,7 @@ Uvoz podatkovne baze 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\? Prosimo počakajte… - Napaka pri izvozu + Napaka pri izvozu Izvoz uspešen Za branje datoteke OPML je potreben dostop do zunanjega pomnilnika Uvoz uspešen diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 9e4da7af..81d220eb 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -545,7 +545,7 @@ Databasimport 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\? Vänta… - Exporteringsfel + Exporteringsfel Exporten lyckades Tillgång till extern lagring krävs för att läsa OPML-filen Importering lyckades diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1efae463..3f5c0eab 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -522,7 +522,7 @@ Veri tabanını içe aktarma 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\? Lütfen bekleyin… - Dışa aktarma hatası + Dışa aktarma hatası Dışa aktarma başarılı OPML dosyasını okumak için harici depolama alanına erişim gereklidir İçe aktarma başarılı diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 1d57d798..505bc8b2 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -573,7 +573,7 @@ Імпортувати базу даних Імпорт бази даних замінить усі ваші поточні підписки та історію відтворення. Ви повинні експортувати свою поточну базу даних як резервну копію. Бажаєте замінити\? Будь ласка, зачекайте… - Помилка експорту + Помилка експорту Успішний експорт Щоб прочитати файл OPML потрібен доступ до зовнішнього носія Імпорт - успішний diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 132c3cc0..20190c22 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -532,7 +532,7 @@ 数据库导入 导入数据库将替换所有当前订阅和播放历史记录。您应该将当前数据库导出为备份。您要替换吗? 请等待… - 导出出错 + 导出出错 成功导出 读取 OPML 文件需要访问外部存储的权限 导入成功 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index bf0d47bd..43121bf6 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -311,7 +311,7 @@ 資料庫匯入 匯入的資料將取代您目前的訂閱清單與播放歷史紀錄,您最好先匯出當前的資料庫以便備份。要取代目前的資料嗎? 請稍候… - 匯出錯誤 + 匯出錯誤 匯出成功 讀取 OPML 檔需要存取外部儲存空間的權限 匯入完畢 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b78ea266..3ccebe59 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -639,7 +639,7 @@ Only accepting directory name including: Only accepting file extension: Please wait… - Export error + Import/Export error Export successful Access to external storage is required to read the OPML file Import successful diff --git a/app/src/main/res/xml/preferences_downloads.xml b/app/src/main/res/xml/preferences_downloads.xml index da36976a..c5ad734c 100644 --- a/app/src/main/res/xml/preferences_downloads.xml +++ b/app/src/main/res/xml/preferences_downloads.xml @@ -3,9 +3,9 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch"> - + + + ? = null @Parameterized.Parameter(2) - var options: EnqueueLocation? = null + var options: Queues.EnqueueLocation? = null @Parameterized.Parameter(3) var curQueue: List? = null diff --git a/build.gradle b/build.gradle index 9cfba1e1..9b19acd2 100644 --- a/build.gradle +++ b/build.gradle @@ -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 } diff --git a/changelog.md b/changelog.md index 43fd9c05..bd1ea44d 100644 --- a/changelog.md +++ b/changelog.md @@ -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. diff --git a/fastlane/metadata/android/en-US/changelogs/3020213.txt b/fastlane/metadata/android/en-US/changelogs/3020213.txt new file mode 100644 index 00000000..53f3de61 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020213.txt @@ -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