diff --git a/7_podcast.jpg b/7_podcast.jpg deleted file mode 100644 index 36ba875c..00000000 Binary files a/7_podcast.jpg and /dev/null differ diff --git a/README.md b/README.md index 33bcfd23..65260872 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,15 @@ This is based on a fork from the popular project AntennaPod ( - + @@ -28,4 +32,4 @@ Podcini, same as its forked project AntennaPod, is licensed under the GNU Genera New files and modifications in the project is copyrighted in 2024 by Xilin Jia. -Original files from the forked project maintains copyrights of the AntennaPod team. \ No newline at end of file +Original contents from the forked project maintains copyrights of the AntennaPod team. \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 14a66bd2..f137841f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,8 +22,8 @@ android { // Version code schema: // "1.2.3-beta4" -> 1020304 // "1.2.3" -> 1020395 - versionCode 3020100 - versionName "3.2.5" + versionCode 3020101 + versionName "4.0.1" def commit = "" try { @@ -37,11 +37,20 @@ android { } buildConfigField "String", "COMMIT_HASH", ('"' + (commit.isEmpty() ? "Unknown commit" : commit) + '"') - javaCompileOptions { - annotationProcessorOptions { - arguments = [eventBusIndex: 'ac.mdiq.podcini.ApEventBusIndex'] - } +// javaCompileOptions { +// annotationProcessorOptions { +// arguments = [eventBusIndex: 'ac.mdiq.podcini.ApEventBusIndex'] +// } +// } + + if (project.hasProperty("podcastindexApiKey")) { + buildConfigField "String", "PODCASTINDEX_API_KEY", '"' + podcastindexApiKey + '"' + buildConfigField "String", "PODCASTINDEX_API_SECRET", '"' + podcastindexApiSecret + '"' + } else { + buildConfigField "String", "PODCASTINDEX_API_KEY", '"XTMMQGA2YZ4WJUBYY4HK"' + buildConfigField "String", "PODCASTINDEX_API_SECRET", '"XAaAhk4^2YBsTE33vdbwbZNj82ZRLABDDqFdKe7x"' } + } signingConfigs { releaseConfig { @@ -91,46 +100,35 @@ dependencies { } } - implementation project(":core") - implementation project(":event") - implementation project(':model') - implementation project(':net:common') - implementation project(':net:discovery') - implementation project(':net:download:service-interface') - implementation project(':net:sync:gpoddernet') - implementation project(':net:sync:model') - implementation project(':parser:feed') - implementation project(':playback:base') - implementation project(':playback:cast') - implementation project(':storage:database') - implementation project(':storage:preferences') - implementation project(':ui:app-start-intent') - implementation project(':ui:common') - implementation project(':ui:echo') - implementation project(':ui:glide') - implementation project(':ui:i18n') - implementation project(':ui:statistics') + kapt "androidx.annotation:annotation:$annotationVersion" - kapt "androidx.annotation:annotation:1.7.1" - - implementation "androidx.appcompat:appcompat:1.6.1" + implementation "androidx.appcompat:appcompat:$appcompatVersion" implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' - implementation "androidx.core:core-ktx:$coreVersion" implementation "androidx.fragment:fragment-ktx:$fragmentVersion" implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation "androidx.media:media:$mediaVersion" + implementation "androidx.media3:media3-exoplayer:$media3Version" + implementation "androidx.media3:media3-ui:$media3Version" + implementation "androidx.media3:media3-datasource-okhttp:$media3Version" implementation "androidx.media3:media3-common:$media3Version" implementation "androidx.palette:palette-ktx:$paletteVersion" implementation "androidx.preference:preference-ktx:$preferenceVersion" implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion" implementation "androidx.viewpager2:viewpager2:$viewPager2Version" implementation "androidx.work:work-runtime:$workManagerVersion" + implementation "androidx.core:core-splashscreen:1.0.1" + implementation 'androidx.documentfile:documentfile:1.0.1' + implementation "com.google.android.material:material:$googleMaterialVersion" implementation "org.apache.commons:commons-lang3:$commonslangVersion" implementation "commons-io:commons-io:$commonsioVersion" implementation "org.jsoup:jsoup:$jsoupVersion" + implementation "com.github.bumptech.glide:glide:$glideVersion" + implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion@aar" + annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion" + implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:okhttp-urlconnection:$okhttpVersion" implementation "com.squareup.okio:okio:$okioVersion" @@ -152,7 +150,6 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.0' compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion" - androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion" androidTestImplementation 'com.nanohttpd:nanohttpd:2.1.1' androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion" @@ -160,6 +157,33 @@ dependencies { androidTestImplementation "androidx.test:runner:$runnerVersion" androidTestImplementation "androidx.test:rules:$rulesVersion" androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion" + + implementation "com.annimon:stream:$annimonStreamVersion" + implementation 'com.github.mfietz:fyydlin:v0.5.0' + + // Non-free dependencies: + + testImplementation "androidx.test:core:$testCoreVersion" + testImplementation "org.awaitility:awaitility:$awaitilityVersion" + testImplementation "junit:junit:$junitVersion" + testImplementation 'org.mockito:mockito-inline:5.2.0' + testImplementation "org.robolectric:robolectric:$robolectricVersion" + testImplementation 'javax.inject:javax.inject:1' + + + playImplementation 'com.google.android.gms:play-services-base:17.5.0' + freeImplementation 'org.conscrypt:conscrypt-android:2.5.2' + + playApi 'androidx.mediarouter:mediarouter:1.6.0' + playApi "com.google.android.support:wearable:$wearableSupportVersion" + playApi 'com.google.android.gms:play-services-cast-framework:21.2.0' +} + +kapt { + arguments { + arg('eventBusIndex', 'ac.mdiq.podcini.ApEventBusIndex') + } } if (project.hasProperty("podciniPlayPublisherCredentials")) { diff --git a/app/src/androidTest/java/ac/test/podcini/EspressoTestUtils.kt b/app/src/androidTest/java/ac/test/podcini/EspressoTestUtils.kt index 0725c1c8..4ef27b30 100644 --- a/app/src/androidTest/java/ac/test/podcini/EspressoTestUtils.kt +++ b/app/src/androidTest/java/ac/test/podcini/EspressoTestUtils.kt @@ -17,15 +17,15 @@ import androidx.test.espresso.util.HumanReadables import androidx.test.espresso.util.TreeIterables import androidx.test.platform.app.InstrumentationRegistry import ac.mdiq.podcini.R -import ac.mdiq.podcini.activity.MainActivity -import ac.mdiq.podcini.core.service.playback.PlaybackService -import ac.mdiq.podcini.dialog.RatingDialog -import ac.mdiq.podcini.dialog.RatingDialog.saveRated -import ac.mdiq.podcini.fragment.NavDrawerFragment +import ac.mdiq.podcini.ui.activity.MainActivity +import ac.mdiq.podcini.service.playback.PlaybackService +import ac.mdiq.podcini.ui.dialog.RatingDialog +import ac.mdiq.podcini.ui.dialog.RatingDialog.saveRated +import ac.mdiq.podcini.ui.fragment.NavDrawerFragment import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.deleteDatabase import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.getInstance import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.init -import ac.mdiq.podcini.storage.preferences.UserPreferences +import ac.mdiq.podcini.preferences.UserPreferences import junit.framework.AssertionFailedError import org.awaitility.Awaitility import org.awaitility.core.ConditionTimeoutException diff --git a/app/src/androidTest/java/ac/test/podcini/dialogs/ShareDialogTest.kt b/app/src/androidTest/java/ac/test/podcini/dialogs/ShareDialogTest.kt index 369df5de..4b88f6b9 100644 --- a/app/src/androidTest/java/ac/test/podcini/dialogs/ShareDialogTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/dialogs/ShareDialogTest.kt @@ -11,8 +11,8 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import ac.mdiq.podcini.R -import ac.mdiq.podcini.activity.MainActivity -import ac.mdiq.podcini.fragment.AllEpisodesFragment +import ac.mdiq.podcini.ui.activity.MainActivity +import ac.mdiq.podcini.ui.fragment.AllEpisodesFragment import de.test.podcini.EspressoTestUtils import de.test.podcini.NthMatcher import de.test.podcini.ui.UITestUtils diff --git a/app/src/androidTest/java/ac/test/podcini/playback/PlaybackTest.kt b/app/src/androidTest/java/ac/test/podcini/playback/PlaybackTest.kt index 9fb74e31..7a3181c4 100644 --- a/app/src/androidTest/java/ac/test/podcini/playback/PlaybackTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/playback/PlaybackTest.kt @@ -13,19 +13,19 @@ import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.ActivityTestRule import ac.mdiq.podcini.R -import ac.mdiq.podcini.activity.MainActivity -import ac.mdiq.podcini.core.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId -import ac.mdiq.podcini.core.receiver.MediaButtonReceiver.Companion.createIntent -import ac.mdiq.podcini.core.storage.DBReader.getEpisodes -import ac.mdiq.podcini.core.storage.DBReader.getFeedItem -import ac.mdiq.podcini.core.storage.DBReader.getQueue -import ac.mdiq.podcini.core.storage.DBReader.getQueueIDList -import ac.mdiq.podcini.core.storage.DBWriter.clearQueue -import ac.mdiq.podcini.core.util.playback.PlaybackController -import ac.mdiq.podcini.model.feed.FeedItemFilter.Companion.unfiltered -import ac.mdiq.podcini.model.feed.SortOrder +import ac.mdiq.podcini.ui.activity.MainActivity +import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId +import ac.mdiq.podcini.receiver.MediaButtonReceiver.Companion.createIntent +import ac.mdiq.podcini.storage.DBReader.getEpisodes +import ac.mdiq.podcini.storage.DBReader.getFeedItem +import ac.mdiq.podcini.storage.DBReader.getQueue +import ac.mdiq.podcini.storage.DBReader.getQueueIDList +import ac.mdiq.podcini.storage.DBWriter.clearQueue +import ac.mdiq.podcini.playback.PlaybackController +import ac.mdiq.podcini.storage.model.feed.FeedItemFilter.Companion.unfiltered +import ac.mdiq.podcini.storage.model.feed.SortOrder import ac.mdiq.podcini.playback.base.PlayerStatus -import ac.mdiq.podcini.storage.preferences.UserPreferences +import ac.mdiq.podcini.preferences.UserPreferences import de.test.podcini.EspressoTestUtils import de.test.podcini.IgnoreOnCi import de.test.podcini.ui.UITestUtils @@ -44,7 +44,7 @@ class PlaybackTest { var activityTestRule: ActivityTestRule = ActivityTestRule(MainActivity::class.java, false, false) private var uiTestUtils: UITestUtils? = null - protected var context: Context? = null + protected lateinit var context: Context private var controller: PlaybackController? = null @Before @@ -54,7 +54,7 @@ class PlaybackTest { EspressoTestUtils.clearPreferences() EspressoTestUtils.clearDatabase() - uiTestUtils = UITestUtils(context!!) + uiTestUtils = UITestUtils(context) uiTestUtils!!.setup() } @@ -192,28 +192,28 @@ class PlaybackTest { } protected fun setContinuousPlaybackPreference(value: Boolean) { - val prefs = PreferenceManager.getDefaultSharedPreferences(context!!) + val prefs = PreferenceManager.getDefaultSharedPreferences(context) prefs.edit().putBoolean(UserPreferences.PREF_FOLLOW_QUEUE, value).commit() } protected fun setSkipKeepsEpisodePreference(value: Boolean) { - val prefs = PreferenceManager.getDefaultSharedPreferences(context!!) + val prefs = PreferenceManager.getDefaultSharedPreferences(context) prefs.edit().putBoolean(UserPreferences.PREF_SKIP_KEEPS_EPISODE, value).commit() } protected fun setSmartMarkAsPlayedPreference(smartMarkAsPlayedSecs: Int) { - val prefs = PreferenceManager.getDefaultSharedPreferences(context!!) + val prefs = PreferenceManager.getDefaultSharedPreferences(context) prefs.edit().putString(UserPreferences.PREF_SMART_MARK_AS_PLAYED_SECS, smartMarkAsPlayedSecs.toString(10)) .commit() } private fun skipEpisode() { - context!!.sendBroadcast(createIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT)) + context.sendBroadcast(createIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT)) } protected fun pauseEpisode() { - context!!.sendBroadcast(createIntent(context, KeyEvent.KEYCODE_MEDIA_PAUSE)) + context.sendBroadcast(createIntent(context, KeyEvent.KEYCODE_MEDIA_PAUSE)) } protected fun startLocalPlayback() { diff --git a/app/src/androidTest/java/ac/test/podcini/service/download/HttpDownloaderTest.kt b/app/src/androidTest/java/ac/test/podcini/service/download/HttpDownloaderTest.kt index 984b75a5..660ead29 100644 --- a/app/src/androidTest/java/ac/test/podcini/service/download/HttpDownloaderTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/service/download/HttpDownloaderTest.kt @@ -3,12 +3,12 @@ package de.test.podcini.service.download import android.util.Log import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry -import ac.mdiq.podcini.core.service.download.Downloader -import ac.mdiq.podcini.core.service.download.HttpDownloader -import ac.mdiq.podcini.model.download.DownloadError -import ac.mdiq.podcini.model.feed.FeedFile +import ac.mdiq.podcini.service.download.Downloader +import ac.mdiq.podcini.service.download.HttpDownloader +import ac.mdiq.podcini.storage.model.download.DownloadError +import ac.mdiq.podcini.storage.model.feed.FeedFile import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest -import ac.mdiq.podcini.storage.preferences.UserPreferences.init +import ac.mdiq.podcini.preferences.UserPreferences.init import de.test.podcini.util.service.download.HTTPBin import org.junit.After import org.junit.Assert @@ -28,10 +28,11 @@ class HttpDownloaderTest { @Throws(Exception::class) fun tearDown() { val contents = destDir!!.listFiles() - for (f in contents) { - Assert.assertTrue(f.delete()) + if (contents != null) { + for (f in contents) { + Assert.assertTrue(f.delete()) + } } - httpServer!!.stop() } @@ -61,18 +62,18 @@ class HttpDownloaderTest { private fun download(url: String?, title: String, expectedResult: Boolean, deleteExisting: Boolean = true, username: String? = null, password: String? = null - ): Downloader { + ): ac.mdiq.podcini.service.download.Downloader { val feedFile: FeedFile = setupFeedFile(url, title, deleteExisting) val request = DownloadRequest( feedFile.getFile_url()!!, url!!, title, 0, feedFile.getTypeAsInt(), username, password, null, false) - val downloader: Downloader = HttpDownloader(request) + val downloader: ac.mdiq.podcini.service.download.Downloader = HttpDownloader(request) downloader.call() val status = downloader.result Assert.assertNotNull(status) Assert.assertEquals(expectedResult, status.isSuccessful) // the file should not exist if the download has failed and deleteExisting was true - Assert.assertTrue(!deleteExisting || File(feedFile.getFile_url()).exists() == expectedResult) + Assert.assertTrue(!deleteExisting || File(feedFile.getFile_url()!!).exists() == expectedResult) return downloader } @@ -100,7 +101,7 @@ class HttpDownloaderTest { fun testCancel() { val url = httpServer!!.baseUrl + "/delay/3" val feedFile = setupFeedFile(url, "delay", true) - val downloader: Downloader = HttpDownloader(DownloadRequest( + val downloader: ac.mdiq.podcini.service.download.Downloader = HttpDownloader(DownloadRequest( feedFile.getFile_url()!!, url, "delay", 0, feedFile.getTypeAsInt(), null, null, null, false)) val t: Thread = object : Thread() { @@ -122,7 +123,7 @@ class HttpDownloaderTest { @Test fun testDeleteOnFailShouldDelete() { val downloader = download(url404, "testDeleteOnFailShouldDelete", false, true, null, null) - Assert.assertFalse(File(downloader.downloadRequest.destination).exists()) + Assert.assertFalse(File(downloader.downloadRequest.destination!!).exists()) } @Test @@ -133,7 +134,7 @@ class HttpDownloaderTest { dest.delete() Assert.assertTrue(dest.createNewFile()) val downloader = download(url404, filename, false, false, null, null) - Assert.assertTrue(File(downloader.downloadRequest.destination).exists()) + Assert.assertTrue(File(downloader.downloadRequest.destination!!).exists()) } @Test diff --git a/app/src/androidTest/java/ac/test/podcini/service/playback/CancelablePSMPCallback.kt b/app/src/androidTest/java/ac/test/podcini/service/playback/CancelablePSMPCallback.kt index 2ad07073..09c38193 100644 --- a/app/src/androidTest/java/ac/test/podcini/service/playback/CancelablePSMPCallback.kt +++ b/app/src/androidTest/java/ac/test/podcini/service/playback/CancelablePSMPCallback.kt @@ -1,7 +1,7 @@ package de.test.podcini.service.playback -import ac.mdiq.podcini.model.playback.MediaType -import ac.mdiq.podcini.model.playback.Playable +import ac.mdiq.podcini.storage.model.playback.MediaType +import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPCallback import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPInfo diff --git a/app/src/androidTest/java/ac/test/podcini/service/playback/DefaultPSMPCallback.kt b/app/src/androidTest/java/ac/test/podcini/service/playback/DefaultPSMPCallback.kt index ca1b62e6..86ad1cda 100644 --- a/app/src/androidTest/java/ac/test/podcini/service/playback/DefaultPSMPCallback.kt +++ b/app/src/androidTest/java/ac/test/podcini/service/playback/DefaultPSMPCallback.kt @@ -1,7 +1,7 @@ package de.test.podcini.service.playback -import ac.mdiq.podcini.model.playback.MediaType -import ac.mdiq.podcini.model.playback.Playable +import ac.mdiq.podcini.storage.model.playback.MediaType +import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPCallback import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPInfo diff --git a/app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceMediaPlayerTest.kt b/app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceMediaPlayerTest.kt index d0eb9276..2494d61f 100644 --- a/app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceMediaPlayerTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceMediaPlayerTest.kt @@ -3,9 +3,9 @@ package de.test.podcini.service.playback import androidx.test.annotation.UiThreadTest import androidx.test.filters.MediumTest import androidx.test.platform.app.InstrumentationRegistry -import ac.mdiq.podcini.core.service.playback.LocalPSMP -import ac.mdiq.podcini.model.feed.* -import ac.mdiq.podcini.model.playback.Playable +import ac.mdiq.podcini.service.playback.LocalPSMP +import ac.mdiq.podcini.storage.model.feed.* +import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPInfo import ac.mdiq.podcini.playback.base.PlayerStatus diff --git a/app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceTaskManagerTest.kt b/app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceTaskManagerTest.kt index 32caf647..b7b2ae7d 100644 --- a/app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceTaskManagerTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceTaskManagerTest.kt @@ -3,15 +3,15 @@ package de.test.podcini.service.playback import androidx.test.annotation.UiThreadTest import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry -import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.setShakeToReset -import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.setVibrate -import ac.mdiq.podcini.core.service.playback.PlaybackServiceTaskManager -import ac.mdiq.podcini.core.service.playback.PlaybackServiceTaskManager.PSTMCallback -import ac.mdiq.podcini.core.widget.WidgetUpdater.WidgetState -import ac.mdiq.podcini.event.playback.SleepTimerUpdatedEvent -import ac.mdiq.podcini.model.feed.Feed -import ac.mdiq.podcini.model.feed.FeedItem -import ac.mdiq.podcini.model.playback.Playable +import ac.mdiq.podcini.preferences.SleepTimerPreferences.setShakeToReset +import ac.mdiq.podcini.preferences.SleepTimerPreferences.setVibrate +import ac.mdiq.podcini.service.playback.PlaybackServiceTaskManager +import ac.mdiq.podcini.service.playback.PlaybackServiceTaskManager.PSTMCallback +import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState +import ac.mdiq.podcini.playback.event.SleepTimerUpdatedEvent +import ac.mdiq.podcini.storage.model.feed.Feed +import ac.mdiq.podcini.storage.model.feed.FeedItem +import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.deleteDatabase import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.getInstance diff --git a/app/src/androidTest/java/ac/test/podcini/service/playback/SleepTimerPreferencesTest.kt b/app/src/androidTest/java/ac/test/podcini/service/playback/SleepTimerPreferencesTest.kt index 4c95f072..8dd62b0f 100644 --- a/app/src/androidTest/java/ac/test/podcini/service/playback/SleepTimerPreferencesTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/service/playback/SleepTimerPreferencesTest.kt @@ -1,6 +1,6 @@ package de.test.podcini.service.playback -import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.isInTimeRange +import ac.mdiq.podcini.preferences.SleepTimerPreferences.isInTimeRange import org.junit.Assert import org.junit.Test diff --git a/app/src/androidTest/java/ac/test/podcini/storage/AutoDownloadTest.kt b/app/src/androidTest/java/ac/test/podcini/storage/AutoDownloadTest.kt index 78cd6efa..0f2f0946 100644 --- a/app/src/androidTest/java/ac/test/podcini/storage/AutoDownloadTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/storage/AutoDownloadTest.kt @@ -2,14 +2,14 @@ package de.test.podcini.storage import android.content.Context import androidx.test.core.app.ApplicationProvider -import ac.mdiq.podcini.core.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId -import ac.mdiq.podcini.core.storage.AutomaticDownloadAlgorithm -import ac.mdiq.podcini.core.storage.DBReader.getQueue -import ac.mdiq.podcini.core.storage.DBTasks.setDownloadAlgorithm -import ac.mdiq.podcini.core.util.playback.PlaybackServiceStarter -import ac.mdiq.podcini.model.feed.FeedItem -import ac.mdiq.podcini.storage.preferences.UserPreferences.isAllowMobileStreaming -import ac.mdiq.podcini.storage.preferences.UserPreferences.isFollowQueue +import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId +import ac.mdiq.podcini.storage.AutomaticDownloadAlgorithm +import ac.mdiq.podcini.storage.DBReader.getQueue +import ac.mdiq.podcini.storage.DBTasks.setDownloadAlgorithm +import ac.mdiq.podcini.playback.PlaybackServiceStarter +import ac.mdiq.podcini.storage.model.feed.FeedItem +import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileStreaming +import ac.mdiq.podcini.preferences.UserPreferences.isFollowQueue import de.test.podcini.EspressoTestUtils import de.test.podcini.ui.UITestUtils import org.awaitility.Awaitility @@ -106,7 +106,7 @@ class AutoDownloadTest { var currentlyPlayingAtDownload: Long = -1 private set - override fun autoDownloadUndownloadedItems(context: Context?): Runnable? { + override fun autoDownloadUndownloadedItems(context: Context): Runnable? { return Runnable { if (currentlyPlayingAtDownload == -1L) { currentlyPlayingAtDownload = currentlyPlayingFeedMediaId diff --git a/app/src/androidTest/java/ac/test/podcini/ui/FeedSettingsTest.kt b/app/src/androidTest/java/ac/test/podcini/ui/FeedSettingsTest.kt index c25ae273..9e2ca0e6 100644 --- a/app/src/androidTest/java/ac/test/podcini/ui/FeedSettingsTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/ui/FeedSettingsTest.kt @@ -8,8 +8,8 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import ac.mdiq.podcini.R -import ac.mdiq.podcini.activity.MainActivity -import ac.mdiq.podcini.model.feed.Feed +import ac.mdiq.podcini.ui.activity.MainActivity +import ac.mdiq.podcini.storage.model.feed.Feed import de.test.podcini.EspressoTestUtils import org.hamcrest.Matchers import org.junit.After diff --git a/app/src/androidTest/java/ac/test/podcini/ui/MainActivityTest.kt b/app/src/androidTest/java/ac/test/podcini/ui/MainActivityTest.kt index 31ebc286..c1aa6cf0 100644 --- a/app/src/androidTest/java/ac/test/podcini/ui/MainActivityTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/ui/MainActivityTest.kt @@ -8,7 +8,7 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import ac.mdiq.podcini.R -import ac.mdiq.podcini.activity.MainActivity +import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.deleteDatabase import de.test.podcini.EspressoTestUtils import org.junit.After diff --git a/app/src/androidTest/java/ac/test/podcini/ui/NavigationDrawerTest.kt b/app/src/androidTest/java/ac/test/podcini/ui/NavigationDrawerTest.kt index 3806cf08..19caf9f4 100644 --- a/app/src/androidTest/java/ac/test/podcini/ui/NavigationDrawerTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/ui/NavigationDrawerTest.kt @@ -11,10 +11,10 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import ac.mdiq.podcini.R -import ac.mdiq.podcini.activity.MainActivity -import ac.mdiq.podcini.activity.PreferenceActivity -import ac.mdiq.podcini.fragment.* -import ac.mdiq.podcini.storage.preferences.UserPreferences.hiddenDrawerItems +import ac.mdiq.podcini.ui.activity.MainActivity +import ac.mdiq.podcini.ui.activity.PreferenceActivity +import ac.mdiq.podcini.ui.fragment.* +import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems import de.test.podcini.EspressoTestUtils import de.test.podcini.NthMatcher import org.hamcrest.Matchers @@ -63,13 +63,6 @@ class NavigationDrawerTest { hiddenDrawerItems = ArrayList() activityRule.launchActivity(Intent()) - // home - openNavDrawer() - EspressoTestUtils.onDrawerItem(ViewMatchers.withText(R.string.home_label)).perform(ViewActions.click()) - Espresso.onView(ViewMatchers.isRoot()) - .perform(EspressoTestUtils.waitForView(Matchers.allOf(ViewMatchers.isDescendantOfA(ViewMatchers.withId(R.id.toolbar)), - ViewMatchers.withText(R.string.home_label)), 1000)) - // queue openNavDrawer() EspressoTestUtils.onDrawerItem(ViewMatchers.withText(R.string.queue_label)).perform(ViewActions.click()) @@ -159,7 +152,7 @@ class NavigationDrawerTest { @Test fun testDrawerPreferencesUnhideSomeElements() { - var hidden = listOf(PlaybackHistoryFragment.TAG, CompletedDownloadsFragment.TAG) + var hidden = listOf(PlaybackHistoryFragment.TAG, CompletedDownloadsFragment.TAG) hiddenDrawerItems = hidden activityRule.launchActivity(Intent()) openNavDrawer() @@ -170,7 +163,7 @@ class NavigationDrawerTest { Espresso.onView(ViewMatchers.withText(R.string.downloads_label)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withText(R.string.confirm_label)).perform(ViewActions.click()) - hidden = hiddenDrawerItems as List + hidden = hiddenDrawerItems?.filterNotNull()?: listOf() Assert.assertEquals(2, hidden.size.toLong()) Assert.assertTrue(hidden.contains(QueueFragment.TAG)) Assert.assertTrue(hidden.contains(PlaybackHistoryFragment.TAG)) diff --git a/app/src/androidTest/java/ac/test/podcini/ui/PreferencesTest.kt b/app/src/androidTest/java/ac/test/podcini/ui/PreferencesTest.kt index 7ff5f647..2fcab376 100644 --- a/app/src/androidTest/java/ac/test/podcini/ui/PreferencesTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/ui/PreferencesTest.kt @@ -11,33 +11,33 @@ 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.activity.PreferenceActivity -import ac.mdiq.podcini.core.storage.APCleanupAlgorithm -import ac.mdiq.podcini.core.storage.APNullCleanupAlgorithm -import ac.mdiq.podcini.core.storage.APQueueCleanupAlgorithm -import ac.mdiq.podcini.core.storage.EpisodeCleanupAlgorithmFactory.build -import ac.mdiq.podcini.core.storage.ExceptFavoriteCleanupAlgorithm -import ac.mdiq.podcini.storage.preferences.UserPreferences -import ac.mdiq.podcini.storage.preferences.UserPreferences.EnqueueLocation -import ac.mdiq.podcini.storage.preferences.UserPreferences.enqueueLocation -import ac.mdiq.podcini.storage.preferences.UserPreferences.episodeCacheSize -import ac.mdiq.podcini.storage.preferences.UserPreferences.fastForwardSecs -import ac.mdiq.podcini.storage.preferences.UserPreferences.init -import ac.mdiq.podcini.storage.preferences.UserPreferences.isAutoDelete -import ac.mdiq.podcini.storage.preferences.UserPreferences.isAutoDeleteLocal -import ac.mdiq.podcini.storage.preferences.UserPreferences.isEnableAutodownload -import ac.mdiq.podcini.storage.preferences.UserPreferences.isEnableAutodownloadOnBattery -import ac.mdiq.podcini.storage.preferences.UserPreferences.isFollowQueue -import ac.mdiq.podcini.storage.preferences.UserPreferences.isPauseOnHeadsetDisconnect -import ac.mdiq.podcini.storage.preferences.UserPreferences.isPersistNotify -import ac.mdiq.podcini.storage.preferences.UserPreferences.isUnpauseOnBluetoothReconnect -import ac.mdiq.podcini.storage.preferences.UserPreferences.isUnpauseOnHeadsetReconnect -import ac.mdiq.podcini.storage.preferences.UserPreferences.rewindSecs -import ac.mdiq.podcini.storage.preferences.UserPreferences.shouldDeleteRemoveFromQueue -import ac.mdiq.podcini.storage.preferences.UserPreferences.shouldPauseForFocusLoss -import ac.mdiq.podcini.storage.preferences.UserPreferences.showNextChapterOnFullNotification -import ac.mdiq.podcini.storage.preferences.UserPreferences.showPlaybackSpeedOnFullNotification -import ac.mdiq.podcini.storage.preferences.UserPreferences.showSkipOnFullNotification +import ac.mdiq.podcini.ui.activity.PreferenceActivity +import ac.mdiq.podcini.storage.APCleanupAlgorithm +import ac.mdiq.podcini.storage.APNullCleanupAlgorithm +import ac.mdiq.podcini.storage.APQueueCleanupAlgorithm +import ac.mdiq.podcini.storage.EpisodeCleanupAlgorithmFactory.build +import ac.mdiq.podcini.storage.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 +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 de.test.podcini.EspressoTestUtils import org.awaitility.Awaitility import org.junit.Assert diff --git a/app/src/androidTest/java/ac/test/podcini/ui/QueueFragmentTest.kt b/app/src/androidTest/java/ac/test/podcini/ui/QueueFragmentTest.kt index f8d41d9c..c8d44a08 100644 --- a/app/src/androidTest/java/ac/test/podcini/ui/QueueFragmentTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/ui/QueueFragmentTest.kt @@ -7,8 +7,8 @@ import androidx.test.espresso.intent.rule.IntentsTestRule import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.runners.AndroidJUnit4 import ac.mdiq.podcini.R -import ac.mdiq.podcini.activity.MainActivity -import ac.mdiq.podcini.fragment.QueueFragment +import ac.mdiq.podcini.ui.activity.MainActivity +import ac.mdiq.podcini.ui.fragment.QueueFragment import de.test.podcini.EspressoTestUtils import de.test.podcini.NthMatcher import org.hamcrest.CoreMatchers diff --git a/app/src/androidTest/java/ac/test/podcini/ui/TextOnlyFeedsTest.kt b/app/src/androidTest/java/ac/test/podcini/ui/TextOnlyFeedsTest.kt index c9cac468..e0744156 100644 --- a/app/src/androidTest/java/ac/test/podcini/ui/TextOnlyFeedsTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/ui/TextOnlyFeedsTest.kt @@ -8,7 +8,7 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import ac.mdiq.podcini.R -import ac.mdiq.podcini.activity.MainActivity +import ac.mdiq.podcini.ui.activity.MainActivity import de.test.podcini.EspressoTestUtils import org.hamcrest.CoreMatchers import org.junit.After diff --git a/app/src/androidTest/java/ac/test/podcini/ui/UITestUtils.kt b/app/src/androidTest/java/ac/test/podcini/ui/UITestUtils.kt index ba72be17..479a633f 100644 --- a/app/src/androidTest/java/ac/test/podcini/ui/UITestUtils.kt +++ b/app/src/androidTest/java/ac/test/podcini/ui/UITestUtils.kt @@ -3,11 +3,11 @@ package de.test.podcini.ui import android.content.Context import android.util.Log import androidx.test.platform.app.InstrumentationRegistry -import ac.mdiq.podcini.event.FeedListUpdateEvent -import ac.mdiq.podcini.event.QueueEvent.Companion.setQueue -import ac.mdiq.podcini.model.feed.Feed -import ac.mdiq.podcini.model.feed.FeedItem -import ac.mdiq.podcini.model.feed.FeedMedia +import ac.mdiq.podcini.util.event.FeedListUpdateEvent +import ac.mdiq.podcini.util.event.QueueEvent.Companion.setQueue +import ac.mdiq.podcini.storage.model.feed.Feed +import ac.mdiq.podcini.storage.model.feed.FeedItem +import ac.mdiq.podcini.storage.model.feed.FeedMedia import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.deleteDatabase import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.getInstance import de.test.podcini.util.service.download.HTTPBin @@ -193,7 +193,7 @@ class UITestUtils(private val context: Context) { adapter.setCompleteFeed(*hostedFeeds.toTypedArray()) adapter.setQueue(queue) adapter.close() - EventBus.getDefault().post(FeedListUpdateEvent(hostedFeeds)) + EventBus.getDefault().post(ac.mdiq.podcini.util.event.FeedListUpdateEvent(hostedFeeds)) EventBus.getDefault().post(setQueue(queue)) } diff --git a/app/src/androidTest/java/ac/test/podcini/ui/UITestUtilsTest.kt b/app/src/androidTest/java/ac/test/podcini/ui/UITestUtilsTest.kt index f3e3a0c8..ba2239b5 100644 --- a/app/src/androidTest/java/ac/test/podcini/ui/UITestUtilsTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/ui/UITestUtilsTest.kt @@ -2,7 +2,7 @@ package de.test.podcini.ui import androidx.test.filters.MediumTest import androidx.test.platform.app.InstrumentationRegistry -import ac.mdiq.podcini.model.feed.Feed +import ac.mdiq.podcini.storage.model.feed.Feed import org.junit.After import org.junit.Assert import org.junit.Before @@ -41,7 +41,7 @@ class UITestUtilsTest { for (feed in feeds) { testUrlReachable(feed.download_url) - for (item in feed.items!!) { + for (item in feed.items) { if (item.hasMedia()) { testUrlReachable(item.media!!.download_url) } @@ -68,14 +68,14 @@ class UITestUtilsTest { for (feed in uiTestUtils!!.hostedFeeds) { Assert.assertTrue(feed.id != 0L) - for (item in feed.items!!) { + for (item in feed.items) { Assert.assertTrue(item.id != 0L) if (item.hasMedia()) { Assert.assertTrue(item.media!!.id != 0L) if (downloadEpisodes) { Assert.assertTrue(item.media!!.isDownloaded()) Assert.assertNotNull(item.media!!.getFile_url()) - val file = File(item.media!!.getFile_url()) + val file = File(item.media!!.getFile_url()!!) Assert.assertTrue(file.exists()) } } diff --git a/app/src/androidTest/java/ac/test/podcini/util/event/FeedItemEventListener.kt b/app/src/androidTest/java/ac/test/podcini/util/event/FeedItemEventListener.kt index a0e4d3ba..f5ea50e0 100644 --- a/app/src/androidTest/java/ac/test/podcini/util/event/FeedItemEventListener.kt +++ b/app/src/androidTest/java/ac/test/podcini/util/event/FeedItemEventListener.kt @@ -1,6 +1,6 @@ package de.test.podcini.util.event -import ac.mdiq.podcini.event.FeedItemEvent +import ac.mdiq.podcini.util.event.FeedItemEvent import io.reactivex.functions.Consumer import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -10,14 +10,14 @@ import org.greenrobot.eventbus.Subscribe * */ class FeedItemEventListener { - private val events: MutableList = ArrayList() + private val events: MutableList = ArrayList() @Subscribe - fun onEvent(event: FeedItemEvent) { + fun onEvent(event: ac.mdiq.podcini.util.event.FeedItemEvent) { events.add(event) } - fun getEvents(): List { + fun getEvents(): List { return events } diff --git a/app/src/androidTest/java/ac/test/podcini/util/syndication/feedgenerator/FeedGenerator.kt b/app/src/androidTest/java/ac/test/podcini/util/syndication/feedgenerator/FeedGenerator.kt index 8391488a..687d3502 100644 --- a/app/src/androidTest/java/ac/test/podcini/util/syndication/feedgenerator/FeedGenerator.kt +++ b/app/src/androidTest/java/ac/test/podcini/util/syndication/feedgenerator/FeedGenerator.kt @@ -1,6 +1,6 @@ package de.test.podcini.util.syndication.feedgenerator -import ac.mdiq.podcini.model.feed.Feed +import ac.mdiq.podcini.storage.model.feed.Feed import java.io.IOException import java.io.OutputStream diff --git a/app/src/androidTest/java/ac/test/podcini/util/syndication/feedgenerator/Rss2Generator.kt b/app/src/androidTest/java/ac/test/podcini/util/syndication/feedgenerator/Rss2Generator.kt index 65d8fd54..4318807b 100644 --- a/app/src/androidTest/java/ac/test/podcini/util/syndication/feedgenerator/Rss2Generator.kt +++ b/app/src/androidTest/java/ac/test/podcini/util/syndication/feedgenerator/Rss2Generator.kt @@ -1,9 +1,9 @@ package de.test.podcini.util.syndication.feedgenerator import android.util.Xml -import ac.mdiq.podcini.core.util.DateFormatter.formatRfc822Date -import ac.mdiq.podcini.model.feed.Feed -import ac.mdiq.podcini.parser.feed.namespace.PodcastIndex +import ac.mdiq.podcini.util.DateFormatter.formatRfc822Date +import ac.mdiq.podcini.storage.model.feed.Feed +import ac.mdiq.podcini.feed.parser.namespace.PodcastIndex import de.test.podcini.util.syndication.feedgenerator.GeneratorUtil.addPaymentLink import java.io.IOException import java.io.OutputStream @@ -56,15 +56,15 @@ class Rss2Generator : FeedGenerator { } val fundingList = feed.paymentLinks - if (fundingList != null) { + if (fundingList.isNotEmpty()) { for (funding in fundingList) { addPaymentLink(xml, funding.url, true) } } // Write FeedItem data - if (feed.items != null) { - for (item in feed.items!!) { + if (feed.items.isNotEmpty()) { + for (item in feed.items) { xml.startTag(null, "item") if (item.title != null) { @@ -99,7 +99,7 @@ class Rss2Generator : FeedGenerator { xml.attribute(null, "type", item.media!!.mime_type) xml.endTag(null, "enclosure") } - if (fundingList != null) { + if (fundingList.isNotEmpty()) { for (funding in fundingList) { xml.startTag(PodcastIndex.NSTAG, "funding") xml.attribute(PodcastIndex.NSTAG, "url", funding.url) diff --git a/app/src/free/java/ac/mdiq/podcini/dialog/RatingDialog.kt b/app/src/free/java/ac/mdiq/podcini/dialog/RatingDialog.kt index c6e17800..372025df 100644 --- a/app/src/free/java/ac/mdiq/podcini/dialog/RatingDialog.kt +++ b/app/src/free/java/ac/mdiq/podcini/dialog/RatingDialog.kt @@ -1,4 +1,4 @@ -package ac.mdiq.podcini.dialog +package ac.mdiq.podcini.ui.dialog import android.content.Context import androidx.annotation.VisibleForTesting diff --git a/net/ssl/src/free/java/ac/mdiq/podcini/net/ssl/SslProviderInstaller.kt b/app/src/free/java/ac/mdiq/podcini/net/ssl/SslProviderInstaller.kt similarity index 100% rename from net/ssl/src/free/java/ac/mdiq/podcini/net/ssl/SslProviderInstaller.kt rename to app/src/free/java/ac/mdiq/podcini/net/ssl/SslProviderInstaller.kt diff --git a/playback/cast/src/free/java/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt b/app/src/free/java/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt similarity index 100% rename from playback/cast/src/free/java/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt rename to app/src/free/java/ac/mdiq/podcini/playback/cast/CastEnabledActivity.kt diff --git a/playback/cast/src/free/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt b/app/src/free/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt similarity index 100% rename from playback/cast/src/free/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt rename to app/src/free/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt diff --git a/app/src/free/java/ac/mdiq/podcini/playback/cast/CastStateListener.kt b/app/src/free/java/ac/mdiq/podcini/playback/cast/CastStateListener.kt new file mode 100644 index 00000000..e3972e1a --- /dev/null +++ b/app/src/free/java/ac/mdiq/podcini/playback/cast/CastStateListener.kt @@ -0,0 +1,11 @@ +package ac.mdiq.podcini.playback.cast + +import android.content.Context + +open class CastStateListener(context: Context) { + fun destroy() { + } + + open fun onSessionStartedOrEnded() { + } +} diff --git a/app/src/free/java/ac/mdiq/podcini/service/playback/WearMediaSession.kt b/app/src/free/java/ac/mdiq/podcini/service/playback/WearMediaSession.kt new file mode 100644 index 00000000..4db068f7 --- /dev/null +++ b/app/src/free/java/ac/mdiq/podcini/service/playback/WearMediaSession.kt @@ -0,0 +1,17 @@ +package ac.mdiq.podcini.service.playback + +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.session.PlaybackStateCompat + +internal object WearMediaSession { + /** + * Take a custom action builder and add no extras, because this is not the Play version of the app. + */ + fun addWearExtrasToAction(actionBuilder: PlaybackStateCompat.CustomAction.Builder?) { + // no-op + } + + fun mediaSessionSetExtraForWear(mediaSession: MediaSessionCompat?) { + // no-op + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index feb448d3..116456ac 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,19 @@ xmlns:tools="http://schemas.android.com/tools" android:installLocation="auto"> + + + + + + + + + + + + @@ -32,7 +45,7 @@ android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher" android:label="@string/app_name" - android:backupAgent=".core.backup.OpmlBackupAgent" + android:backupAgent=".storage.backup.OpmlBackupAgent" android:restoreAnyVersion="true" android:theme="@style/Theme.Podcini.Splash" android:usesCleartextTraffic="true" @@ -42,15 +55,51 @@ android:allowAudioPlaybackCapture="true" android:networkSecurityConfig="@xml/network_security_config"> - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -100,7 +149,7 @@ + android:value="ac.mdiq.podcini.ui.activity.MainActivity"/> @@ -157,7 +206,7 @@ @@ -170,7 +219,7 @@ @@ -196,22 +245,22 @@ + android:value="ac.mdiq.podcini.ui.activity.PreferenceActivity"/> + android:value="ac.mdiq.podcini.ui.activity.MainActivity"/> @@ -219,14 +268,14 @@ + android:value="ac.mdiq.podcini.ui.activity.MainActivity"/> @@ -336,7 +385,7 @@ - + + + + + + + diff --git a/core/src/main/assets/html-export-favorites-item-template.html b/app/src/main/assets/html-export-favorites-item-template.html similarity index 100% rename from core/src/main/assets/html-export-favorites-item-template.html rename to app/src/main/assets/html-export-favorites-item-template.html diff --git a/core/src/main/assets/html-export-feed-template.html b/app/src/main/assets/html-export-feed-template.html similarity index 100% rename from core/src/main/assets/html-export-feed-template.html rename to app/src/main/assets/html-export-feed-template.html diff --git a/core/src/main/assets/html-export-template.html b/app/src/main/assets/html-export-template.html similarity index 100% rename from core/src/main/assets/html-export-template.html rename to app/src/main/assets/html-export-template.html diff --git a/core/src/main/assets/shownotes-style.css b/app/src/main/assets/shownotes-style.css similarity index 100% rename from core/src/main/assets/shownotes-style.css rename to app/src/main/assets/shownotes-style.css diff --git a/app/src/main/java/ac/mdiq/podcini/PodciniApp.kt b/app/src/main/java/ac/mdiq/podcini/PodciniApp.kt index b5412054..4e3d5ad2 100644 --- a/app/src/main/java/ac/mdiq/podcini/PodciniApp.kt +++ b/app/src/main/java/ac/mdiq/podcini/PodciniApp.kt @@ -9,15 +9,14 @@ import com.google.android.material.color.DynamicColors import com.joanzapata.iconify.Iconify import com.joanzapata.iconify.fonts.FontAwesomeModule import com.joanzapata.iconify.fonts.MaterialModule -import ac.mdiq.podcini.activity.SplashActivity -import ac.mdiq.podcini.config.ApplicationCallbacksImpl -import ac.mdiq.podcini.core.ApCoreEventBusIndex -import ac.mdiq.podcini.core.ClientConfig -import ac.mdiq.podcini.core.ClientConfigurator -import ac.mdiq.podcini.error.CrashReportWriter -import ac.mdiq.podcini.error.RxJavaErrorHandlerSetup +import ac.mdiq.podcini.ui.activity.SplashActivity +import ac.mdiq.podcini.util.config.ApplicationCallbacksImpl +import ac.mdiq.podcini.util.config.ClientConfig +import ac.mdiq.podcini.util.config.ClientConfigurator +import ac.mdiq.podcini.util.error.CrashReportWriter +import ac.mdiq.podcini.util.error.RxJavaErrorHandlerSetup import ac.mdiq.podcini.preferences.PreferenceUpgrader -import ac.mdiq.podcini.spa.SPAUtil +import ac.mdiq.podcini.util.SPAUtil import org.greenrobot.eventbus.EventBus /** Main application class. */ @@ -26,7 +25,8 @@ class PodciniApp : Application() { override fun onCreate() { super.onCreate() ClientConfig.USER_AGENT = "Podcini/" + BuildConfig.VERSION_NAME - ClientConfig.applicationCallbacks = ApplicationCallbacksImpl() + ClientConfig.applicationCallbacks = + ApplicationCallbacksImpl() Thread.setDefaultUncaughtExceptionHandler(CrashReportWriter()) RxJavaErrorHandlerSetup.setupRxJavaErrorHandler() @@ -53,7 +53,7 @@ class PodciniApp : Application() { SPAUtil.sendSPAppsQueryFeedsIntent(this) EventBus.builder() .addIndex(ApEventBusIndex()) - .addIndex(ApCoreEventBusIndex()) +// .addIndex(ApCoreEventBusIndex()) .logNoSubscriberMessages(false) .sendNoSubscriberEvent(false) .installDefaultEventBus() diff --git a/app/src/main/java/ac/mdiq/podcini/activity/BugReportActivity.kt b/app/src/main/java/ac/mdiq/podcini/activity/BugReportActivity.kt deleted file mode 100644 index 9826da00..00000000 --- a/app/src/main/java/ac/mdiq/podcini/activity/BugReportActivity.kt +++ /dev/null @@ -1,125 +0,0 @@ -package ac.mdiq.podcini.activity - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.DialogInterface -import android.os.Build -import android.os.Bundle -import android.util.Log -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.widget.TextView -import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ShareCompat.IntentBuilder -import androidx.core.content.FileProvider -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.preferences.ThemeSwitcher.getTheme -import ac.mdiq.podcini.core.util.IntentUtils.openInBrowser -import ac.mdiq.podcini.error.CrashReportWriter -import ac.mdiq.podcini.storage.preferences.UserPreferences.getDataFolder -import org.apache.commons.io.IOUtils -import java.io.File -import java.io.FileInputStream -import java.io.IOException -import java.nio.charset.Charset - -/** - * Displays the 'crash report' screen - */ -class BugReportActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - setTheme(getTheme(this)) - super.onCreate(savedInstanceState) - supportActionBar!!.setDisplayShowHomeEnabled(true) - setContentView(R.layout.bug_report) - - var stacktrace = "No crash report recorded" - try { - val crashFile = CrashReportWriter.file - if (crashFile.exists()) { - stacktrace = IOUtils.toString(FileInputStream(crashFile), Charset.forName("UTF-8")) - } else { - Log.d(TAG, stacktrace) - } - } catch (e: IOException) { - e.printStackTrace() - } - - val crashDetailsTextView = findViewById(R.id.crash_report_logs) - crashDetailsTextView.text = """ - ${CrashReportWriter.systemInfo} - - $stacktrace - """.trimIndent() - - findViewById(R.id.btn_open_bug_tracker).setOnClickListener { v: View? -> - openInBrowser( - this@BugReportActivity, "https://github.com/XilinJia/Podcini/issues") - } - - findViewById(R.id.btn_copy_log).setOnClickListener { v: View? -> - val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText(getString(R.string.bug_report_title), crashDetailsTextView.text) - clipboard.setPrimaryClip(clip) - if (Build.VERSION.SDK_INT < 32) { - Snackbar.make(findViewById(android.R.id.content), R.string.copied_to_clipboard, - Snackbar.LENGTH_SHORT).show() - } - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.bug_report_options, menu) - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == R.id.export_logcat) { - val alertBuilder = MaterialAlertDialogBuilder(this) - alertBuilder.setMessage(R.string.confirm_export_log_dialog_message) - alertBuilder.setPositiveButton(R.string.confirm_label) { dialog: DialogInterface, which: Int -> - exportLog() - dialog.dismiss() - } - alertBuilder.setNegativeButton(R.string.cancel_label, null) - alertBuilder.show() - return true - } - return super.onOptionsItemSelected(item) - } - - private fun exportLog() { - try { - val filename = File(getDataFolder(null), "full-logs.txt") - val cmd = "logcat -d -f " + filename.absolutePath - Runtime.getRuntime().exec(cmd) - //share file - try { - val authority = getString(R.string.provider_authority) - val fileUri = FileProvider.getUriForFile(this, authority, filename) - - IntentBuilder(this) - .setType("text/*") - .addStream(fileUri) - .setChooserTitle(R.string.share_file_label) - .startChooser() - } catch (e: Exception) { - e.printStackTrace() - val strResId = R.string.log_file_share_exception - Snackbar.make(findViewById(android.R.id.content), strResId, Snackbar.LENGTH_LONG) - .show() - } - } catch (e: IOException) { - e.printStackTrace() - Snackbar.make(findViewById(android.R.id.content), e.message!!, Snackbar.LENGTH_LONG).show() - } - } - - - companion object { - private const val TAG = "BugReportActivity" - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/activity/MainActivity.kt b/app/src/main/java/ac/mdiq/podcini/activity/MainActivity.kt deleted file mode 100644 index d6ebe7ea..00000000 --- a/app/src/main/java/ac/mdiq/podcini/activity/MainActivity.kt +++ /dev/null @@ -1,682 +0,0 @@ -package ac.mdiq.podcini.activity - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.content.res.Configuration -import android.media.AudioManager -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.util.DisplayMetrics -import android.util.Log -import android.view.KeyEvent -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup.MarginLayoutParams -import android.widget.EditText -import androidx.appcompat.app.ActionBarDrawerToggle -import androidx.core.graphics.Insets -import androidx.core.view.ViewCompat -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsCompat -import androidx.drawerlayout.widget.DrawerLayout -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentContainerView -import androidx.media3.common.util.UnstableApi -import androidx.recyclerview.widget.RecyclerView -import androidx.work.WorkInfo -import androidx.work.WorkManager -import com.bumptech.glide.Glide -import com.google.android.material.appbar.MaterialToolbar -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback -import com.google.android.material.snackbar.Snackbar -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.preferences.ThemeSwitcher.getNoTitleTheme -import ac.mdiq.podcini.core.receiver.MediaButtonReceiver.Companion.createIntent -import ac.mdiq.podcini.core.storage.DBReader -import ac.mdiq.podcini.core.sync.queue.SynchronizationQueueSink -import ac.mdiq.podcini.core.util.download.FeedUpdateManager -import ac.mdiq.podcini.core.util.download.FeedUpdateManager.restartUpdateAlarm -import ac.mdiq.podcini.core.util.download.FeedUpdateManager.runOnceOrAsk -import ac.mdiq.podcini.dialog.RatingDialog -import ac.mdiq.podcini.event.EpisodeDownloadEvent -import ac.mdiq.podcini.event.FeedUpdateRunningEvent -import ac.mdiq.podcini.event.MessageEvent -import ac.mdiq.podcini.fragment.* -import ac.mdiq.podcini.model.download.DownloadStatus -import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface -import ac.mdiq.podcini.playback.cast.CastEnabledActivity -import ac.mdiq.podcini.storage.preferences.UserPreferences -import ac.mdiq.podcini.storage.preferences.UserPreferences.backButtonOpensDrawer -import ac.mdiq.podcini.storage.preferences.UserPreferences.defaultPage -import ac.mdiq.podcini.storage.preferences.UserPreferences.hiddenDrawerItems -import ac.mdiq.podcini.ui.appstartintent.MainActivityStarter -import ac.mdiq.podcini.ui.common.ThemeUtils.getDrawableFromAttr -import ac.mdiq.podcini.ui.home.HomeFragment -import ac.mdiq.podcini.view.LockableBottomSheetBehavior -import org.apache.commons.lang3.ArrayUtils -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import kotlin.math.min - -/** - * The activity that is shown when the user launches the app. - */ -@UnstableApi -class MainActivity : CastEnabledActivity() { -// some device doesn't have a drawer - private var drawerLayout: DrawerLayout? = null - - private lateinit var navDrawer: View - private lateinit var dummyView : View - lateinit var bottomSheet: LockableBottomSheetBehavior<*> - private set - - private var drawerToggle: ActionBarDrawerToggle? = null - - @JvmField - val recycledViewPool: RecyclerView.RecycledViewPool = RecyclerView.RecycledViewPool() - private var lastTheme = 0 - private var navigationBarInsets = Insets.NONE - - @UnstableApi public override fun onCreate(savedInstanceState: Bundle?) { - lastTheme = getNoTitleTheme(this) - setTheme(lastTheme) - - DBReader.updateFeedList() - - if (savedInstanceState != null) { - ensureGeneratedViewIdGreaterThan(savedInstanceState.getInt(KEY_GENERATED_VIEW_ID, 0)) - } - WindowCompat.setDecorFitsSystemWindows(window, false) - super.onCreate(savedInstanceState) - setContentView(R.layout.main) - recycledViewPool.setMaxRecycledViews(R.id.view_type_episode_item, 25) - - dummyView = object : View(this) {} - - drawerLayout = findViewById(R.id.drawer_layout) - navDrawer = findViewById(R.id.navDrawerFragment) - setNavDrawerSize() - - // Consume navigation bar insets - we apply them in setPlayerVisible() - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main_view)) { v: View?, insets: WindowInsetsCompat -> - navigationBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) - updateInsets() - WindowInsetsCompat.Builder(insets) - .setInsets(WindowInsetsCompat.Type.navigationBars(), Insets.NONE) - .build() - } - - val fm = supportFragmentManager - if (fm.findFragmentByTag(MAIN_FRAGMENT_TAG) == null) { - if (UserPreferences.DEFAULT_PAGE_REMEMBER != defaultPage) { - loadFragment(defaultPage, null) - } else { - val lastFragment = NavDrawerFragment.getLastNavFragment(this) - if (ArrayUtils.contains(NavDrawerFragment.NAV_DRAWER_TAGS, lastFragment)) { - loadFragment(lastFragment, null) - } else { - try { - loadFeedFragmentById(lastFragment.toInt().toLong(), null) - } catch (e: NumberFormatException) { - // it's not a number, this happens if we removed - // a label from the NAV_DRAWER_TAGS - // give them a nice default... - loadFragment(HomeFragment.TAG, null) - } - } - } - } - - val transaction = fm.beginTransaction() - val navDrawerFragment = NavDrawerFragment() - transaction.replace(R.id.navDrawerFragment, navDrawerFragment, NavDrawerFragment.TAG) - val audioPlayerFragment = AudioPlayerFragment() - transaction.replace(R.id.audioplayerFragment, audioPlayerFragment, AudioPlayerFragment.TAG) - transaction.commit() - - checkFirstLaunch() - val bottomSheet = findViewById(R.id.audioplayerFragment) - this.bottomSheet = BottomSheetBehavior.from(bottomSheet) as LockableBottomSheetBehavior<*> - this.bottomSheet.isHideable = false - this.bottomSheet.setBottomSheetCallback(bottomSheetCallback) - - restartUpdateAlarm(this, false) - SynchronizationQueueSink.syncNowIfNotSyncedRecently() - - WorkManager.getInstance(this) - .getWorkInfosByTagLiveData(FeedUpdateManager.WORK_TAG_FEED_UPDATE) - .observe(this) { workInfos: List -> - var isRefreshingFeeds = false - for (workInfo in workInfos) { - if (workInfo.state == WorkInfo.State.RUNNING) { - isRefreshingFeeds = true - } else if (workInfo.state == WorkInfo.State.ENQUEUED) { - isRefreshingFeeds = true - } - } - EventBus.getDefault().postSticky(FeedUpdateRunningEvent(isRefreshingFeeds)) - } - WorkManager.getInstance(this) - .getWorkInfosByTagLiveData(DownloadServiceInterface.WORK_TAG) - .observe(this) { workInfos: List -> - val updatedEpisodes: MutableMap = HashMap() - for (workInfo in workInfos) { - var downloadUrl: String? = null - for (tag in workInfo.tags) { - if (tag.startsWith(DownloadServiceInterface.WORK_TAG_EPISODE_URL)) { - downloadUrl = tag.substring(DownloadServiceInterface.WORK_TAG_EPISODE_URL.length) - } - } - if (downloadUrl == null) { - continue - } - var status: Int - status = when (workInfo.state) { - WorkInfo.State.RUNNING -> { - DownloadStatus.STATE_RUNNING - } - WorkInfo.State.ENQUEUED, WorkInfo.State.BLOCKED -> { - DownloadStatus.STATE_QUEUED - } - else -> { - DownloadStatus.STATE_COMPLETED - } - } - var progress = workInfo.progress.getInt(DownloadServiceInterface.WORK_DATA_PROGRESS, -1) - if (progress == -1 && status != DownloadStatus.STATE_COMPLETED) { - status = DownloadStatus.STATE_QUEUED - progress = 0 - } - updatedEpisodes[downloadUrl] = DownloadStatus(status, progress) - } - DownloadServiceInterface.get()?.setCurrentDownloads(updatedEpisodes) - EventBus.getDefault().postSticky(EpisodeDownloadEvent(updatedEpisodes)) - } - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - updateInsets() - } - - /** - * View.generateViewId stores the current ID in a static variable. - * When the process is killed, the variable gets reset. - * This makes sure that we do not get ID collisions - * and therefore errors when trying to restore state from another view. - */ - private fun ensureGeneratedViewIdGreaterThan(minimum: Int) { - while (View.generateViewId() <= minimum) { - // Generate new IDs - } - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putInt(KEY_GENERATED_VIEW_ID, View.generateViewId()) - } - - private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() { - override fun onStateChanged(view: View, state: Int) { - if (state == BottomSheetBehavior.STATE_COLLAPSED) { - onSlide(view,0.0f) - } else if (state == BottomSheetBehavior.STATE_EXPANDED) { - onSlide(view, 1.0f) - } - } - override fun onSlide(view: View, slideOffset: Float) { - val audioPlayer = supportFragmentManager - .findFragmentByTag(AudioPlayerFragment.TAG) as AudioPlayerFragment? - if (audioPlayer == null) { - return - } - - if (slideOffset == 0.0f) { //STATE_COLLAPSED - audioPlayer.scrollToPage(AudioPlayerFragment.POS_COVER) - } - - audioPlayer.fadePlayerToToolbar(slideOffset) - } - } - - fun setupToolbarToggle(toolbar: MaterialToolbar, displayUpArrow: Boolean) { - if (drawerLayout != null) { // Tablet layout does not have a drawer - if (drawerToggle != null) { - drawerLayout!!.removeDrawerListener(drawerToggle!!) - } - drawerToggle = ActionBarDrawerToggle(this, drawerLayout, toolbar, - R.string.drawer_open, R.string.drawer_close) - drawerLayout!!.addDrawerListener(drawerToggle!!) - drawerToggle!!.syncState() - drawerToggle!!.isDrawerIndicatorEnabled = !displayUpArrow - drawerToggle!!.toolbarNavigationClickListener = View.OnClickListener { v: View? -> supportFragmentManager.popBackStack() } - } else if (!displayUpArrow) { - toolbar.navigationIcon = null - } else { - toolbar.setNavigationIcon(getDrawableFromAttr(this, R.attr.homeAsUpIndicator)) - toolbar.setNavigationOnClickListener { v: View? -> supportFragmentManager.popBackStack() } - } - } - - override fun onDestroy() { - super.onDestroy() - drawerLayout?.removeDrawerListener(drawerToggle!!) - } - - private fun checkFirstLaunch() { - val prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE) - if (prefs.getBoolean(PREF_IS_FIRST_LAUNCH, true)) { - restartUpdateAlarm(this, true) - - val edit = prefs.edit() - edit.putBoolean(PREF_IS_FIRST_LAUNCH, false) - edit.apply() - } - } - - val isDrawerOpen: Boolean - get() = drawerLayout?.isDrawerOpen(navDrawer)?:false - - private fun updateInsets() { - setPlayerVisible(findViewById(R.id.audioplayerFragment).visibility == View.VISIBLE) - val playerHeight = resources.getDimension(R.dimen.external_player_height).toInt() - bottomSheet.peekHeight = playerHeight + navigationBarInsets.bottom - } - - fun setPlayerVisible(visible: Boolean) { - bottomSheet.setLocked(!visible) - if (visible) { - bottomSheetCallback.onStateChanged(dummyView, bottomSheet.state) // Update toolbar visibility - } else { - bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) - } - val mainView = findViewById(R.id.main_view) - val params = mainView.layoutParams as MarginLayoutParams - val externalPlayerHeight = resources.getDimension(R.dimen.external_player_height).toInt() - params.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, - navigationBarInsets.bottom + (if (visible) externalPlayerHeight else 0)) - mainView.layoutParams = params - val playerView = findViewById(R.id.playerFragment) - val playerParams = playerView.layoutParams as MarginLayoutParams - playerParams.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, 0) - playerView.layoutParams = playerParams - findViewById(R.id.audioplayerFragment).visibility = if (visible) View.VISIBLE else View.GONE - } - - fun loadFragment(tag: String?, args: Bundle?) { - var tag = tag - var args = args - Log.d(TAG, "loadFragment(tag: $tag, args: $args)") - val fragment: Fragment - when (tag) { - HomeFragment.TAG -> fragment = HomeFragment() - QueueFragment.TAG -> fragment = QueueFragment() - InboxFragment.TAG -> fragment = InboxFragment() - AllEpisodesFragment.TAG -> fragment = AllEpisodesFragment() - CompletedDownloadsFragment.TAG -> fragment = CompletedDownloadsFragment() - PlaybackHistoryFragment.TAG -> fragment = PlaybackHistoryFragment() - AddFeedFragment.TAG -> fragment = AddFeedFragment() - SubscriptionFragment.TAG -> fragment = SubscriptionFragment() - else -> { - // default to home screen - fragment = HomeFragment() - tag = HomeFragment.TAG - args = null - } - } - if (args != null) { - fragment.arguments = args - } - NavDrawerFragment.saveLastNavFragment(this, tag) - loadFragment(fragment) - } - - fun loadFeedFragmentById(feedId: Long, args: Bundle?) { - val fragment: Fragment = FeedItemlistFragment.newInstance(feedId) - if (args != null) { - fragment.arguments = args - } - NavDrawerFragment.saveLastNavFragment(this, feedId.toString()) - loadFragment(fragment) - } - - private fun loadFragment(fragment: Fragment) { - val fragmentManager = supportFragmentManager - // clear back stack - for (i in 0 until fragmentManager.backStackEntryCount) { - fragmentManager.popBackStack() - } - val t = fragmentManager.beginTransaction() - t.replace(R.id.main_view, fragment, MAIN_FRAGMENT_TAG) - fragmentManager.popBackStack() - // TODO: we have to allow state loss here - // since this function can get called from an AsyncTask which - // could be finishing after our app has already committed state - // and is about to get shutdown. What we *should* do is - // not commit anything in an AsyncTask, but that's a bigger - // change than we want now. - t.commitAllowingStateLoss() - - // Tablet layout does not have a drawer - drawerLayout?.closeDrawer(navDrawer) - } - - @JvmOverloads - fun loadChildFragment(fragment: Fragment, transition: TransitionEffect? = TransitionEffect.NONE) { - val transaction = supportFragmentManager.beginTransaction() - - when (transition) { - TransitionEffect.FADE -> transaction.setCustomAnimations(R.anim.fade_in, R.anim.fade_out) - TransitionEffect.SLIDE -> transaction.setCustomAnimations( - R.anim.slide_right_in, - R.anim.slide_left_out, - R.anim.slide_left_in, - R.anim.slide_right_out) - TransitionEffect.NONE -> {} - null -> {} - } - transaction - .hide(supportFragmentManager.findFragmentByTag(MAIN_FRAGMENT_TAG)!!) - .add(R.id.main_view, fragment, MAIN_FRAGMENT_TAG) - .addToBackStack(null) - .commit() - } - - override fun onPostCreate(savedInstanceState: Bundle?) { - super.onPostCreate(savedInstanceState) - if (drawerToggle != null) { // Tablet layout does not have a drawer - drawerToggle!!.syncState() - } - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - if (drawerToggle != null) { // Tablet layout does not have a drawer - drawerToggle!!.onConfigurationChanged(newConfig) - } - setNavDrawerSize() - } - - private fun setNavDrawerSize() { - if (drawerToggle == null) { // Tablet layout does not have a drawer - return - } - val screenPercent = resources.getInteger(R.integer.nav_drawer_screen_size_percent) * 0.01f - val width = (screenWidth * screenPercent).toInt() - val maxWidth = resources.getDimension(R.dimen.nav_drawer_max_screen_size).toInt() - - navDrawer.layoutParams.width = min(width.toDouble(), maxWidth.toDouble()).toInt() - } - - private val screenWidth: Int - get() { - val displayMetrics = DisplayMetrics() - windowManager.defaultDisplay.getMetrics(displayMetrics) - return displayMetrics.widthPixels - } - - override fun onRestoreInstanceState(savedInstanceState: Bundle) { - super.onRestoreInstanceState(savedInstanceState) - - if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) { - bottomSheetCallback.onSlide(dummyView, 1.0f) - } - } - - public override fun onStart() { - super.onStart() - EventBus.getDefault().register(this) - RatingDialog.init(this) - } - - override fun onResume() { - super.onResume() - handleNavIntent() - RatingDialog.check() - - if (lastTheme != getNoTitleTheme(this)) { - finish() - startActivity(Intent(this, MainActivity::class.java)) - } - if (hiddenDrawerItems!!.contains(NavDrawerFragment.getLastNavFragment(this))) { - loadFragment(defaultPage, null) - } - } - - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - lastTheme = getNoTitleTheme(this) // Don't recreate activity when a result is pending - } - - override fun onStop() { - super.onStop() - EventBus.getDefault().unregister(this) - } - - override fun onTrimMemory(level: Int) { - super.onTrimMemory(level) - Glide.get(this).trimMemory(level) - } - - override fun onLowMemory() { - super.onLowMemory() - Glide.get(this).clearMemory() - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (drawerToggle != null && drawerToggle!!.onOptionsItemSelected(item)) { // Tablet layout does not have a drawer - return true - } else if (item.itemId == android.R.id.home) { - if (supportFragmentManager.backStackEntryCount > 0) { - supportFragmentManager.popBackStack() - } - return true - } else { - return super.onOptionsItemSelected(item) - } - } - - @Deprecated("Deprecated in Java") - override fun onBackPressed() { - if (isDrawerOpen) { - drawerLayout?.closeDrawer(navDrawer) - } else if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) { - bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) - } else if (supportFragmentManager.backStackEntryCount != 0) { - super.onBackPressed() - } else { - val toPage = defaultPage - if (NavDrawerFragment.getLastNavFragment(this) == toPage || UserPreferences.DEFAULT_PAGE_REMEMBER == toPage) { - if (backButtonOpensDrawer()) { - drawerLayout?.openDrawer(navDrawer) - } else { - super.onBackPressed() - } - } else { - loadFragment(toPage, null) - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: MessageEvent) { - Log.d(TAG, "onEvent($event)") - - val snackbar = showSnackbarAbovePlayer(event.message, Snackbar.LENGTH_LONG) - if (event.action != null) { - snackbar.setAction(event.actionText) { v: View? -> event.action!!.accept(this) } - } - } - - private fun handleNavIntent() { - Log.d(TAG, "handleNavIntent()") - val intent = intent - if (intent.hasExtra(EXTRA_FEED_ID)) { - val feedId = intent.getLongExtra(EXTRA_FEED_ID, 0) - val args = intent.getBundleExtra(MainActivityStarter.EXTRA_FRAGMENT_ARGS) - if (feedId > 0) { - val startedFromSearch = intent.getBooleanExtra(EXTRA_STARTED_FROM_SEARCH, false) - val addToBackStack = intent.getBooleanExtra(EXTRA_ADD_TO_BACK_STACK, false) - if (startedFromSearch || addToBackStack) { - loadChildFragment(FeedItemlistFragment.newInstance(feedId)) - } else { - loadFeedFragmentById(feedId, args) - } - } - bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) - } else if (intent.hasExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG)) { - val tag = intent.getStringExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG) - val args = intent.getBundleExtra(MainActivityStarter.EXTRA_FRAGMENT_ARGS) - if (tag != null) { - loadFragment(tag, args) - } - bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) - } else if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, false)) { - bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED - bottomSheetCallback.onSlide(dummyView, 1.0f) - } else { - handleDeeplink(intent.data) - } - - if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DRAWER, false)) { - drawerLayout?.open() - } - if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DOWNLOAD_LOGS, false)) { - DownloadLogFragment().show(supportFragmentManager, null) - } - if (intent.getBooleanExtra(EXTRA_REFRESH_ON_START, false)) { - runOnceOrAsk(this) - } - // to avoid handling the intent twice when the configuration changes - setIntent(Intent(this@MainActivity, MainActivity::class.java)) - } - - @SuppressLint("MissingSuperCall") - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - setIntent(intent) - handleNavIntent() - } - - fun showSnackbarAbovePlayer(text: CharSequence?, duration: Int): Snackbar { - val s: Snackbar - if (bottomSheet.state == BottomSheetBehavior.STATE_COLLAPSED) { - s = Snackbar.make(findViewById(R.id.main_view), text!!, duration) - if (findViewById(R.id.audioplayerFragment).visibility == View.VISIBLE) { - s.setAnchorView(findViewById(R.id.audioplayerFragment)) - } - } else { - s = Snackbar.make(findViewById(android.R.id.content), text!!, duration) - } - s.show() - return s - } - - fun showSnackbarAbovePlayer(text: Int, duration: Int): Snackbar { - return showSnackbarAbovePlayer(resources.getText(text), duration) - } - - /** - * Handles the deep link incoming via App Actions. - * Performs an in-app search or opens the relevant feature of the app - * depending on the query. - * - * @param uri incoming deep link - */ - private fun handleDeeplink(uri: Uri?) { - if (uri == null || uri.path == null) { - return - } - Log.d(TAG, "Handling deeplink: $uri") - when (uri.path) { - "/deeplink/search" -> { - val query = uri.getQueryParameter("query") ?: return - - this.loadChildFragment(SearchFragment.newInstance(query)) - } - "/deeplink/main" -> { - val feature = uri.getQueryParameter("page") ?: return - when (feature) { - "DOWNLOADS" -> loadFragment(CompletedDownloadsFragment.TAG, null) - "HISTORY" -> loadFragment(PlaybackHistoryFragment.TAG, null) - "EPISODES" -> loadFragment(AllEpisodesFragment.TAG, null) - "QUEUE" -> loadFragment(QueueFragment.TAG, null) - "SUBSCRIPTIONS" -> loadFragment(SubscriptionFragment.TAG, null) - else -> { - showSnackbarAbovePlayer(getString(R.string.app_action_not_found)+feature, Snackbar.LENGTH_LONG) - return - } - } - } - else -> {} - } - } - - //Hardware keyboard support - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { - val currentFocus = currentFocus - if (currentFocus is EditText) { - return super.onKeyUp(keyCode, event) - } - - val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager - var customKeyCode: Int? = null - EventBus.getDefault().post(event) - - when (keyCode) { - KeyEvent.KEYCODE_P -> customKeyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE - KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_COMMA -> customKeyCode = - KeyEvent.KEYCODE_MEDIA_REWIND - KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_PERIOD -> customKeyCode = - KeyEvent.KEYCODE_MEDIA_FAST_FORWARD - KeyEvent.KEYCODE_PLUS, KeyEvent.KEYCODE_W -> { - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI) - return true - } - KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_S -> { - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI) - return true - } - KeyEvent.KEYCODE_M -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_TOGGLE_MUTE, AudioManager.FLAG_SHOW_UI) - return true - } - } - if (customKeyCode != null) { - sendBroadcast(createIntent(this, customKeyCode)) - return true - } - return super.onKeyUp(keyCode, event) - } - - companion object { - private const val TAG = "MainActivity" - const val MAIN_FRAGMENT_TAG: String = "main" - - const val PREF_NAME: String = "MainActivityPrefs" - const val PREF_IS_FIRST_LAUNCH: String = "prefMainActivityIsFirstLaunch" - - const val EXTRA_FEED_ID: String = "fragment_feed_id" - const val EXTRA_REFRESH_ON_START: String = "refresh_on_start" - const val EXTRA_STARTED_FROM_SEARCH: String = "started_from_search" - const val EXTRA_ADD_TO_BACK_STACK: String = "add_to_back_stack" - const val KEY_GENERATED_VIEW_ID: String = "generated_view_id" - - @JvmStatic - fun getIntentToOpenFeed(context: Context, feedId: Long): Intent { - val intent = Intent(context.applicationContext, MainActivity::class.java) - intent.putExtra(EXTRA_FEED_ID, feedId) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - return intent - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/activity/OnlineFeedViewActivity.kt b/app/src/main/java/ac/mdiq/podcini/activity/OnlineFeedViewActivity.kt deleted file mode 100644 index 74a3c19f..00000000 --- a/app/src/main/java/ac/mdiq/podcini/activity/OnlineFeedViewActivity.kt +++ /dev/null @@ -1,706 +0,0 @@ -package ac.mdiq.podcini.activity - -import android.app.Dialog -import android.content.Context -import android.content.DialogInterface -import android.content.Intent -import android.graphics.LightingColorFilter -import android.os.Bundle -import android.text.Spannable -import android.text.SpannableString -import android.text.TextUtils -import android.text.style.ForegroundColorSpan -import android.util.Log -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.Toast -import androidx.annotation.UiThread -import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.NavUtils -import androidx.media3.common.util.UnstableApi -import com.bumptech.glide.Glide -import com.bumptech.glide.request.RequestOptions -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar -import ac.mdiq.podcini.R -import ac.mdiq.podcini.adapter.FeedItemlistDescriptionAdapter -import ac.mdiq.podcini.core.feed.FeedUrlNotFoundException -import ac.mdiq.podcini.core.preferences.PlaybackPreferences.Companion.currentlyPlayingMediaType -import ac.mdiq.podcini.core.preferences.PlaybackPreferences.Companion.writeNoMediaPlaying -import ac.mdiq.podcini.core.preferences.ThemeSwitcher.getTranslucentTheme -import ac.mdiq.podcini.core.service.download.DownloadRequestCreator.create -import ac.mdiq.podcini.core.service.download.Downloader -import ac.mdiq.podcini.core.service.download.HttpDownloader -import ac.mdiq.podcini.core.service.playback.PlaybackServiceInterface -import ac.mdiq.podcini.core.storage.DBReader -import ac.mdiq.podcini.core.storage.DBTasks -import ac.mdiq.podcini.core.storage.DBWriter -import ac.mdiq.podcini.core.util.DownloadErrorLabel.from -import ac.mdiq.podcini.core.util.IntentUtils.sendLocalBroadcast -import ac.mdiq.podcini.core.util.syndication.FeedDiscoverer -import ac.mdiq.podcini.core.util.syndication.HtmlToPlainText -import ac.mdiq.podcini.databinding.EditTextDialogBinding -import ac.mdiq.podcini.databinding.OnlinefeedviewActivityBinding -import ac.mdiq.podcini.databinding.OnlinefeedviewHeaderBinding -import ac.mdiq.podcini.dialog.AuthenticationDialog -import ac.mdiq.podcini.event.EpisodeDownloadEvent -import ac.mdiq.podcini.event.FeedListUpdateEvent -import ac.mdiq.podcini.event.PlayerStatusEvent -import ac.mdiq.podcini.model.download.DownloadError -import ac.mdiq.podcini.model.download.DownloadResult -import ac.mdiq.podcini.model.feed.Feed -import ac.mdiq.podcini.model.playback.RemoteMedia -import ac.mdiq.podcini.net.common.UrlChecker.prepareUrl -import ac.mdiq.podcini.net.discovery.CombinedSearcher -import ac.mdiq.podcini.net.discovery.PodcastSearcherRegistry -import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface -import ac.mdiq.podcini.parser.feed.FeedHandler -import ac.mdiq.podcini.parser.feed.FeedHandlerResult -import ac.mdiq.podcini.parser.feed.UnsupportedFeedtypeException -import ac.mdiq.podcini.storage.preferences.UserPreferences.isEnableAutodownload -import ac.mdiq.podcini.ui.common.ThemeUtils.getColorFromAttr -import ac.mdiq.podcini.ui.glide.FastBlurTransformation -import io.reactivex.Maybe -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.observers.DisposableMaybeObserver -import io.reactivex.schedulers.Schedulers -import org.apache.commons.lang3.StringUtils -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import java.io.File -import java.io.IOException -import kotlin.concurrent.Volatile - -/** - * Downloads a feed from a feed URL and parses it. Subclasses can display the - * feed object that was parsed. This activity MUST be started with a given URL - * or an Exception will be thrown. - * - * - * If the feed cannot be downloaded or parsed, an error dialog will be displayed - * and the activity will finish as soon as the error dialog is closed. - */ -class OnlineFeedViewActivity : AppCompatActivity() { - @Volatile - private var feeds: List? = null - private var selectedDownloadUrl: String? = null - private var downloader: Downloader? = null - private var username: String? = null - private var password: String? = null - - private var isPaused = false - private var didPressSubscribe = false - private var isFeedFoundBySearch = false - - private var dialog: Dialog? = null - - private var download: Disposable? = null - private var parser: Disposable? = null - private var updater: Disposable? = null - - private lateinit var headerBinding: OnlinefeedviewHeaderBinding - private lateinit var viewBinding: OnlinefeedviewActivityBinding - - override fun onCreate(savedInstanceState: Bundle?) { - setTheme(getTranslucentTheme(this)) - super.onCreate(savedInstanceState) - - viewBinding = OnlinefeedviewActivityBinding.inflate(layoutInflater) - setContentView(viewBinding.root) - - viewBinding.transparentBackground.setOnClickListener { v: View? -> finish() } - viewBinding.closeButton.setOnClickListener { view: View? -> finish() } - viewBinding.card.setOnClickListener(null) - viewBinding.card.setCardBackgroundColor(getColorFromAttr(this, R.attr.colorSurface)) - headerBinding = OnlinefeedviewHeaderBinding.inflate(layoutInflater) - - var feedUrl: String? = null - if (intent.hasExtra(ARG_FEEDURL)) { - feedUrl = intent.getStringExtra(ARG_FEEDURL) - } else if (TextUtils.equals(intent.action, Intent.ACTION_SEND)) { - feedUrl = intent.getStringExtra(Intent.EXTRA_TEXT) - } else if (TextUtils.equals(intent.action, Intent.ACTION_VIEW)) { - feedUrl = intent.dataString - } - - if (feedUrl == null) { - Log.e(TAG, "feedUrl is null.") - showNoPodcastFoundError() - } else { - Log.d(TAG, "Activity was started with url $feedUrl") - setLoadingLayout() - // Remove subscribeonandroid.com from feed URL in order to subscribe to the actual feed URL - if (feedUrl.contains("subscribeonandroid.com")) { - feedUrl = feedUrl.replaceFirst("((www.)?(subscribeonandroid.com/))".toRegex(), "") - } - if (savedInstanceState != null) { - username = savedInstanceState.getString("username") - password = savedInstanceState.getString("password") - } - lookupUrlAndDownload(feedUrl) - } - } - - private fun showNoPodcastFoundError() { - runOnUiThread { - MaterialAlertDialogBuilder(this@OnlineFeedViewActivity) - .setNeutralButton(android.R.string.ok) { dialog: DialogInterface?, which: Int -> finish() } - .setTitle(R.string.error_label) - .setMessage(R.string.null_value_podcast_error) - .setOnDismissListener { dialog1: DialogInterface? -> - setResult(RESULT_ERROR) - finish() - } - .show() - } - } - - /** - * Displays a progress indicator. - */ - private fun setLoadingLayout() { - viewBinding.progressBar.visibility = View.VISIBLE - viewBinding.feedDisplayContainer.visibility = View.GONE - } - - override fun onStart() { - super.onStart() - isPaused = false - EventBus.getDefault().register(this) - } - - override fun onStop() { - super.onStop() - isPaused = true - EventBus.getDefault().unregister(this) - if (downloader != null && !downloader!!.isFinished) { - downloader!!.cancel() - } - if (dialog != null && dialog!!.isShowing) { - dialog!!.dismiss() - } - } - - public override fun onDestroy() { - super.onDestroy() - updater?.dispose() - download?.dispose() - parser?.dispose() - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putString("username", username) - outState.putString("password", password) - } - - private fun resetIntent(url: String) { - val intent = Intent() - intent.putExtra(ARG_FEEDURL, url) - setIntent(intent) - } - - override fun finish() { - super.finish() - overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) - } - - @UnstableApi override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - val destIntent = Intent(this, MainActivity::class.java) - if (NavUtils.shouldUpRecreateTask(this, destIntent)) { - startActivity(destIntent) - } else { - NavUtils.navigateUpFromSameTask(this) - } - return true - } - return super.onOptionsItemSelected(item) - } - - private fun lookupUrlAndDownload(url: String) { - download = PodcastSearcherRegistry.lookupUrl(url) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(Schedulers.io()) - ?.subscribe({ url1: String -> this.startFeedDownload(url1) }, - { error: Throwable? -> - if (error is FeedUrlNotFoundException) { - tryToRetrieveFeedUrlBySearch(error) - } else { - showNoPodcastFoundError() - Log.e(TAG, Log.getStackTraceString(error)) - } - }) - } - - private fun tryToRetrieveFeedUrlBySearch(error: FeedUrlNotFoundException) { - Log.d(TAG, "Unable to retrieve feed url, trying to retrieve feed url from search") - val url = searchFeedUrlByTrackName(error.trackName, error.artistName) - if (url != null) { - Log.d(TAG, "Successfully retrieve feed url") - isFeedFoundBySearch = true - startFeedDownload(url) - } else { - showNoPodcastFoundError() - Log.d(TAG, "Failed to retrieve feed url") - } - } - - private fun searchFeedUrlByTrackName(trackName: String, artistName: String): String? { - val searcher = CombinedSearcher() - val query = "$trackName $artistName" - val results = searcher.search(query)?.blockingGet() - if (results.isNullOrEmpty()) return null - for (result in results) { - if (result?.feedUrl != null && result.author != null && - result.author.equals(artistName, ignoreCase = true) && - result.title.equals(trackName, ignoreCase = true)) { - return result.feedUrl - } - } - return null - } - - private fun startFeedDownload(url: String) { - Log.d(TAG, "Starting feed download") - selectedDownloadUrl = prepareUrl(url) - val request = create(Feed(selectedDownloadUrl, null)) - .withAuthentication(username, password) - .withInitiatedByUser(true) - .build() - - download = Observable.fromCallable { - feeds = DBReader.getFeedList() - downloader = HttpDownloader(request) - downloader?.call() - downloader?.result - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ status: DownloadResult? -> if (request.destination != null) checkDownloadResult(status, request.destination!!) }, - { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) - } - - private fun checkDownloadResult(status: DownloadResult?, destination: String) { - if (status == null) return - if (status.isSuccessful) { - parseFeed(destination) - } else if (status.reason == DownloadError.ERROR_UNAUTHORIZED) { - if (!isFinishing && !isPaused) { - if (username != null && password != null) { - Toast.makeText(this, R.string.download_error_unauthorized, Toast.LENGTH_LONG).show() - } - if (downloader?.downloadRequest?.source != null) { - dialog = FeedViewAuthenticationDialog(this@OnlineFeedViewActivity, - R.string.authentication_notification_title, downloader!!.downloadRequest.source!!).create() - dialog?.show() - } - } - } else { - showErrorDialog(getString(from(status.reason)), status.reasonDetailed) - } - } - - @UnstableApi @Subscribe - fun onFeedListChanged(event: FeedListUpdateEvent?) { - updater = Observable.fromCallable { DBReader.getFeedList() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { feeds: List? -> - this@OnlineFeedViewActivity.feeds = feeds - handleUpdatedFeedStatus() - }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) } - ) - } - - @UnstableApi @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: EpisodeDownloadEvent?) { - handleUpdatedFeedStatus() - } - - private fun parseFeed(destination: String) { - Log.d(TAG, "Parsing feed") - parser = Maybe.fromCallable { doParseFeed(destination) } - .subscribeOn(Schedulers.computation()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeWith(object : DisposableMaybeObserver() { - - @UnstableApi override fun onSuccess(result: FeedHandlerResult) { - showFeedInformation(result.feed, result.alternateFeedUrls) - } - - override fun onComplete() { - // Ignore null result: We showed the discovery dialog. - } - - override fun onError(error: Throwable) { - showErrorDialog(error.message, "") - Log.d(TAG, "Feed parser exception: " + Log.getStackTraceString(error)) - } - }) - } - - /** - * Try to parse the feed. - * @return The FeedHandlerResult if successful. - * Null if unsuccessful but we started another attempt. - * @throws Exception If unsuccessful but we do not know a resolution. - */ - @Throws(Exception::class) - private fun doParseFeed(destination: String): FeedHandlerResult? { - val handler = FeedHandler() - val feed = Feed(selectedDownloadUrl, null) - feed.file_url = destination - val destinationFile = File(destination) - return try { - handler.parseFeed(feed) - } catch (e: UnsupportedFeedtypeException) { - Log.d(TAG, "Unsupported feed type detected") - if ("html".equals(e.rootElement, ignoreCase = true)) { - if (selectedDownloadUrl != null) { - val dialogShown = showFeedDiscoveryDialog(destinationFile, selectedDownloadUrl!!) - if (dialogShown) { - null // Should not display an error message - } else { - throw UnsupportedFeedtypeException(getString(R.string.download_error_unsupported_type_html)) - } - } else null - } else { - throw e - } - } catch (e: Exception) { - Log.e(TAG, Log.getStackTraceString(e)) - throw e - } finally { - val rc = destinationFile.delete() - Log.d(TAG, "Deleted feed source file. Result: $rc") - } - } - - /** - * Called when feed parsed successfully. - * This method is executed on the GUI thread. - */ - @UnstableApi private fun showFeedInformation(feed: Feed, alternateFeedUrls: Map) { - viewBinding.progressBar.visibility = View.GONE - viewBinding.feedDisplayContainer.visibility = View.VISIBLE - if (isFeedFoundBySearch) { - val resId = R.string.no_feed_url_podcast_found_by_search - Snackbar.make(findViewById(android.R.id.content), resId, Snackbar.LENGTH_LONG).show() - } - - viewBinding.backgroundImage.colorFilter = LightingColorFilter(-0x7d7d7e, 0x000000) - - viewBinding.listView.addHeaderView(headerBinding.root) - viewBinding.listView.setSelector(android.R.color.transparent) - viewBinding.listView.adapter = FeedItemlistDescriptionAdapter(this, 0, feed.items) - - if (StringUtils.isNotBlank(feed.imageUrl)) { - Glide.with(this) - .load(feed.imageUrl) - .apply(RequestOptions() - .placeholder(R.color.light_gray) - .error(R.color.light_gray) - .fitCenter() - .dontAnimate()) - .into(viewBinding.coverImage) - Glide.with(this) - .load(feed.imageUrl) - .apply(RequestOptions() - .placeholder(R.color.image_readability_tint) - .error(R.color.image_readability_tint) - .transform(FastBlurTransformation()) - .dontAnimate()) - .into(viewBinding.backgroundImage) - } - - viewBinding.titleLabel.text = feed.title - viewBinding.authorLabel.text = feed.author - headerBinding.txtvDescription.text = HtmlToPlainText.getPlainText(feed.description?:"") - - viewBinding.subscribeButton.setOnClickListener { v: View? -> - if (feedInFeedlist()) { - openFeed() - } else { - DBTasks.updateFeed(this, feed, false) - didPressSubscribe = true - handleUpdatedFeedStatus() - } - } - - viewBinding.stopPreviewButton.setOnClickListener { v: View? -> - writeNoMediaPlaying() - sendLocalBroadcast(this, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE) - } - - if (isEnableAutodownload) { - val preferences = getSharedPreferences(PREFS, MODE_PRIVATE) - viewBinding.autoDownloadCheckBox.isChecked = preferences.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true) - } - - headerBinding.txtvDescription.maxLines = DESCRIPTION_MAX_LINES_COLLAPSED - headerBinding.txtvDescription.setOnClickListener { v: View? -> - if (headerBinding.txtvDescription.maxLines > DESCRIPTION_MAX_LINES_COLLAPSED) { - headerBinding.txtvDescription.maxLines = DESCRIPTION_MAX_LINES_COLLAPSED - } else { - headerBinding.txtvDescription.maxLines = 2000 - } - } - - if (alternateFeedUrls.isEmpty()) { - viewBinding.alternateUrlsSpinner.visibility = View.GONE - } else { - viewBinding.alternateUrlsSpinner.visibility = View.VISIBLE - - val alternateUrlsList: MutableList = ArrayList() - val alternateUrlsTitleList: MutableList = ArrayList() - - if (feed.download_url != null) alternateUrlsList.add(feed.download_url!!) - alternateUrlsTitleList.add(feed.title) - - - alternateUrlsList.addAll(alternateFeedUrls.keys) - for (url in alternateFeedUrls.keys) { - alternateUrlsTitleList.add(alternateFeedUrls[url]) - } - - val adapter: ArrayAdapter = object : ArrayAdapter(this, - R.layout.alternate_urls_item, alternateUrlsTitleList) { - override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { - // reusing the old view causes a visual bug on Android <= 10 - return super.getDropDownView(position, null, parent) - } - } - - adapter.setDropDownViewResource(R.layout.alternate_urls_dropdown_item) - viewBinding.alternateUrlsSpinner.adapter = adapter - viewBinding.alternateUrlsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) { - selectedDownloadUrl = alternateUrlsList[position] - } - - override fun onNothingSelected(parent: AdapterView<*>?) { - } - } - } - handleUpdatedFeedStatus() - } - - @UnstableApi private fun openFeed() { - // feed.getId() is always 0, we have to retrieve the id from the feed list from - // the database - val intent = MainActivity.getIntentToOpenFeed(this, feedId) - intent.putExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH, - getIntent().getBooleanExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH, false)) - finish() - startActivity(intent) - } - - @UnstableApi private fun handleUpdatedFeedStatus() { - val dli = DownloadServiceInterface.get() - if (dli == null || selectedDownloadUrl == null) return - - if (dli.isDownloadingEpisode(selectedDownloadUrl!!)) { - viewBinding.subscribeButton.isEnabled = false - viewBinding.subscribeButton.setText(R.string.subscribing_label) - } else if (feedInFeedlist()) { - viewBinding.subscribeButton.isEnabled = true - viewBinding.subscribeButton.setText(R.string.open_podcast) - if (didPressSubscribe) { - didPressSubscribe = false - - val feed1 = DBReader.getFeed(feedId)?: return - val feedPreferences = feed1.preferences - if (feedPreferences != null) { - if (isEnableAutodownload) { - val autoDownload = viewBinding.autoDownloadCheckBox.isChecked - feedPreferences.autoDownload = autoDownload - - val preferences = getSharedPreferences(PREFS, MODE_PRIVATE) - val editor = preferences.edit() - editor.putBoolean(PREF_LAST_AUTO_DOWNLOAD, autoDownload) - editor.apply() - } - if (username != null) { - feedPreferences.username = username - feedPreferences.password = password - } - DBWriter.setFeedPreferences(feedPreferences) - } - openFeed() - } - } else { - viewBinding.subscribeButton.isEnabled = true - viewBinding.subscribeButton.setText(R.string.subscribe_label) - if (isEnableAutodownload) { - viewBinding.autoDownloadCheckBox.visibility = View.VISIBLE - } - } - } - - private fun feedInFeedlist(): Boolean { - return feedId != 0L - } - - private val feedId: Long - get() { - if (feeds == null) { - return 0 - } - for (f in feeds!!) { - if (f.download_url == selectedDownloadUrl) { - return f.id - } - } - return 0 - } - - @UiThread - private fun showErrorDialog(errorMsg: String?, details: String) { - if (!isFinishing && !isPaused) { - val builder = MaterialAlertDialogBuilder(this) - builder.setTitle(R.string.error_label) - if (errorMsg != null) { - val total = """ - $errorMsg - - $details - """.trimIndent() - val errorMessage = SpannableString(total) - errorMessage.setSpan(ForegroundColorSpan(-0x77777778), - errorMsg.length, total.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - builder.setMessage(errorMessage) - } else { - builder.setMessage(R.string.download_error_error_unknown) - } - builder.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, which: Int -> dialog.cancel() } - if (intent.getBooleanExtra(ARG_WAS_MANUAL_URL, false)) { - builder.setNeutralButton(R.string.edit_url_menu) { dialog: DialogInterface?, which: Int -> editUrl() } - } - builder.setOnCancelListener { dialog: DialogInterface? -> - setResult(RESULT_ERROR) - finish() - } - if (dialog != null && dialog!!.isShowing) { - dialog!!.dismiss() - } - dialog = builder.show() - } - } - - private fun editUrl() { - val builder = MaterialAlertDialogBuilder(this) - builder.setTitle(R.string.edit_url_menu) - val dialogBinding = EditTextDialogBinding.inflate(layoutInflater) - if (downloader != null) { - dialogBinding.urlEditText.setText(downloader!!.downloadRequest.source) - } - builder.setView(dialogBinding.root) - builder.setPositiveButton(R.string.confirm_label) { dialog: DialogInterface?, which: Int -> - setLoadingLayout() - lookupUrlAndDownload(dialogBinding.urlEditText.text.toString()) - } - builder.setNegativeButton(R.string.cancel_label) { dialog1: DialogInterface, which: Int -> dialog1.cancel() } - builder.setOnCancelListener { dialog1: DialogInterface? -> - setResult(RESULT_ERROR) - finish() - } - builder.show() - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun playbackStateChanged(event: PlayerStatusEvent?) { - val isPlayingPreview = currentlyPlayingMediaType == RemoteMedia.PLAYABLE_TYPE_REMOTE_MEDIA.toLong() - viewBinding.stopPreviewButton.visibility = if (isPlayingPreview) View.VISIBLE else View.GONE - } - - /** - * - * @return true if a FeedDiscoveryDialog is shown, false otherwise (e.g., due to no feed found). - */ - private fun showFeedDiscoveryDialog(feedFile: File, baseUrl: String): Boolean { - val fd = FeedDiscoverer() - val urlsMap: Map - try { - urlsMap = fd.findLinks(feedFile, baseUrl) - if (urlsMap.isEmpty()) { - return false - } - } catch (e: IOException) { - e.printStackTrace() - return false - } - - if (isPaused || isFinishing) { - return false - } - - val titles: MutableList = ArrayList() - - val urls: List = ArrayList(urlsMap.keys) - for (url in urls) { - titles.add(urlsMap[url]) - } - - if (urls.size == 1) { - // Skip dialog and display the item directly - resetIntent(urls[0]) - startFeedDownload(urls[0]) - return true - } - - val adapter = ArrayAdapter(this@OnlineFeedViewActivity, - R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles) - val onClickListener = DialogInterface.OnClickListener { dialog: DialogInterface, which: Int -> - val selectedUrl = urls[which] - dialog.dismiss() - resetIntent(selectedUrl) - startFeedDownload(selectedUrl) - } - - val ab = MaterialAlertDialogBuilder(this@OnlineFeedViewActivity) - .setTitle(R.string.feeds_label) - .setCancelable(true) - .setOnCancelListener { dialog: DialogInterface? -> finish() } - .setAdapter(adapter, onClickListener) - - runOnUiThread { - if (dialog != null && dialog!!.isShowing) { - dialog!!.dismiss() - } - dialog = ab.show() - } - return true - } - - private inner class FeedViewAuthenticationDialog(context: Context?, titleRes: Int, private val feedUrl: String) : - AuthenticationDialog(context, titleRes, true, username, password) { - override fun onCancelled() { - super.onCancelled() - finish() - } - - override fun onConfirmed(username: String, password: String) { - this@OnlineFeedViewActivity.username = username - this@OnlineFeedViewActivity.password = password - startFeedDownload(feedUrl) - } - } - - companion object { - const val ARG_FEEDURL: String = "arg.feedurl" - const val ARG_WAS_MANUAL_URL: String = "manual_url" - private const val RESULT_ERROR = 2 - private const val TAG = "OnlineFeedViewActivity" - private const val PREFS = "OnlineFeedViewActivityPreferences" - private const val PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload" - private const val DESCRIPTION_MAX_LINES_COLLAPSED = 4 - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/activity/OpmlImportActivity.kt b/app/src/main/java/ac/mdiq/podcini/activity/OpmlImportActivity.kt deleted file mode 100644 index 8ab8d028..00000000 --- a/app/src/main/java/ac/mdiq/podcini/activity/OpmlImportActivity.kt +++ /dev/null @@ -1,270 +0,0 @@ -package ac.mdiq.podcini.activity - -import android.Manifest -import android.content.DialogInterface -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.text.Spannable -import android.text.SpannableString -import android.text.style.ForegroundColorSpan -import android.util.Log -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.widget.AdapterView -import android.widget.AdapterView.OnItemClickListener -import android.widget.ArrayAdapter -import android.widget.ListView -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ActivityCompat -import androidx.media3.common.util.UnstableApi -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.export.opml.OpmlElement -import ac.mdiq.podcini.core.export.opml.OpmlReader -import ac.mdiq.podcini.core.preferences.ThemeSwitcher.getTheme -import ac.mdiq.podcini.core.storage.DBTasks -import ac.mdiq.podcini.core.util.download.FeedUpdateManager.runOnce -import ac.mdiq.podcini.databinding.OpmlSelectionBinding -import ac.mdiq.podcini.model.feed.Feed -import io.reactivex.Completable -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers -import org.apache.commons.io.input.BOMInputStream -import java.io.InputStreamReader -import java.io.Reader - -/** - * Activity for Opml Import. - */ -class OpmlImportActivity : AppCompatActivity() { - private var uri: Uri? = null - private lateinit var viewBinding: OpmlSelectionBinding - private lateinit var selectAll: MenuItem - private lateinit var deselectAll: MenuItem - - private var listAdapter: ArrayAdapter? = null - private var readElements: ArrayList? = null - - @UnstableApi override fun onCreate(savedInstanceState: Bundle?) { - setTheme(getTheme(this)) - super.onCreate(savedInstanceState) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - viewBinding = OpmlSelectionBinding.inflate(layoutInflater) - setContentView(viewBinding.root) - - viewBinding.feedlist.choiceMode = ListView.CHOICE_MODE_MULTIPLE - viewBinding.feedlist.onItemClickListener = - OnItemClickListener { parent: AdapterView<*>?, view: View?, position: Int, id: Long -> - val checked = viewBinding.feedlist.checkedItemPositions - var checkedCount = 0 - for (i in 0 until checked.size()) { - if (checked.valueAt(i)) { - checkedCount++ - } - } - if (listAdapter != null) { - if (checkedCount == listAdapter!!.count) { - selectAll.setVisible(false) - deselectAll.setVisible(true) - } else { - deselectAll.setVisible(false) - selectAll.setVisible(true) - } - } - } - viewBinding.butCancel.setOnClickListener { v: View? -> - setResult(RESULT_CANCELED) - finish() - } - viewBinding.butConfirm.setOnClickListener { v: View? -> - viewBinding.progressBar.visibility = View.VISIBLE - Completable.fromAction { - val checked = viewBinding.feedlist.checkedItemPositions - for (i in 0 until checked.size()) { - if (!checked.valueAt(i)) { - continue - } - if (!readElements.isNullOrEmpty()) { - val element = readElements!![checked.keyAt(i)] - val feed = Feed(element.xmlUrl, null, - if (element.text != null) element.text else "Unknown podcast") - feed.items = mutableListOf() - DBTasks.updateFeed(this, feed, false) - } - } - runOnce(this) - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - viewBinding.progressBar.visibility = View.GONE - val intent = Intent(this@OpmlImportActivity, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - finish() - }, { e: Throwable -> - e.printStackTrace() - viewBinding.progressBar.visibility = View.GONE - Toast.makeText(this, e.message, Toast.LENGTH_LONG).show() - }) - } - - var uri = intent.data - if (uri != null && uri.toString().startsWith("/")) { - uri = Uri.parse("file://$uri") - } else { - val extraText = intent.getStringExtra(Intent.EXTRA_TEXT) - if (extraText != null) { - uri = Uri.parse(extraText) - } - } - importUri(uri) - } - - fun importUri(uri: Uri?) { - if (uri == null) { - MaterialAlertDialogBuilder(this) - .setMessage(R.string.opml_import_error_no_file) - .setPositiveButton(android.R.string.ok, null) - .show() - return - } - this.uri = uri - startImport() - } - - private val titleList: List - get() { - val result: MutableList = ArrayList() - if (!readElements.isNullOrEmpty()) { - for (element in readElements!!) { - if (element.text != null) result.add(element.text!!) - } - } - return result - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - super.onCreateOptionsMenu(menu) - val inflater = menuInflater - inflater.inflate(R.menu.opml_selection_options, menu) - selectAll = menu.findItem(R.id.select_all_item) - deselectAll = menu.findItem(R.id.deselect_all_item) - deselectAll.setVisible(false) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - val itemId = item.itemId - when (itemId) { - R.id.select_all_item -> { - selectAll.setVisible(false) - selectAllItems(true) - deselectAll.setVisible(true) - return true - } - R.id.deselect_all_item -> { - deselectAll.setVisible(false) - selectAllItems(false) - selectAll.setVisible(true) - return true - } - android.R.id.home -> { - finish() - } - } - return false - } - - private fun selectAllItems(b: Boolean) { - for (i in 0 until viewBinding.feedlist.count) { - viewBinding.feedlist.setItemChecked(i, b) - } - } - - private fun requestPermission() { - requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) - } - - private val requestPermissionLauncher = - registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> - if (isGranted) { - startImport() - } else { - MaterialAlertDialogBuilder(this) - .setMessage(R.string.opml_import_ask_read_permission) - .setPositiveButton(android.R.string.ok) { dialog: DialogInterface?, which: Int -> requestPermission() } - .setNegativeButton(R.string.cancel_label) { dialog: DialogInterface?, which: Int -> finish() } - .show() - } - } - - /** Starts the import process. */ - private fun startImport() { - viewBinding.progressBar.visibility = View.VISIBLE - - Observable.fromCallable { - val opmlFileStream = contentResolver.openInputStream(uri!!) - val bomInputStream = BOMInputStream(opmlFileStream) - val bom = bomInputStream.bom - val charsetName = if (bom == null) "UTF-8" else bom.charsetName - val reader: Reader = InputStreamReader(bomInputStream, charsetName) - val opmlReader = OpmlReader() - val result = opmlReader.readDocument(reader) - reader.close() - result - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { result: ArrayList? -> - viewBinding.progressBar.visibility = View.GONE - Log.d(TAG, "Parsing was successful") - readElements = result - listAdapter = ArrayAdapter(this@OpmlImportActivity, - android.R.layout.simple_list_item_multiple_choice, - titleList) - viewBinding.feedlist.adapter = listAdapter - }, { e: Throwable -> - Log.d(TAG, Log.getStackTraceString(e)) - val message = if (e.message == null) "" else e.message!! - if (message.lowercase().contains("permission") - && Build.VERSION.SDK_INT >= 23) { - val permission = ActivityCompat.checkSelfPermission(this, - Manifest.permission.READ_EXTERNAL_STORAGE) - if (permission != PackageManager.PERMISSION_GRANTED) { - requestPermission() - return@subscribe - } - } - viewBinding.progressBar.visibility = View.GONE - val alert = MaterialAlertDialogBuilder(this) - alert.setTitle(R.string.error_label) - val userReadable = getString(R.string.opml_reader_error) - val details = e.message - val total = """ - $userReadable - - $details - """.trimIndent() - val errorMessage = SpannableString(total) - errorMessage.setSpan(ForegroundColorSpan(-0x77777778), - userReadable.length, total.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - alert.setMessage(errorMessage) - alert.setPositiveButton(android.R.string.ok) { dialog: DialogInterface?, which: Int -> finish() } - alert.show() - }) - } - - companion object { - private const val TAG = "OpmlImportBaseActivity" - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/activity/PlaybackSpeedDialogActivity.kt b/app/src/main/java/ac/mdiq/podcini/activity/PlaybackSpeedDialogActivity.kt deleted file mode 100644 index 61007cfb..00000000 --- a/app/src/main/java/ac/mdiq/podcini/activity/PlaybackSpeedDialogActivity.kt +++ /dev/null @@ -1,23 +0,0 @@ -package ac.mdiq.podcini.activity - -import android.content.DialogInterface -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import ac.mdiq.podcini.core.preferences.ThemeSwitcher.getTranslucentTheme -import ac.mdiq.podcini.dialog.VariableSpeedDialog - -class PlaybackSpeedDialogActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - setTheme(getTranslucentTheme(this)) - super.onCreate(savedInstanceState) - val speedDialog: VariableSpeedDialog = InnerVariableSpeedDialog() - speedDialog.show(supportFragmentManager, null) - } - - class InnerVariableSpeedDialog : VariableSpeedDialog() { - override fun onDismiss(dialog: DialogInterface) { - super.onDismiss(dialog) - requireActivity().finish() - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/activity/PreferenceActivity.kt b/app/src/main/java/ac/mdiq/podcini/activity/PreferenceActivity.kt deleted file mode 100644 index f8dd98a3..00000000 --- a/app/src/main/java/ac/mdiq/podcini/activity/PreferenceActivity.kt +++ /dev/null @@ -1,203 +0,0 @@ -package ac.mdiq.podcini.activity - -import android.annotation.SuppressLint -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.provider.Settings -import android.util.Log -import android.view.MenuItem -import android.view.View -import android.view.inputmethod.InputMethodManager -import androidx.appcompat.app.AppCompatActivity -import androidx.preference.PreferenceFragmentCompat -import com.bytehamster.lib.preferencesearch.SearchPreferenceResult -import com.bytehamster.lib.preferencesearch.SearchPreferenceResultListener -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.preferences.ThemeSwitcher.getTheme -import ac.mdiq.podcini.databinding.SettingsActivityBinding -import ac.mdiq.podcini.event.MessageEvent -import ac.mdiq.podcini.fragment.preferences.* -import ac.mdiq.podcini.fragment.preferences.synchronization.SynchronizationPreferencesFragment -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode - -/** - * PreferenceActivity for API 11+. In order to change the behavior of the preference UI, see - * PreferenceController. - */ -class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener { - private lateinit var binding: SettingsActivityBinding - - @SuppressLint("CommitTransaction") - override fun onCreate(savedInstanceState: Bundle?) { - setTheme(getTheme(this)) - super.onCreate(savedInstanceState) - - val ab = supportActionBar - ab?.setDisplayHomeAsUpEnabled(true) - - binding = SettingsActivityBinding.inflate(layoutInflater) - setContentView(binding.root) - - if (supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) == null) { - supportFragmentManager.beginTransaction() - .replace(binding.settingsContainer.id, MainPreferencesFragment(), FRAGMENT_TAG) - .commit() - } - val intent = intent - if (intent.getBooleanExtra(OPEN_AUTO_DOWNLOAD_SETTINGS, false)) { - openScreen(R.xml.preferences_autodownload) - } - } - - private fun getPreferenceScreen(screen: Int): PreferenceFragmentCompat? { - var prefFragment: PreferenceFragmentCompat? = null - - when (screen) { - R.xml.preferences_user_interface -> { - prefFragment = UserInterfacePreferencesFragment() - } - R.xml.preferences_downloads -> { - prefFragment = DownloadsPreferencesFragment() - } - R.xml.preferences_import_export -> { - prefFragment = ImportExportPreferencesFragment() - } - R.xml.preferences_autodownload -> { - prefFragment = AutoDownloadPreferencesFragment() - } - R.xml.preferences_synchronization -> { - prefFragment = SynchronizationPreferencesFragment() - } - R.xml.preferences_playback -> { - prefFragment = PlaybackPreferencesFragment() - } - R.xml.preferences_notifications -> { - prefFragment = NotificationPreferencesFragment() - } - R.xml.preferences_swipe -> { - prefFragment = SwipePreferencesFragment() - } - } - return prefFragment - } - - @SuppressLint("CommitTransaction") - fun openScreen(screen: Int): PreferenceFragmentCompat? { - val fragment = getPreferenceScreen(screen) - if (screen == R.xml.preferences_notifications && Build.VERSION.SDK_INT >= 26) { - val intent = Intent() - intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS) - intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName) - startActivity(intent) - } else { - supportFragmentManager.beginTransaction() - .replace(binding.settingsContainer.id, fragment!!) - .addToBackStack(getString(getTitleOfPage(screen))) - .commit() - } - - - return fragment - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - if (supportFragmentManager.backStackEntryCount == 0) { - finish() - } else { - val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager - var view = currentFocus - //If no view currently has focus, create a new one, just so we can grab a window token from it - if (view == null) { - view = View(this) - } - imm.hideSoftInputFromWindow(view.windowToken, 0) - supportFragmentManager.popBackStack() - } - return true - } - return false - } - - override fun onSearchResultClicked(result: SearchPreferenceResult) { - when (val screen = result.resourceFile) { - R.xml.feed_settings -> { - val builder = MaterialAlertDialogBuilder(this) - builder.setTitle(R.string.feed_settings_label) - builder.setMessage(R.string.pref_feed_settings_dialog_msg) - builder.setPositiveButton(android.R.string.ok, null) - builder.show() - } - R.xml.preferences_notifications -> { - openScreen(screen) - } - else -> { - val fragment = openScreen(result.resourceFile) - result.highlight(fragment) - } - } - } - - override fun onStart() { - super.onStart() - EventBus.getDefault().register(this) - } - - override fun onStop() { - super.onStop() - EventBus.getDefault().unregister(this) - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: MessageEvent) { - Log.d(FRAGMENT_TAG, "onEvent($event)") - val s = Snackbar.make(binding.root, event.message, Snackbar.LENGTH_LONG) - if (event.action != null) { - s.setAction(event.actionText) { v: View? -> event.action!!.accept(this) } - } - s.show() - } - - companion object { - private const val FRAGMENT_TAG = "tag_preferences" - const val OPEN_AUTO_DOWNLOAD_SETTINGS: String = "OpenAutoDownloadSettings" - @JvmStatic - fun getTitleOfPage(preferences: Int): Int { - when (preferences) { - R.xml.preferences_downloads -> { - return R.string.downloads_pref - } - R.xml.preferences_autodownload -> { - return R.string.pref_automatic_download_title - } - R.xml.preferences_playback -> { - return R.string.playback_pref - } - R.xml.preferences_import_export -> { - return R.string.import_export_pref - } - R.xml.preferences_user_interface -> { - return R.string.user_interface_label - } - R.xml.preferences_synchronization -> { - return R.string.synchronization_pref - } - R.xml.preferences_notifications -> { - return R.string.notification_pref_fragment - } - R.xml.feed_settings -> { - return R.string.feed_settings_label - } - R.xml.preferences_swipe -> { - return R.string.swipeactions_label - } - else -> return R.string.settings_label - } - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/activity/SelectSubscriptionActivity.kt b/app/src/main/java/ac/mdiq/podcini/activity/SelectSubscriptionActivity.kt deleted file mode 100644 index d88a1ab7..00000000 --- a/app/src/main/java/ac/mdiq/podcini/activity/SelectSubscriptionActivity.kt +++ /dev/null @@ -1,158 +0,0 @@ -package ac.mdiq.podcini.activity - -import ac.mdiq.podcini.R -import ac.mdiq.podcini.activity.MainActivity.Companion.EXTRA_FEED_ID -import ac.mdiq.podcini.core.preferences.ThemeSwitcher -import ac.mdiq.podcini.core.storage.DBReader -import ac.mdiq.podcini.core.storage.NavDrawerData -import ac.mdiq.podcini.databinding.SubscriptionSelectionActivityBinding -import ac.mdiq.podcini.model.feed.Feed -import ac.mdiq.podcini.storage.preferences.UserPreferences -import android.app.Activity -import android.content.Intent -import android.graphics.Bitmap -import android.os.Bundle -import android.util.Log -import android.view.View -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.ListView -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.pm.ShortcutInfoCompat -import androidx.core.content.pm.ShortcutManagerCompat -import androidx.core.graphics.drawable.IconCompat -import androidx.media3.common.util.UnstableApi -import com.bumptech.glide.Glide -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.RequestOptions -import com.bumptech.glide.request.target.Target -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers - -class SelectSubscriptionActivity : AppCompatActivity() { - private var disposable: Disposable? = null - - @Volatile - private var listItems: List? = null - - private lateinit var viewBinding: SubscriptionSelectionActivityBinding - - override fun onCreate(savedInstanceState: Bundle?) { - setTheme(ThemeSwitcher.getTranslucentTheme(this)) - super.onCreate(savedInstanceState) - - viewBinding = SubscriptionSelectionActivityBinding.inflate(layoutInflater) - setContentView(viewBinding.root) - setSupportActionBar(viewBinding.toolbar) - setTitle(R.string.shortcut_select_subscription) - - viewBinding.transparentBackground.setOnClickListener { v: View? -> finish() } - viewBinding.card.setOnClickListener(null) - - loadSubscriptions() - - val checkedPosition = arrayOfNulls(1) - viewBinding.list.choiceMode = ListView.CHOICE_MODE_SINGLE - viewBinding.list.onItemClickListener = - AdapterView.OnItemClickListener { listView: AdapterView<*>?, view1: View?, position: Int, rowId: Long -> - checkedPosition[0] = position - } - viewBinding.shortcutBtn.setOnClickListener { view: View? -> - if (checkedPosition[0] != null && Intent.ACTION_CREATE_SHORTCUT == intent.action) { - getBitmapFromUrl(listItems!![checkedPosition[0]!!]) - } - } - } - - fun getFeedItems(items: List, result: MutableList): List { - for (item in items) { - if (item == null) continue - if (item.type == NavDrawerData.DrawerItem.Type.TAG) { - getFeedItems((item as NavDrawerData.TagDrawerItem).children, result) - } else { - val feed: Feed = (item as NavDrawerData.FeedDrawerItem).feed - if (!result.contains(feed)) { - result.add(feed) - } - } - } - return result - } - - @UnstableApi private fun addShortcut(feed: Feed, bitmap: Bitmap?) { - val intent = Intent(this, MainActivity::class.java) - intent.setAction(Intent.ACTION_MAIN) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) - intent.putExtra(EXTRA_FEED_ID, feed.id) - val id = "subscription-" + feed.id - - val icon: IconCompat = if (bitmap != null) { - IconCompat.createWithAdaptiveBitmap(bitmap) - } else { - IconCompat.createWithResource(this, R.drawable.ic_subscriptions_shortcut) - } - - val shortcut: ShortcutInfoCompat = ShortcutInfoCompat.Builder(this, id) - .setShortLabel(feed.title?:"") - .setLongLabel(feed.feedTitle?:"") - .setIntent(intent) - .setIcon(icon) - .build() - - setResult(Activity.RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this, shortcut)) - finish() - } - - private fun getBitmapFromUrl(feed: Feed) { - val iconSize = (128 * resources.displayMetrics.density).toInt() - Glide.with(this) - .asBitmap() - .load(feed.imageUrl) - .apply(RequestOptions.overrideOf(iconSize, iconSize)) - .listener(object : RequestListener { - @UnstableApi override fun onLoadFailed(e: GlideException?, model: Any?, - target: Target, isFirstResource: Boolean - ): Boolean { - addShortcut(feed, null) - return true - } - - @UnstableApi override fun onResourceReady(resource: Bitmap, model: Any, - target: Target, dataSource: DataSource, isFirstResource: Boolean - ): Boolean { - addShortcut(feed, resource) - return true - } - }).submit() - } - - private fun loadSubscriptions() { - disposable?.dispose() - - disposable = Observable.fromCallable { - val data: NavDrawerData = DBReader.getNavDrawerData(UserPreferences.subscriptionsFilter) - getFeedItems(data.items, ArrayList()) - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { result: List -> - listItems = result - val titles = ArrayList() - for (feed in result) { - if (feed.title != null) titles.add(feed.title!!) - } - val adapter: ArrayAdapter = ArrayAdapter(this, - R.layout.simple_list_item_multiple_choice_on_start, titles) - viewBinding.list.adapter = adapter - }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) - } - - companion object { - private const val TAG = "SelectSubscription" - } -} \ No newline at end of file diff --git a/app/src/main/java/ac/mdiq/podcini/activity/SplashActivity.kt b/app/src/main/java/ac/mdiq/podcini/activity/SplashActivity.kt deleted file mode 100644 index 17262236..00000000 --- a/app/src/main/java/ac/mdiq/podcini/activity/SplashActivity.kt +++ /dev/null @@ -1,48 +0,0 @@ -package ac.mdiq.podcini.activity - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.view.View -import android.widget.Toast -import androidx.media3.common.util.UnstableApi -import ac.mdiq.podcini.error.CrashReportWriter -import ac.mdiq.podcini.storage.database.PodDBAdapter -import io.reactivex.Completable -import io.reactivex.CompletableEmitter -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers - -/** - * Shows the Podcini logo while waiting for the main activity to start. - */ -@SuppressLint("CustomSplashScreen") -class SplashActivity : Activity() { - @UnstableApi override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val content = findViewById(android.R.id.content) - content.viewTreeObserver.addOnPreDrawListener { false } // Keep splash screen active - - Completable.create { subscriber: CompletableEmitter -> - // Trigger schema updates - PodDBAdapter.getInstance()?.open() - PodDBAdapter.getInstance()?.close() - subscriber.onComplete() - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - val intent = Intent(this@SplashActivity, MainActivity::class.java) - startActivity(intent) - overridePendingTransition(0, 0) - finish() - }, { error: Throwable -> - error.printStackTrace() - CrashReportWriter.write(error) - Toast.makeText(this, error.localizedMessage, Toast.LENGTH_LONG).show() - finish() - }) - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/activity/VideoplayerActivity.kt b/app/src/main/java/ac/mdiq/podcini/activity/VideoplayerActivity.kt deleted file mode 100644 index 16d7cf25..00000000 --- a/app/src/main/java/ac/mdiq/podcini/activity/VideoplayerActivity.kt +++ /dev/null @@ -1,787 +0,0 @@ -package ac.mdiq.podcini.activity - -import ac.mdiq.podcini.R -import ac.mdiq.podcini.activity.MainActivity -import ac.mdiq.podcini.core.service.playback.PlaybackService.Companion.getPlayerActivityIntent -import ac.mdiq.podcini.core.service.playback.PlaybackService.Companion.isCasting -import ac.mdiq.podcini.core.storage.DBReader -import ac.mdiq.podcini.core.storage.DBWriter -import ac.mdiq.podcini.core.util.Converter.getDurationStringLong -import ac.mdiq.podcini.core.util.FeedItemUtil.getLinkWithFallback -import ac.mdiq.podcini.core.util.IntentUtils.openInBrowser -import ac.mdiq.podcini.core.util.ShareUtils.hasLinkToShare -import ac.mdiq.podcini.core.util.TimeSpeedConverter -import ac.mdiq.podcini.core.util.gui.PictureInPictureUtil -import ac.mdiq.podcini.core.util.playback.PlaybackController -import ac.mdiq.podcini.databinding.VideoplayerActivityBinding -import ac.mdiq.podcini.dialog.* -import ac.mdiq.podcini.event.MessageEvent -import ac.mdiq.podcini.event.PlayerErrorEvent -import ac.mdiq.podcini.event.playback.BufferUpdateEvent -import ac.mdiq.podcini.event.playback.PlaybackPositionEvent -import ac.mdiq.podcini.event.playback.PlaybackServiceEvent -import ac.mdiq.podcini.event.playback.SleepTimerUpdatedEvent -import ac.mdiq.podcini.fragment.ChaptersFragment -import ac.mdiq.podcini.model.feed.FeedItem -import ac.mdiq.podcini.model.feed.FeedMedia -import ac.mdiq.podcini.model.playback.Playable -import ac.mdiq.podcini.playback.base.PlayerStatus -import ac.mdiq.podcini.playback.cast.CastEnabledActivity -import ac.mdiq.podcini.storage.preferences.UserPreferences.fastForwardSecs -import ac.mdiq.podcini.storage.preferences.UserPreferences.rewindSecs -import ac.mdiq.podcini.storage.preferences.UserPreferences.setShowRemainTimeSetting -import ac.mdiq.podcini.storage.preferences.UserPreferences.shouldShowRemainingTime -import ac.mdiq.podcini.ui.appstartintent.MainActivityStarter -import android.content.DialogInterface -import android.content.Intent -import android.graphics.PixelFormat -import android.graphics.drawable.ColorDrawable -import android.media.AudioManager -import android.os.Build -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.util.Log -import android.view.* -import android.view.View.OnTouchListener -import android.view.animation.* -import android.widget.EditText -import android.widget.FrameLayout -import android.widget.SeekBar -import android.widget.SeekBar.OnSeekBarChangeListener -import androidx.interpolator.view.animation.FastOutSlowInInterpolator -import androidx.media3.common.util.UnstableApi -import com.bumptech.glide.Glide -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode - -/** - * Activity for playing video files. - */ -@UnstableApi -class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener { - - private lateinit var viewBinding: VideoplayerActivityBinding - - /** - * True if video controls are currently visible. - */ - private var videoControlsShowing = true - private var videoSurfaceCreated = false - private var destroyingDueToReload = false - private var lastScreenTap: Long = 0 - private val videoControlsHider = Handler(Looper.getMainLooper()) - private var controller: PlaybackController? = null - private var showTimeLeft = false - private var isFavorite = false - private var switchToAudioOnly = false - private var disposable: Disposable? = null - private var prog = 0f - - override fun onCreate(savedInstanceState: Bundle?) { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN) - // has to be called before setting layout content - supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY) - setTheme(R.style.Theme_Podcini_VideoPlayer) - super.onCreate(savedInstanceState) - - Log.d(TAG, "onCreate()") - - window.setFormat(PixelFormat.TRANSPARENT) - viewBinding = VideoplayerActivityBinding.inflate(LayoutInflater.from(this)) - setContentView(viewBinding.root) - setupView() - supportActionBar?.setBackgroundDrawable(ColorDrawable(-0x80000000)) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - @UnstableApi - override fun onResume() { - super.onResume() - switchToAudioOnly = false - if (isCasting) { - val intent = getPlayerActivityIntent(this) - if (intent.component!!.className != VideoplayerActivity::class.java.name) { - destroyingDueToReload = true - finish() - startActivity(intent) - } - } - } - - @UnstableApi - override fun onStop() { - controller?.release() - controller = null // prevent leak - disposable?.dispose() - - EventBus.getDefault().unregister(this) - super.onStop() - if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { - videoControlsHider.removeCallbacks(hideVideoControls) - } - // Controller released; we will not receive buffering updates - viewBinding.progressBar.visibility = View.GONE - } - - public override fun onUserLeaveHint() { - if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { - compatEnterPictureInPicture() - } - } - - @UnstableApi - override fun onStart() { - super.onStart() - controller = newPlaybackController() - controller!!.init() - loadMediaInfo() - onPositionObserverUpdate() - EventBus.getDefault().register(this) - } - - @UnstableApi - override fun onPause() { - if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { - if (controller != null && controller!!.status == PlayerStatus.PLAYING) { - controller!!.pause() - } - } - super.onPause() - } - - override fun onTrimMemory(level: Int) { - super.onTrimMemory(level) - Glide.get(this).trimMemory(level) - } - - override fun onLowMemory() { - super.onLowMemory() - Glide.get(this).clearMemory() - } - - @UnstableApi - private fun newPlaybackController(): PlaybackController { - return object : PlaybackController(this@VideoplayerActivity) { - override fun updatePlayButtonShowsPlay(showPlay: Boolean) { - viewBinding.playButton.setIsShowPlay(showPlay) - if (showPlay) { - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } else { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - setupVideoAspectRatio() - if (videoSurfaceCreated && controller != null) { - Log.d(TAG, "Videosurface already created, setting videosurface now") - controller!!.setVideoSurface(viewBinding.videoView.holder) - } - } - } - - override fun loadMediaInfo() { - this@VideoplayerActivity.loadMediaInfo() - } - - override fun onPlaybackEnd() { - finish() - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun bufferUpdate(event: BufferUpdateEvent) { - if (event.hasStarted()) { - viewBinding.progressBar.visibility = View.VISIBLE - } else if (event.hasEnded()) { - viewBinding.progressBar.visibility = View.INVISIBLE - } else { - viewBinding.sbPosition.secondaryProgress = (event.progress * viewBinding.sbPosition.max).toInt() - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun sleepTimerUpdate(event: SleepTimerUpdatedEvent) { - if (event.isCancelled || event.wasJustEnabled()) { - supportInvalidateOptionsMenu() - } - } - - @UnstableApi - private fun loadMediaInfo() { - Log.d(TAG, "loadMediaInfo()") - if (controller?.getMedia() == null) { - return - } - if (controller!!.status == PlayerStatus.PLAYING && !controller!!.isPlayingVideoLocally) { - Log.d(TAG, "Closing, no longer video") - destroyingDueToReload = true - finish() - MainActivityStarter(this).withOpenPlayer().start() - return - } - showTimeLeft = shouldShowRemainingTime() - onPositionObserverUpdate() - checkFavorite() - val media = controller!!.getMedia() - if (media != null) { - supportActionBar!!.subtitle = media.getEpisodeTitle() - supportActionBar!!.title = media.getFeedTitle() - } - } - - @UnstableApi - private fun setupView() { - showTimeLeft = shouldShowRemainingTime() - Log.d("timeleft", if (showTimeLeft) "true" else "false") - viewBinding.durationLabel.setOnClickListener { v: View? -> - showTimeLeft = !showTimeLeft - val media = controller?.getMedia() ?: return@setOnClickListener - - val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier) - val length: String - if (showTimeLeft) { - val remainingTime = converter.convert(media.getDuration() - media.getPosition()) - length = "-" + getDurationStringLong(remainingTime) - } else { - val duration = converter.convert(media.getDuration()) - length = getDurationStringLong(duration) - } - viewBinding.durationLabel.text = length - - setShowRemainTimeSetting(showTimeLeft) - Log.d("timeleft on click", if (showTimeLeft) "true" else "false") - } - - viewBinding.sbPosition.setOnSeekBarChangeListener(this) - viewBinding.rewindButton.setOnClickListener { v: View? -> onRewind() } - viewBinding.rewindButton.setOnLongClickListener { v: View? -> - SkipPreferenceDialog.showSkipPreference(this@VideoplayerActivity, - SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null) - true - } - viewBinding.playButton.setIsVideoScreen(true) - viewBinding.playButton.setOnClickListener { v: View? -> onPlayPause() } - viewBinding.fastForwardButton.setOnClickListener { v: View? -> onFastForward() } - viewBinding.fastForwardButton.setOnLongClickListener { v: View? -> - SkipPreferenceDialog.showSkipPreference(this@VideoplayerActivity, - SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null) - false - } - // To suppress touches directly below the slider - viewBinding.bottomControlsContainer.setOnTouchListener { view: View?, motionEvent: MotionEvent? -> true } - viewBinding.bottomControlsContainer.fitsSystemWindows = true - viewBinding.videoView.holder.addCallback(surfaceHolderCallback) - viewBinding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - - setupVideoControlsToggler() - window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN) - - viewBinding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched) - viewBinding.videoPlayerContainer.viewTreeObserver.addOnGlobalLayoutListener { - viewBinding.videoView.setAvailableSize( - viewBinding.videoPlayerContainer.width.toFloat(), viewBinding.videoPlayerContainer.height.toFloat()) - } - } - - private val hideVideoControls = Runnable { - if (videoControlsShowing) { - Log.d(TAG, "Hiding video controls") - supportActionBar?.hide() - hideVideoControls(true) - videoControlsShowing = false - } - } - - private val onVideoviewTouched = OnTouchListener { v: View, event: MotionEvent -> - if (event.action != MotionEvent.ACTION_DOWN) { - return@OnTouchListener false - } - if (PictureInPictureUtil.isInPictureInPictureMode(this)) { - return@OnTouchListener true - } - videoControlsHider.removeCallbacks(hideVideoControls) - - if (System.currentTimeMillis() - lastScreenTap < 300) { - if (event.x > v.measuredWidth / 2.0f) { - onFastForward() - showSkipAnimation(true) - } else { - onRewind() - showSkipAnimation(false) - } - if (videoControlsShowing) { - supportActionBar?.hide() - hideVideoControls(false) - videoControlsShowing = false - } - return@OnTouchListener true - } - - toggleVideoControlsVisibility() - if (videoControlsShowing) { - setupVideoControlsToggler() - } - - lastScreenTap = System.currentTimeMillis() - true - } - - private fun showSkipAnimation(isForward: Boolean) { - val skipAnimation = AnimationSet(true) - skipAnimation.addAnimation(ScaleAnimation(1f, 2f, 1f, 2f, - Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)) - skipAnimation.addAnimation(AlphaAnimation(1f, 0f)) - skipAnimation.fillAfter = false - skipAnimation.duration = 800 - - val params = viewBinding.skipAnimationImage.layoutParams as FrameLayout.LayoutParams - if (isForward) { - viewBinding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white) - params.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL - } else { - viewBinding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white) - params.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL - } - - viewBinding.skipAnimationImage.visibility = View.VISIBLE - viewBinding.skipAnimationImage.layoutParams = params - viewBinding.skipAnimationImage.startAnimation(skipAnimation) - skipAnimation.setAnimationListener(object : Animation.AnimationListener { - override fun onAnimationStart(animation: Animation) { - } - - override fun onAnimationEnd(animation: Animation) { - viewBinding.skipAnimationImage.visibility = View.GONE - } - - override fun onAnimationRepeat(animation: Animation) { - } - }) - } - - private fun setupVideoControlsToggler() { - videoControlsHider.removeCallbacks(hideVideoControls) - videoControlsHider.postDelayed(hideVideoControls, 2500) - } - - @UnstableApi - private fun setupVideoAspectRatio() { - if (videoSurfaceCreated && controller != null) { - val videoSize = controller!!.videoSize - if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) { - Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second) - viewBinding.videoView.setVideoSize(videoSize.first, videoSize.second) - } else { - Log.e(TAG, "Could not determine video size") - } - } - } - - private fun toggleVideoControlsVisibility() { - if (videoControlsShowing) { - supportActionBar?.hide() - hideVideoControls(true) - } else { - supportActionBar?.show() - showVideoControls() - } - videoControlsShowing = !videoControlsShowing - } - - @UnstableApi - fun onRewind() { - if (controller == null) { - return - } - val curr = controller!!.position - controller!!.seekTo(curr - rewindSecs * 1000) - setupVideoControlsToggler() - } - - @UnstableApi - fun onPlayPause() { - if (controller == null) { - return - } - controller!!.playPause() - setupVideoControlsToggler() - } - - @UnstableApi - fun onFastForward() { - if (controller == null) { - return - } - val curr = controller!!.position - controller!!.seekTo(curr + fastForwardSecs * 1000) - setupVideoControlsToggler() - } - - private val surfaceHolderCallback: SurfaceHolder.Callback = object : SurfaceHolder.Callback { - override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { - holder.setFixedSize(width, height) - } - - @UnstableApi - override fun surfaceCreated(holder: SurfaceHolder) { - Log.d(TAG, "Videoview holder created") - videoSurfaceCreated = true - if (controller?.status == PlayerStatus.PLAYING) { - controller!!.setVideoSurface(holder) - } - setupVideoAspectRatio() - } - - override fun surfaceDestroyed(holder: SurfaceHolder) { - Log.d(TAG, "Videosurface was destroyed") - videoSurfaceCreated = false - if (controller != null && !destroyingDueToReload && !switchToAudioOnly) { - controller!!.notifyVideoSurfaceAbandoned() - } - } - } - - private fun showVideoControls() { - viewBinding.bottomControlsContainer.visibility = View.VISIBLE - viewBinding.controlsContainer.visibility = View.VISIBLE - val animation = AnimationUtils.loadAnimation(this, R.anim.fade_in) - if (animation != null) { - viewBinding.bottomControlsContainer.startAnimation(animation) - viewBinding.controlsContainer.startAnimation(animation) - } - viewBinding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE - } - - private fun hideVideoControls(showAnimation: Boolean) { - if (showAnimation) { - val animation = AnimationUtils.loadAnimation(this, R.anim.fade_out) - if (animation != null) { - viewBinding.bottomControlsContainer.startAnimation(animation) - viewBinding.controlsContainer.startAnimation(animation) - } - } - window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE - or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) - viewBinding.bottomControlsContainer.fitsSystemWindows = true - - viewBinding.bottomControlsContainer.visibility = View.GONE - viewBinding.controlsContainer.visibility = View.GONE - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: PlaybackPositionEvent?) { - onPositionObserverUpdate() - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onPlaybackServiceChanged(event: PlaybackServiceEvent) { - if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) { - finish() - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onMediaPlayerError(event: PlayerErrorEvent) { - MediaPlayerErrorDialog.show(this, event) - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: MessageEvent) { - Log.d(TAG, "onEvent($event)") - val errorDialog = MaterialAlertDialogBuilder(this) - errorDialog.setMessage(event.message) - errorDialog.setPositiveButton(event.actionText) { dialog: DialogInterface?, which: Int -> - event.action?.accept(this) - } - - errorDialog.show() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - super.onCreateOptionsMenu(menu) - requestCastButton(menu) - val inflater = menuInflater - inflater.inflate(R.menu.mediaplayer, menu) - return true - } - - @UnstableApi - override fun onPrepareOptionsMenu(menu: Menu): Boolean { - super.onPrepareOptionsMenu(menu) - if (controller == null) { - return false - } - val media = controller!!.getMedia() - val isFeedMedia = (media is FeedMedia) - - menu.findItem(R.id.open_feed_item).setVisible(isFeedMedia) // FeedMedia implies it belongs to a Feed - - val hasWebsiteLink = getWebsiteLinkWithFallback(media) != null - menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink) - - val isItemAndHasLink = isFeedMedia && hasLinkToShare((media as FeedMedia).getItem()) - val isItemHasDownloadLink = isFeedMedia && (media as FeedMedia?)?.download_url != null - menu.findItem(R.id.share_item).setVisible(hasWebsiteLink || isItemAndHasLink || isItemHasDownloadLink) - - menu.findItem(R.id.add_to_favorites_item).setVisible(false) - menu.findItem(R.id.remove_from_favorites_item).setVisible(false) - if (isFeedMedia) { - menu.findItem(R.id.add_to_favorites_item).setVisible(!isFavorite) - menu.findItem(R.id.remove_from_favorites_item).setVisible(isFavorite) - } - - menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller!!.sleepTimerActive()) - menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller!!.sleepTimerActive()) - - menu.findItem(R.id.player_switch_to_audio_only).setVisible(true) - - menu.findItem(R.id.audio_controls).setVisible(controller!!.audioTracks.size >= 2) - menu.findItem(R.id.playback_speed).setVisible(true) - menu.findItem(R.id.player_show_chapters).setVisible(true) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - // some options option requires FeedItem - when { - item.itemId == R.id.player_switch_to_audio_only -> { - switchToAudioOnly = true - finish() - return true - } - item.itemId == android.R.id.home -> { - val intent = Intent(this@VideoplayerActivity, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - finish() - return true - } - item.itemId == R.id.player_show_chapters -> { - ChaptersFragment().show(supportFragmentManager, ChaptersFragment.TAG) - return true - } - controller == null -> { - return false - } - else -> { - val media = controller?.getMedia() ?: return false - val feedItem = getFeedItem(media) // some options option requires FeedItem - if (item.itemId == R.id.add_to_favorites_item && feedItem != null) { - DBWriter.addFavoriteItem(feedItem) - isFavorite = true - invalidateOptionsMenu() - } else if (item.itemId == R.id.remove_from_favorites_item && feedItem != null) { - DBWriter.removeFavoriteItem(feedItem) - isFavorite = false - invalidateOptionsMenu() - } else if (item.itemId == R.id.disable_sleeptimer_item - || item.itemId == R.id.set_sleeptimer_item) { - SleepTimerDialog().show(supportFragmentManager, "SleepTimerDialog") - } else if (item.itemId == R.id.audio_controls) { - val dialog = PlaybackControlsDialog.newInstance() - dialog.show(supportFragmentManager, "playback_controls") - } else if (item.itemId == R.id.open_feed_item && feedItem != null) { - val intent = MainActivity.getIntentToOpenFeed(this, feedItem.feedId) - startActivity(intent) - } else if (item.itemId == R.id.visit_website_item) { - val url = getWebsiteLinkWithFallback(media) - if (url != null) openInBrowser(this@VideoplayerActivity, url) - } else if (item.itemId == R.id.share_item && feedItem != null) { - val shareDialog = ShareDialog.newInstance(feedItem) - shareDialog.show(supportFragmentManager, "ShareEpisodeDialog") - } else if (item.itemId == R.id.playback_speed) { - VariableSpeedDialog().show(supportFragmentManager, null) - } else { - return false - } - return true - } - } - } - - fun onPositionObserverUpdate() { - if (controller == null) { - return - } - - val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier) - val currentPosition = converter.convert(controller!!.position) - val duration = converter.convert(controller!!.duration) - val remainingTime = converter.convert( - controller!!.duration - controller!!.position) - // Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); - if (currentPosition == Playable.INVALID_TIME - || duration == Playable.INVALID_TIME) { - Log.w(TAG, "Could not react to position observer update because of invalid time") - return - } - viewBinding.positionLabel.text = getDurationStringLong(currentPosition) - if (showTimeLeft) { - viewBinding.durationLabel.text = "-" + getDurationStringLong(remainingTime) - } else { - viewBinding.durationLabel.text = getDurationStringLong(duration) - } - updateProgressbarPosition(currentPosition, duration) - } - - private fun updateProgressbarPosition(position: Int, duration: Int) { - Log.d(TAG, "updateProgressbarPosition($position, $duration)") - val progress = (position.toFloat()) / duration - viewBinding.sbPosition.progress = (progress * viewBinding.sbPosition.max).toInt() - } - - override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { - if (controller == null) { - return - } - if (fromUser) { - prog = progress / (seekBar.max.toFloat()) - val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier) - val position = converter.convert((prog * controller!!.duration).toInt()) - viewBinding.seekPositionLabel.text = getDurationStringLong(position) - } - } - - override fun onStartTrackingTouch(seekBar: SeekBar) { - viewBinding.seekCardView.scaleX = .8f - viewBinding.seekCardView.scaleY = .8f - viewBinding.seekCardView.animate() - .setInterpolator(FastOutSlowInInterpolator()) - .alpha(1f).scaleX(1f).scaleY(1f) - .setDuration(200) - .start() - videoControlsHider.removeCallbacks(hideVideoControls) - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - if (controller != null) { - controller!!.seekTo((prog * controller!!.duration).toInt()) - } - viewBinding.seekCardView.scaleX = 1f - viewBinding.seekCardView.scaleY = 1f - viewBinding.seekCardView.animate() - .setInterpolator(FastOutSlowInInterpolator()) - .alpha(0f).scaleX(.8f).scaleY(.8f) - .setDuration(200) - .start() - setupVideoControlsToggler() - } - - private fun checkFavorite() { - val feedItem = getFeedItem(controller?.getMedia()) ?: return - disposable?.dispose() - - disposable = Observable.fromCallable { DBReader.getFeedItem(feedItem.id) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { item: FeedItem? -> - if (item != null) { - val isFav = item.isTagged(FeedItem.TAG_FAVORITE) - if (isFavorite != isFav) { - isFavorite = isFav - invalidateOptionsMenu() - } - } - }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) - } - - private fun compatEnterPictureInPicture() { - if (PictureInPictureUtil.supportsPictureInPicture(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - supportActionBar?.hide() - hideVideoControls(false) - enterPictureInPictureMode() - } - } - - //Hardware keyboard support - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { - val currentFocus = currentFocus - if (currentFocus is EditText) { - return super.onKeyUp(keyCode, event) - } - - val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager - - when (keyCode) { - KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE -> { - onPlayPause() - toggleVideoControlsVisibility() - return true - } - KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_COMMA -> { - onRewind() - showSkipAnimation(false) - return true - } - KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_PERIOD -> { - onFastForward() - showSkipAnimation(true) - return true - } - KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_ESCAPE -> { - //Exit fullscreen mode - onBackPressed() - return true - } - KeyEvent.KEYCODE_I -> { - compatEnterPictureInPicture() - return true - } - KeyEvent.KEYCODE_PLUS, KeyEvent.KEYCODE_W -> { - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI) - return true - } - KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_S -> { - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI) - return true - } - KeyEvent.KEYCODE_M -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_TOGGLE_MUTE, AudioManager.FLAG_SHOW_UI) - return true - } - } - //Go to x% of video: - if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { - controller?.seekTo((0.1f * (keyCode - KeyEvent.KEYCODE_0) * controller!!.duration).toInt()) - return true - } - return super.onKeyUp(keyCode, event) - } - - companion object { - private const val TAG = "VideoplayerActivity" - - private fun getWebsiteLinkWithFallback(media: Playable?): String? { - if (media == null) { - return null - } else if (!media.getWebsiteLink().isNullOrBlank()) { - return media.getWebsiteLink() - } else if (media is FeedMedia) { - return getLinkWithFallback(media.getItem()) - } - return null - } - - private fun getFeedItem(playable: Playable?): FeedItem? { - return if (playable is FeedMedia) { - playable.getItem() - } else { - null - } - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/activity/WidgetConfigActivity.kt b/app/src/main/java/ac/mdiq/podcini/activity/WidgetConfigActivity.kt deleted file mode 100644 index f135f6d3..00000000 --- a/app/src/main/java/ac/mdiq/podcini/activity/WidgetConfigActivity.kt +++ /dev/null @@ -1,141 +0,0 @@ -package ac.mdiq.podcini.activity - -import android.appwidget.AppWidgetManager -import android.content.Intent -import android.graphics.Color -import android.os.Build -import android.os.Bundle -import android.view.View -import android.widget.CheckBox -import android.widget.SeekBar -import android.widget.SeekBar.OnSeekBarChangeListener -import android.widget.TextView -import androidx.appcompat.app.AppCompatActivity -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.preferences.ThemeSwitcher.getTheme -import ac.mdiq.podcini.core.receiver.PlayerWidget -import ac.mdiq.podcini.core.widget.WidgetUpdaterWorker - -class WidgetConfigActivity : AppCompatActivity() { - private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID - - private lateinit var widgetPreview: View - private lateinit var opacitySeekBar: SeekBar - private lateinit var opacityTextView: TextView - private lateinit var ckPlaybackSpeed: CheckBox - private lateinit var ckRewind: CheckBox - private lateinit var ckFastForward: CheckBox - private lateinit var ckSkip: CheckBox - - override fun onCreate(savedInstanceState: Bundle?) { - setTheme(getTheme(this)) - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_widget_config) - - val configIntent = intent - val extras = configIntent.extras - if (extras != null) { - appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, - AppWidgetManager.INVALID_APPWIDGET_ID) - } - - val resultValue = Intent() - resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) - setResult(RESULT_CANCELED, resultValue) - if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { - finish() - } - - opacityTextView = findViewById(R.id.widget_opacity_textView) - opacitySeekBar = findViewById(R.id.widget_opacity_seekBar) - widgetPreview = findViewById(R.id.widgetLayout) - findViewById(R.id.butConfirm).setOnClickListener { v: View? -> confirmCreateWidget() } - opacitySeekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) { - opacityTextView.text = seekBar.progress.toString() + "%" - val color = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.progress) - widgetPreview.setBackgroundColor(color) - } - - override fun onStartTrackingTouch(seekBar: SeekBar) { - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - } - }) - - widgetPreview.findViewById(R.id.txtNoPlaying).visibility = View.GONE - val title = widgetPreview.findViewById(R.id.txtvTitle) - title.visibility = View.VISIBLE - title.setText(R.string.app_name) - val progress = widgetPreview.findViewById(R.id.txtvProgress) - progress.visibility = View.VISIBLE - progress.setText(R.string.position_default_label) - - ckPlaybackSpeed = findViewById(R.id.ckPlaybackSpeed) - ckPlaybackSpeed.setOnClickListener { v: View? -> displayPreviewPanel() } - ckRewind = findViewById(R.id.ckRewind) - ckRewind.setOnClickListener { v: View? -> displayPreviewPanel() } - ckFastForward = findViewById(R.id.ckFastForward) - ckFastForward.setOnClickListener { v: View? -> displayPreviewPanel() } - ckSkip = findViewById(R.id.ckSkip) - ckSkip.setOnClickListener { v: View? -> displayPreviewPanel() } - - setInitialState() - } - - private fun setInitialState() { - val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE) - ckPlaybackSpeed.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, false) - ckRewind.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, false) - ckFastForward.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, false) - ckSkip.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, false) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - val color = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, PlayerWidget.DEFAULT_COLOR) - val opacity = Color.alpha(color) * 100 / 0xFF - - opacitySeekBar.setProgress(opacity, false) - } - displayPreviewPanel() - } - - private fun displayPreviewPanel() { - val showExtendedPreview = - ckPlaybackSpeed.isChecked || ckRewind.isChecked || ckFastForward.isChecked || ckSkip.isChecked - widgetPreview.findViewById(R.id.extendedButtonsContainer).visibility = - if (showExtendedPreview) View.VISIBLE else View.GONE - widgetPreview.findViewById(R.id.butPlay).visibility = - if (showExtendedPreview) View.GONE else View.VISIBLE - widgetPreview.findViewById(R.id.butPlaybackSpeed).visibility = - if (ckPlaybackSpeed.isChecked) View.VISIBLE else View.GONE - widgetPreview.findViewById(R.id.butFastForward).visibility = - if (ckFastForward.isChecked) View.VISIBLE else View.GONE - widgetPreview.findViewById(R.id.butSkip).visibility = - if (ckSkip.isChecked) View.VISIBLE else View.GONE - widgetPreview.findViewById(R.id.butRew).visibility = - if (ckRewind.isChecked) View.VISIBLE else View.GONE - } - - private fun confirmCreateWidget() { - val backgroundColor = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.progress) - - val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE) - val editor = prefs.edit() - editor.putInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, backgroundColor) - editor.putBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, ckPlaybackSpeed.isChecked) - editor.putBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, ckSkip.isChecked) - editor.putBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, ckRewind.isChecked) - editor.putBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, ckFastForward.isChecked) - editor.apply() - - val resultValue = Intent() - resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) - setResult(RESULT_OK, resultValue) - finish() - WidgetUpdaterWorker.enqueueWork(this) - } - - private fun getColorWithAlpha(color: Int, opacity: Int): Int { - return Math.round(0xFF * (0.01 * opacity)).toInt() * 0x1000000 + (color and 0xffffff) - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/ChaptersListAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/ChaptersListAdapter.kt deleted file mode 100644 index e342bde7..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/ChaptersListAdapter.kt +++ /dev/null @@ -1,146 +0,0 @@ -package ac.mdiq.podcini.adapter - -import android.content.Context -import android.text.TextUtils -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.Glide -import com.bumptech.glide.load.resource.bitmap.FitCenter -import com.bumptech.glide.load.resource.bitmap.RoundedCorners -import com.bumptech.glide.request.RequestOptions -import com.google.android.material.elevation.SurfaceColors -import ac.mdiq.podcini.R -import ac.mdiq.podcini.adapter.ChaptersListAdapter.ChapterHolder -import ac.mdiq.podcini.core.util.Converter.getDurationStringLocalized -import ac.mdiq.podcini.core.util.Converter.getDurationStringLong -import ac.mdiq.podcini.core.util.IntentUtils.openInBrowser -import ac.mdiq.podcini.model.feed.Chapter -import ac.mdiq.podcini.model.feed.EmbeddedChapterImage -import ac.mdiq.podcini.model.playback.Playable -import ac.mdiq.podcini.ui.common.CircularProgressBar -import kotlin.math.max -import kotlin.math.min - -class ChaptersListAdapter(private val context: Context, private val callback: Callback?) : RecyclerView.Adapter() { - - private var media: Playable? = null - private var currentChapterIndex = -1 - private var currentChapterPosition: Long = -1 - private var hasImages = false - - fun setMedia(media: Playable) { - this.media = media - hasImages = false - for (chapter in media.getChapters()) { - if (!TextUtils.isEmpty(chapter.imageUrl)) { - hasImages = true - } - } - notifyDataSetChanged() - } - - override fun onBindViewHolder(holder: ChapterHolder, position: Int) { - val sc = getItem(position)?: return - holder.title.text = sc.title - holder.start.text = getDurationStringLong(sc.start.toInt()) - val duration = if (position + 1 < itemCount) { - media!!.getChapters()[position + 1].start - sc.start - } else { - (media?.getDuration()?:0) - sc.start - } - holder.duration.text = context.getString(R.string.chapter_duration, - getDurationStringLocalized(context, duration.toInt().toLong())) - - if (TextUtils.isEmpty(sc.link)) { - holder.link.visibility = View.GONE - } else { - holder.link.visibility = View.VISIBLE - holder.link.text = sc.link - holder.link.setOnClickListener { v: View? -> - if (sc.link!=null) openInBrowser(context, sc.link!!) - } - } - holder.secondaryActionIcon.setImageResource(R.drawable.ic_play_48dp) - holder.secondaryActionButton.contentDescription = context.getString(R.string.play_chapter) - holder.secondaryActionButton.setOnClickListener { v: View? -> - callback?.onPlayChapterButtonClicked(position) - } - - if (position == currentChapterIndex) { - val density = context.resources.displayMetrics.density - holder.itemView.setBackgroundColor(SurfaceColors.getColorForElevation(context, 32 * density)) - var progress = ((currentChapterPosition - sc.start).toFloat()) / duration - progress = max(progress.toDouble(), CircularProgressBar.MINIMUM_PERCENTAGE.toDouble()).toFloat() - progress = min(progress.toDouble(), CircularProgressBar.MAXIMUM_PERCENTAGE.toDouble()).toFloat() - holder.progressBar.setPercentage(progress, position) - holder.secondaryActionIcon.setImageResource(R.drawable.ic_replay) - } else { - holder.itemView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent)) - holder.progressBar.setPercentage(0f, null) - } - - if (hasImages) { - holder.image.visibility = View.VISIBLE - if (TextUtils.isEmpty(sc.imageUrl)) { - Glide.with(context).clear(holder.image) - } else { - if (media != null) Glide.with(context) - .load(EmbeddedChapterImage.getModelFor(media!!, position)) - .apply(RequestOptions() - .dontAnimate() - .transform(FitCenter(), RoundedCorners((4 * context.resources.displayMetrics.density).toInt()))) - .into(holder.image) - } - } else { - holder.image.visibility = View.GONE - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChapterHolder { - val inflater = LayoutInflater.from(context) - return ChapterHolder(inflater.inflate(R.layout.simplechapter_item, parent, false)) - } - - override fun getItemCount(): Int { - return media?.getChapters()?.size?:0 - } - - class ChapterHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val title: TextView = itemView.findViewById(R.id.txtvTitle) - val start: TextView = itemView.findViewById(R.id.txtvStart) - val link: TextView = itemView.findViewById(R.id.txtvLink) - val duration: TextView = itemView.findViewById(R.id.txtvDuration) - val image: ImageView = itemView.findViewById(R.id.imgvCover) - val secondaryActionButton: View = itemView.findViewById(R.id.secondaryActionButton) - val secondaryActionIcon: ImageView = itemView.findViewById(R.id.secondaryActionIcon) - val progressBar: CircularProgressBar = itemView.findViewById(R.id.secondaryActionProgress) - } - - fun notifyChapterChanged(newChapterIndex: Int) { - currentChapterIndex = newChapterIndex - currentChapterPosition = getItem(newChapterIndex)?.start?:0 - notifyDataSetChanged() - } - - fun notifyTimeChanged(timeMs: Long) { - currentChapterPosition = timeMs - // Passing an argument prevents flickering. - // See EpisodeItemListAdapter.notifyItemChangedCompat. - notifyItemChanged(currentChapterIndex, "foo") - } - - fun getItem(position: Int): Chapter? { - val chapters = media?.getChapters()?: return null - if (position < 0 || position >= chapters.size) return null - return chapters[position] - } - - interface Callback { - fun onPlayChapterButtonClicked(position: Int) - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/CoverLoader.kt b/app/src/main/java/ac/mdiq/podcini/adapter/CoverLoader.kt deleted file mode 100644 index 9394bc42..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/CoverLoader.kt +++ /dev/null @@ -1,122 +0,0 @@ -package ac.mdiq.podcini.adapter - -import ac.mdiq.podcini.activity.MainActivity -import android.graphics.drawable.Drawable -import android.view.View -import android.widget.ImageView -import android.widget.TextView -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestBuilder -import com.bumptech.glide.request.RequestOptions -import com.bumptech.glide.request.target.CustomViewTarget -import com.bumptech.glide.request.transition.Transition -import java.lang.ref.WeakReference - -class CoverLoader(activity: MainActivity) { - private var resource = 0 - private var uri: String? = null - private var fallbackUri: String? = null - private var imgvCover: ImageView? = null - private var textAndImageCombined = false - private var fallbackTitle: TextView? = null - - fun withUri(uri: String?): CoverLoader { - this.uri = uri - return this - } - - fun withResource(resource: Int): CoverLoader { - this.resource = resource - return this - } - - fun withFallbackUri(uri: String?): CoverLoader { - fallbackUri = uri - return this - } - - fun withCoverView(coverView: ImageView): CoverLoader { - imgvCover = coverView - return this - } - - fun withPlaceholderView(title: TextView): CoverLoader { - this.fallbackTitle = title - return this - } - - /** - * Set cover text and if it should be shown even if there is a cover image. - * @param fallbackTitle Fallback title text - * @param textAndImageCombined Show cover text even if there is a cover image? - */ - fun withPlaceholderView(fallbackTitle: TextView?, textAndImageCombined: Boolean): CoverLoader { - this.fallbackTitle = fallbackTitle - this.textAndImageCombined = textAndImageCombined - return this - } - - fun load() { - if (imgvCover == null) return - - val coverTarget = CoverTarget(fallbackTitle, imgvCover!!, textAndImageCombined) - - if (resource != 0) { - Glide.with(imgvCover!!).clear(coverTarget) - imgvCover!!.setImageResource(resource) - CoverTarget.setTitleVisibility(fallbackTitle, textAndImageCombined) - return - } - - val options: RequestOptions = RequestOptions() - .fitCenter() - .dontAnimate() - - var builder: RequestBuilder = Glide.with(imgvCover!!) - .`as`(Drawable::class.java) - .load(uri) - .apply(options) - - if (fallbackUri != null) { - builder = builder.error(Glide.with(imgvCover!!) - .`as`(Drawable::class.java) - .load(fallbackUri) - .apply(options)) - } - - builder.into(coverTarget) - } - - internal class CoverTarget(fallbackTitle: TextView?, - coverImage: ImageView, - private val textAndImageCombined: Boolean - ) : CustomViewTarget(coverImage) { - - private val fallbackTitle: WeakReference = WeakReference(fallbackTitle) - private val cover: WeakReference = WeakReference(coverImage) - - override fun onLoadFailed(errorDrawable: Drawable?) { - setTitleVisibility(fallbackTitle.get(), true) - } - - override fun onResourceReady(resource: Drawable, - transition: Transition? - ) { - val ivCover = cover.get() - ivCover!!.setImageDrawable(resource) - setTitleVisibility(fallbackTitle.get(), textAndImageCombined) - } - - override fun onResourceCleared(placeholder: Drawable?) { - val ivCover = cover.get() - ivCover!!.setImageDrawable(placeholder) - setTitleVisibility(fallbackTitle.get(), textAndImageCombined) - } - - companion object { - fun setTitleVisibility(fallbackTitle: TextView?, textAndImageCombined: Boolean) { - fallbackTitle?.visibility = if (textAndImageCombined) View.VISIBLE else View.GONE - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/DataFolderAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/DataFolderAdapter.kt deleted file mode 100644 index 6ea6972e..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/DataFolderAdapter.kt +++ /dev/null @@ -1,112 +0,0 @@ -package ac.mdiq.podcini.adapter - -import android.content.Context -import android.text.format.Formatter -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ProgressBar -import android.widget.RadioButton -import android.widget.TextView -import androidx.core.util.Consumer -import androidx.recyclerview.widget.RecyclerView -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.util.StorageUtils.getFreeSpaceAvailable -import ac.mdiq.podcini.core.util.StorageUtils.getTotalSpaceAvailable -import ac.mdiq.podcini.storage.preferences.UserPreferences.getDataFolder -import java.io.File - -class DataFolderAdapter(context: Context, selectionHandler: Consumer) : RecyclerView.Adapter() { - - private val selectionHandler: Consumer - private val currentPath: String? - private val entries: List - private val freeSpaceString: String - - init { - this.entries = getStorageEntries(context) - this.currentPath = getCurrentPath() - this.selectionHandler = selectionHandler - this.freeSpaceString = context.getString(R.string.choose_data_directory_available_space) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(parent.context) - val entryView = inflater.inflate(R.layout.choose_data_folder_dialog_entry, parent, false) - return ViewHolder(entryView) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val storagePath = entries[position] - val context = holder.root.context - val freeSpace = Formatter.formatShortFileSize(context, storagePath.availableSpace) - val totalSpace = Formatter.formatShortFileSize(context, storagePath.totalSpace) - - holder.path.text = storagePath.shortPath - holder.size.text = String.format(freeSpaceString, freeSpace, totalSpace) - holder.progressBar.progress = storagePath.usagePercentage - val selectListener = View.OnClickListener { v: View? -> - selectionHandler.accept( - storagePath.fullPath) - } - holder.root.setOnClickListener(selectListener) - holder.radioButton.setOnClickListener(selectListener) - - if (storagePath.fullPath == currentPath) { - holder.radioButton.toggle() - } - } - - override fun getItemCount(): Int { - return entries.size - } - - private fun getCurrentPath(): String? { - val dataFolder = getDataFolder(null) - return dataFolder?.absolutePath - } - - private fun getStorageEntries(context: Context): List { - val mediaDirs = context.getExternalFilesDirs(null) - val entries: MutableList = ArrayList(mediaDirs.size) - for (dir in mediaDirs) { - if (!isWritable(dir)) { - continue - } - entries.add(StoragePath(dir.absolutePath)) - } - if (entries.isEmpty() && isWritable(context.filesDir)) { - entries.add(StoragePath(context.filesDir.absolutePath)) - } - return entries - } - - private fun isWritable(dir: File?): Boolean { - return dir != null && dir.exists() && dir.canRead() && dir.canWrite() - } - - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val root: View = itemView.findViewById(R.id.root) - val path: TextView = itemView.findViewById(R.id.path) - val size: TextView = itemView.findViewById(R.id.size) - val radioButton: RadioButton = itemView.findViewById(R.id.radio_button) - val progressBar: ProgressBar = itemView.findViewById(R.id.used_space) - } - - internal class StoragePath(val fullPath: String) { - val shortPath: String - get() { - val prefixIndex = fullPath.indexOf("Android") - return if ((prefixIndex > 0)) fullPath.substring(0, prefixIndex) else fullPath - } - - val availableSpace: Long - get() = getFreeSpaceAvailable(fullPath) - - val totalSpace: Long - get() = getTotalSpaceAvailable(fullPath) - - val usagePercentage: Int - get() = 100 - (100 * availableSpace / totalSpace.toFloat()).toInt() - } -} \ No newline at end of file diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/DownloadLogAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/DownloadLogAdapter.kt deleted file mode 100644 index 71046047..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/DownloadLogAdapter.kt +++ /dev/null @@ -1,149 +0,0 @@ -package ac.mdiq.podcini.adapter - -import ac.mdiq.podcini.activity.MainActivity -import android.app.Activity -import android.text.format.DateUtils -import android.util.Log -import android.view.View -import android.view.ViewGroup -import android.widget.BaseAdapter -import android.widget.Toast -import ac.mdiq.podcini.R -import ac.mdiq.podcini.adapter.actionbutton.DownloadActionButton -import ac.mdiq.podcini.core.storage.DBReader -import ac.mdiq.podcini.core.util.DownloadErrorLabel -import ac.mdiq.podcini.core.util.download.FeedUpdateManager -import ac.mdiq.podcini.model.download.DownloadError -import ac.mdiq.podcini.model.download.DownloadResult -import ac.mdiq.podcini.model.feed.Feed -import ac.mdiq.podcini.model.feed.FeedMedia -import ac.mdiq.podcini.ui.common.ThemeUtils -import ac.mdiq.podcini.view.viewholder.DownloadLogItemViewHolder -import androidx.media3.common.util.UnstableApi - -/** - * Displays a list of DownloadStatus entries. - */ -class DownloadLogAdapter(private val context: Activity) : BaseAdapter() { - private var downloadLog: List = ArrayList() - - fun setDownloadLog(downloadLog: List) { - this.downloadLog = downloadLog - notifyDataSetChanged() - } - - @UnstableApi override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val holder: DownloadLogItemViewHolder - if (convertView == null) { - holder = DownloadLogItemViewHolder(context, parent) - holder.itemView.tag = holder - } else { - holder = convertView.tag as DownloadLogItemViewHolder - } - val item = getItem(position) - if (item != null) bind(holder, item, position) - return holder.itemView - } - - @UnstableApi private fun bind(holder: DownloadLogItemViewHolder, status: DownloadResult, position: Int) { - var statusText: String? = "" - if (status.feedfileType == Feed.FEEDFILETYPE_FEED) { - statusText += context.getString(R.string.download_type_feed) - } else if (status.feedfileType == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - statusText += context.getString(R.string.download_type_media) - } - statusText += " · " - statusText += DateUtils.getRelativeTimeSpanString(status.getCompletionDate().time, - System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, 0) - holder.status.text = statusText - - if (status.title.isNotEmpty()) { - holder.title.text = status.title - } else { - holder.title.setText(R.string.download_log_title_unknown) - } - - if (status.isSuccessful) { - holder.icon.setTextColor(ThemeUtils.getColorFromAttr(context, R.attr.icon_green)) - holder.icon.text = "{fa-check-circle}" - holder.icon.setContentDescription(context.getString(R.string.download_successful)) - holder.secondaryActionButton.visibility = View.INVISIBLE - holder.reason.visibility = View.GONE - holder.tapForDetails.visibility = View.GONE - } else { - if (status.reason == DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE) { - holder.icon.setTextColor(ThemeUtils.getColorFromAttr(context, R.attr.icon_yellow)) - holder.icon.text = "{fa-exclamation-circle}" - } else { - holder.icon.setTextColor(ThemeUtils.getColorFromAttr(context, R.attr.icon_red)) - holder.icon.text = "{fa-times-circle}" - } - holder.icon.setContentDescription(context.getString(R.string.error_label)) - holder.reason.setText(DownloadErrorLabel.from(status.reason)) - holder.reason.visibility = View.VISIBLE - holder.tapForDetails.visibility = View.VISIBLE - - if (newerWasSuccessful(position, status.feedfileType, status.feedfileId)) { - holder.secondaryActionButton.visibility = View.INVISIBLE - holder.secondaryActionButton.setOnClickListener(null) - holder.secondaryActionButton.tag = null - } else { - holder.secondaryActionIcon.setImageResource(R.drawable.ic_refresh) - holder.secondaryActionButton.visibility = View.VISIBLE - - if (status.feedfileType == Feed.FEEDFILETYPE_FEED) { - holder.secondaryActionButton.setOnClickListener(View.OnClickListener setOnClickListener@{ v: View? -> - holder.secondaryActionButton.visibility = View.INVISIBLE - val feed: Feed? = DBReader.getFeed(status.feedfileId) - if (feed == null) { - Log.e(TAG, "Could not find feed for feed id: " + status.feedfileId) - return@setOnClickListener - } - FeedUpdateManager.runOnce(context, feed) - }) - } else if (status.feedfileType == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - holder.secondaryActionButton.setOnClickListener(View.OnClickListener { v: View? -> - holder.secondaryActionButton.visibility = View.INVISIBLE - val media: FeedMedia? = DBReader.getFeedMedia(status.feedfileId) - if (media == null) { - Log.e(TAG, "Could not find feed media for feed id: " + status.feedfileId) - return@OnClickListener - } - if (media.getItem() != null) DownloadActionButton(media.getItem()!!).onClick(context) - (context as MainActivity).showSnackbarAbovePlayer( - R.string.status_downloading_label, Toast.LENGTH_SHORT) - }) - } - } - } - } - - private fun newerWasSuccessful(downloadStatusIndex: Int, feedTypeId: Int, id: Long): Boolean { - for (i in 0 until downloadStatusIndex) { - val status: DownloadResult = downloadLog[i] - if (status.feedfileType == feedTypeId && status.feedfileId == id && status.isSuccessful) { - return true - } - } - return false - } - - override fun getCount(): Int { - return downloadLog.size - } - - override fun getItem(position: Int): DownloadResult? { - if (position in downloadLog.indices) { - return downloadLog[position] - } - return null - } - - override fun getItemId(position: Int): Long { - return position.toLong() - } - - companion object { - private const val TAG = "DownloadLogAdapter" - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/EpisodeItemListAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/EpisodeItemListAdapter.kt deleted file mode 100644 index 032bb9ce..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/EpisodeItemListAdapter.kt +++ /dev/null @@ -1,213 +0,0 @@ -package ac.mdiq.podcini.adapter - -import ac.mdiq.podcini.R -import ac.mdiq.podcini.activity.MainActivity -import ac.mdiq.podcini.core.util.FeedItemUtil -import ac.mdiq.podcini.fragment.ItemPagerFragment -import ac.mdiq.podcini.menuhandler.FeedItemMenuHandler -import ac.mdiq.podcini.model.feed.FeedItem -import ac.mdiq.podcini.ui.common.ThemeUtils -import ac.mdiq.podcini.view.viewholder.EpisodeItemViewHolder -import android.R.color -import android.app.Activity -import android.os.Build -import android.view.* -import androidx.media3.common.util.UnstableApi -import androidx.recyclerview.widget.RecyclerView -import org.apache.commons.lang3.ArrayUtils -import java.lang.ref.WeakReference - - -/** - * List adapter for the list of new episodes. - */ -open class EpisodeItemListAdapter(mainActivity: MainActivity) : SelectableAdapter(mainActivity), - View.OnCreateContextMenuListener { - - private val mainActivityRef: WeakReference = WeakReference(mainActivity) - private var episodes: List = ArrayList() - var longPressedItem: FeedItem? = null - var longPressedPosition: Int = 0 // used to init actionMode - private var dummyViews = 0 - - init { - setHasStableIds(true) - } - - fun setDummyViews(dummyViews: Int) { - this.dummyViews = dummyViews - notifyDataSetChanged() - } - - fun updateItems(items: List) { - episodes = items - notifyDataSetChanged() - updateTitle() - } - - override fun getItemViewType(position: Int): Int { - return R.id.view_type_episode_item - } - - @UnstableApi override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeItemViewHolder { - return EpisodeItemViewHolder(mainActivityRef.get()!!, parent) - } - - @UnstableApi override fun onBindViewHolder(holder: EpisodeItemViewHolder, pos: Int) { - if (pos >= episodes.size || pos < 0) { - beforeBindViewHolder(holder, pos) - holder.bindDummy() - afterBindViewHolder(holder, pos) - holder.hideSeparatorIfNecessary() - return - } - - // Reset state of recycled views - holder.coverHolder.visibility = View.VISIBLE - holder.dragHandle.setVisibility(View.GONE) - - beforeBindViewHolder(holder, pos) - - val item: FeedItem = episodes[pos] - holder.bind(item) - - holder.itemView.setOnClickListener { v: View? -> - val activity: MainActivity? = mainActivityRef.get() - if (!inActionMode()) { - val ids: LongArray = FeedItemUtil.getIds(episodes) - val position = ArrayUtils.indexOf(ids, item.id) - activity?.loadChildFragment(ItemPagerFragment.newInstance(ids, position)) - } else { - toggleSelection(holder.bindingAdapterPosition) - } - } - holder.itemView.setOnCreateContextMenuListener(this) - holder.itemView.setOnLongClickListener { v: View? -> - longPressedItem = item - longPressedPosition = holder.bindingAdapterPosition - false - } - holder.itemView.setOnTouchListener(View.OnTouchListener { v: View?, e: MotionEvent -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (e.isFromSource(InputDevice.SOURCE_MOUSE) && e.buttonState == MotionEvent.BUTTON_SECONDARY) { - longPressedItem = item - longPressedPosition = holder.bindingAdapterPosition - return@OnTouchListener false - } - } - false - }) - - if (inActionMode()) { - holder.secondaryActionButton.setOnClickListener(null) - if (isSelected(pos)) { - holder.itemView.setBackgroundColor(-0x78000000 - + (0xffffff and ThemeUtils.getColorFromAttr(mainActivityRef.get()!!, R.attr.colorAccent))) - } else { - holder.itemView.setBackgroundResource(color.transparent) - } - } - - afterBindViewHolder(holder, pos) - holder.hideSeparatorIfNecessary() - } - - protected open fun beforeBindViewHolder(holder: EpisodeItemViewHolder, pos: Int) { - } - - protected open fun afterBindViewHolder(holder: EpisodeItemViewHolder, pos: Int) { - } - - @UnstableApi override fun onViewRecycled(holder: EpisodeItemViewHolder) { - super.onViewRecycled(holder) - // Set all listeners to null. This is required to prevent leaking fragments that have set a listener. - // Activity -> recycledViewPool -> EpisodeItemViewHolder -> Listener -> Fragment (can not be garbage collected) - holder.itemView.setOnClickListener(null) - holder.itemView.setOnCreateContextMenuListener(null) - holder.itemView.setOnLongClickListener(null) - holder.itemView.setOnTouchListener(null) - holder.secondaryActionButton.setOnClickListener(null) - holder.dragHandle.setOnTouchListener(null) - holder.coverHolder.setOnTouchListener(null) - } - - /** - * [.notifyItemChanged] is final, so we can not override. - * Calling [.notifyItemChanged] may bind the item to a new ViewHolder and execute a transition. - * This causes flickering and breaks the download animation that stores the old progress in the View. - * Instead, we tell the adapter to use partial binding by calling [.notifyItemChanged]. - * We actually ignore the payload and always do a full bind but calling the partial bind method ensures - * that ViewHolders are always re-used. - * - * @param position Position of the item that has changed - */ - fun notifyItemChangedCompat(position: Int) { - notifyItemChanged(position, "foo") - } - - override fun getItemId(position: Int): Long { -// if (position >= episodes.size) { -// return RecyclerView.NO_ID // Dummy views -// } -// val item = episodes[position] -// return item.id ?: RecyclerView.NO_POSITION.toLong() - return getItem(position)?.id ?: RecyclerView.NO_ID - } - - override fun getItemCount(): Int { - return dummyViews + episodes.size - } - - protected fun getItem(index: Int): FeedItem? { -// return episodes[index] - return if (index in episodes.indices) episodes[index] else null - } - - protected val activity: Activity? - get() = mainActivityRef.get() - - @UnstableApi override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { - val inflater: MenuInflater = activity!!.menuInflater - if (inActionMode()) { - inflater.inflate(R.menu.multi_select_context_popup, menu) - } else { - if (longPressedItem == null) { - return - } - inflater.inflate(R.menu.feeditemlist_context, menu) - menu.setHeaderTitle(longPressedItem!!.title) - FeedItemMenuHandler.onPrepareMenu(menu, longPressedItem, R.id.skip_episode_item) - } - } - - fun onContextItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.multi_select -> { - startSelectMode(longPressedPosition) - return true - } - R.id.select_all_above -> { - setSelected(0, longPressedPosition, true) - return true - } - R.id.select_all_below -> { - shouldSelectLazyLoadedItems = true - setSelected(longPressedPosition + 1, itemCount, true) - return true - } - else -> return false - } - } - - val selectedItems: List - get() { - val items: MutableList = ArrayList() - for (i in 0 until itemCount) { - if (i < episodes.size && isSelected(i)) { - val item = getItem(i) - if (item != null) items.add(item) - } - } - return items - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/FeedDiscoverAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/FeedDiscoverAdapter.kt deleted file mode 100644 index 5ceb3be7..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/FeedDiscoverAdapter.kt +++ /dev/null @@ -1,68 +0,0 @@ -package ac.mdiq.podcini.adapter - -import ac.mdiq.podcini.activity.MainActivity -import android.view.View -import android.view.ViewGroup -import android.widget.BaseAdapter -import android.widget.ImageView -import com.bumptech.glide.Glide -import com.bumptech.glide.load.resource.bitmap.FitCenter -import com.bumptech.glide.load.resource.bitmap.RoundedCorners -import com.bumptech.glide.request.RequestOptions -import ac.mdiq.podcini.R -import ac.mdiq.podcini.net.discovery.PodcastSearchResult -import java.lang.ref.WeakReference - -class FeedDiscoverAdapter(mainActivity: MainActivity) : BaseAdapter() { - private val mainActivityRef: WeakReference = WeakReference(mainActivity) - private val data: MutableList = ArrayList() - - fun updateData(newData: List) { - data.clear() - data.addAll(newData) - notifyDataSetChanged() - } - - override fun getCount(): Int { - return data.size - } - - override fun getItem(position: Int): PodcastSearchResult? { - return if (position in data.indices) data[position] else null - } - - override fun getItemId(position: Int): Long { - return 0 - } - - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - var convertView = convertView - val holder: Holder - - if (convertView == null) { - convertView = View.inflate(mainActivityRef.get(), R.layout.quick_feed_discovery_item, null) - holder = Holder() - holder.imageView = convertView.findViewById(R.id.discovery_cover) - convertView.tag = holder - } else { - holder = convertView.tag as Holder - } - - val podcast: PodcastSearchResult? = getItem(position) - holder.imageView!!.contentDescription = podcast?.title - - Glide.with(mainActivityRef.get()!!) - .load(podcast?.imageUrl) - .apply(RequestOptions() - .placeholder(R.color.light_gray) - .transform(FitCenter(), RoundedCorners((8 * mainActivityRef.get()!!.resources.displayMetrics.density).toInt())) - .dontAnimate()) - .into(holder.imageView!!) - - return convertView!! - } - - internal class Holder { - var imageView: ImageView? = null - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/FeedItemlistDescriptionAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/FeedItemlistDescriptionAdapter.kt deleted file mode 100644 index a679dc4f..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/FeedItemlistDescriptionAdapter.kt +++ /dev/null @@ -1,104 +0,0 @@ -package ac.mdiq.podcini.adapter - -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.service.playback.PlaybackService.Companion.getPlayerActivityIntent -import ac.mdiq.podcini.core.util.DateFormatter.formatAbbrev -import ac.mdiq.podcini.core.util.NetworkUtils.isStreamingAllowed -import ac.mdiq.podcini.core.util.playback.PlaybackServiceStarter -import ac.mdiq.podcini.core.util.syndication.HtmlToPlainText -import ac.mdiq.podcini.dialog.StreamingConfirmationDialog -import ac.mdiq.podcini.model.feed.FeedItem -import ac.mdiq.podcini.model.playback.MediaType -import ac.mdiq.podcini.model.playback.Playable -import ac.mdiq.podcini.model.playback.RemoteMedia -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import android.widget.Button -import android.widget.TextView -import androidx.media3.common.util.UnstableApi - -/** - * List adapter for showing a list of FeedItems with their title and description. - */ -class FeedItemlistDescriptionAdapter(context: Context, resource: Int, objects: List?) : - ArrayAdapter(context, resource, objects!!) { - @UnstableApi override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - var convertView = convertView - val holder: Holder - - val item = getItem(position) - - // Inflate layout - if (convertView == null) { - holder = Holder() - val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - convertView = inflater.inflate(R.layout.itemdescription_listitem, parent, false) - holder.title = convertView.findViewById(R.id.txtvTitle) - holder.pubDate = convertView.findViewById(R.id.txtvPubDate) - holder.description = convertView.findViewById(R.id.txtvDescription) - holder.preview = convertView.findViewById(R.id.butPreview) - - convertView.tag = holder - } else { - holder = convertView.tag as Holder - } - - holder.title!!.text = item!!.title - holder.pubDate!!.text = formatAbbrev(context, item.pubDate) - if (item.description != null) { - val description = HtmlToPlainText.getPlainText(item.description!!) - .replace("\n".toRegex(), " ") - .replace("\\s+".toRegex(), " ") - .trim { it <= ' ' } - holder.description!!.text = description - holder.description!!.maxLines = MAX_LINES_COLLAPSED - } - holder.description!!.tag = false - holder.preview!!.visibility = View.GONE - holder.preview!!.setOnClickListener { v: View? -> - if (item.media == null) { - return@setOnClickListener - } - val playable: Playable = RemoteMedia(item) - if (!isStreamingAllowed) { - StreamingConfirmationDialog(context, playable).show() - return@setOnClickListener - } - - PlaybackServiceStarter(context, playable) - .callEvenIfRunning(true) - .start() - if (playable.getMediaType() == MediaType.VIDEO) { - context.startActivity(getPlayerActivityIntent(context, playable)) - } - } - convertView!!.setOnClickListener { v: View? -> - if (holder.description!!.tag == true) { - holder.description!!.maxLines = MAX_LINES_COLLAPSED - holder.preview!!.visibility = View.GONE - holder.description!!.tag = false - } else { - holder.description!!.maxLines = 30 - holder.description!!.tag = true - - holder.preview!!.visibility = if (item.media != null) View.VISIBLE else View.GONE - holder.preview!!.setText(R.string.preview_episode) - } - } - return convertView - } - - internal class Holder { - var title: TextView? = null - var pubDate: TextView? = null - var description: TextView? = null - var preview: Button? = null - } - - companion object { - private const val MAX_LINES_COLLAPSED = 2 - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/HorizontalFeedListAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/HorizontalFeedListAdapter.kt deleted file mode 100644 index f1cba636..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/HorizontalFeedListAdapter.kt +++ /dev/null @@ -1,125 +0,0 @@ -package ac.mdiq.podcini.adapter - -import ac.mdiq.podcini.activity.MainActivity -import android.view.ContextMenu -import android.view.MenuInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import androidx.annotation.StringRes -import androidx.cardview.widget.CardView -import androidx.media3.common.util.UnstableApi -import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.Glide -import com.bumptech.glide.request.RequestOptions -import ac.mdiq.podcini.R -import ac.mdiq.podcini.fragment.FeedItemlistFragment -import ac.mdiq.podcini.model.feed.Feed -import ac.mdiq.podcini.ui.common.SquareImageView -import java.lang.ref.WeakReference - -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 - - 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 - holder.actionButton.visibility = View.VISIBLE - holder.actionButton.setText(endButtonText) - holder.actionButton.setOnClickListener { v: View? -> endButtonAction!!.run() } - return - } - holder.cardView.visibility = View.VISIBLE - holder.actionButton.visibility = View.GONE - if (position >= data.size) { - holder.itemView.alpha = 0.1f - Glide.with(mainActivityRef.get()!!).clear(holder.imageView) - 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 { v: View? -> - mainActivityRef.get()?.loadChildFragment(FeedItemlistFragment.newInstance(podcast.id)) - } - - holder.imageView.setOnCreateContextMenuListener(this) - holder.imageView.setOnLongClickListener { v: View? -> - val currentItemPosition = holder.bindingAdapterPosition - longPressedItem = data[currentItemPosition] - false - } - - Glide.with(mainActivityRef.get()!!) - .load(podcast.imageUrl) - .apply(RequestOptions() - .placeholder(R.color.light_gray) - .fitCenter() - .dontAnimate()) - .into(holder.imageView) - } - - 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) { - var imageView: SquareImageView = itemView.findViewById(R.id.discovery_cover) - var cardView: CardView - var actionButton: Button - - init { - imageView.setDirection(SquareImageView.DIRECTION_HEIGHT) - actionButton = itemView.findViewById(R.id.actionButton) - cardView = itemView.findViewById(R.id.cardView) - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/HorizontalItemListAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/HorizontalItemListAdapter.kt deleted file mode 100644 index 8dfcdf7e..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/HorizontalItemListAdapter.kt +++ /dev/null @@ -1,120 +0,0 @@ -package ac.mdiq.podcini.adapter - -import ac.mdiq.podcini.activity.MainActivity -import android.view.ContextMenu -import android.view.MenuInflater -import android.view.View -import android.view.ViewGroup -import androidx.media3.common.util.UnstableApi -import androidx.recyclerview.widget.RecyclerView -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.util.FeedItemUtil -import ac.mdiq.podcini.fragment.ItemPagerFragment -import ac.mdiq.podcini.menuhandler.FeedItemMenuHandler -import ac.mdiq.podcini.model.feed.FeedItem -import ac.mdiq.podcini.view.viewholder.HorizontalItemViewHolder -import org.apache.commons.lang3.ArrayUtils -import java.lang.ref.WeakReference - -open class HorizontalItemListAdapter(mainActivity: MainActivity) : RecyclerView.Adapter(), - View.OnCreateContextMenuListener { - - private val mainActivityRef: WeakReference = WeakReference(mainActivity) - private var data: List = ArrayList() - var longPressedItem: FeedItem? = null - private var dummyViews = 0 - - init { - setHasStableIds(true) - } - - fun setDummyViews(dummyViews: Int) { - this.dummyViews = dummyViews - } - - fun updateData(newData: List) { - data = newData - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HorizontalItemViewHolder { - return HorizontalItemViewHolder(mainActivityRef.get()!!, parent) - } - - @UnstableApi override fun onBindViewHolder(holder: HorizontalItemViewHolder, position: Int) { - if (position >= data.size) { - holder.bindDummy() - return - } - - val item: FeedItem = data[position] - holder.bind(item) - - holder.card.setOnCreateContextMenuListener(this) - holder.card.setOnLongClickListener { v: View? -> - longPressedItem = item - false - } - holder.secondaryActionIcon.setOnCreateContextMenuListener(this) - holder.secondaryActionIcon.setOnLongClickListener { v: View? -> - longPressedItem = item - false - } - holder.card.setOnClickListener { v: View? -> - val activity: MainActivity? = mainActivityRef.get() - if (activity != null) { - val ids: LongArray = FeedItemUtil.getIds(data) - val clickPosition = ArrayUtils.indexOf(ids, item.id) - activity.loadChildFragment(ItemPagerFragment.newInstance(ids, clickPosition)) - } - } - } - - override fun getItemId(position: Int): Long { - if (position in data.indices) { - val item: FeedItem = data[position] - return item.id - } - return RecyclerView.NO_ID // Dummy views - } - - override fun getItemCount(): Int { - return dummyViews + data.size - } - - override fun onViewRecycled(holder: HorizontalItemViewHolder) { - super.onViewRecycled(holder) - // Set all listeners to null. This is required to prevent leaking fragments that have set a listener. - // Activity -> recycledViewPool -> ViewHolder -> Listener -> Fragment (can not be garbage collected) - holder.card.setOnClickListener(null) - holder.card.setOnCreateContextMenuListener(null) - holder.card.setOnLongClickListener(null) - holder.secondaryActionIcon.setOnClickListener(null) - holder.secondaryActionIcon.setOnCreateContextMenuListener(null) - holder.secondaryActionIcon.setOnLongClickListener(null) - } - - /** - * [.notifyItemChanged] is final, so we can not override. - * Calling [.notifyItemChanged] may bind the item to a new ViewHolder and execute a transition. - * This causes flickering and breaks the download animation that stores the old progress in the View. - * Instead, we tell the adapter to use partial binding by calling [.notifyItemChanged]. - * We actually ignore the payload and always do a full bind but calling the partial bind method ensures - * that ViewHolders are always re-used. - * - * @param position Position of the item that has changed - */ - fun notifyItemChangedCompat(position: Int) { - notifyItemChanged(position, "foo") - } - - @UnstableApi override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { - val inflater: MenuInflater = mainActivityRef.get()!!.menuInflater - if (longPressedItem == null) return - - menu.clear() - inflater.inflate(R.menu.feeditemlist_context, menu) - menu.setHeaderTitle(longPressedItem!!.title) - FeedItemMenuHandler.onPrepareMenu(menu, longPressedItem, R.id.skip_episode_item) - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/NavListAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/NavListAdapter.kt deleted file mode 100644 index f869de4d..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/NavListAdapter.kt +++ /dev/null @@ -1,376 +0,0 @@ -package ac.mdiq.podcini.adapter - -import android.app.Activity -import android.content.DialogInterface -import android.content.Intent -import android.content.SharedPreferences -import android.content.SharedPreferences.OnSharedPreferenceChangeListener -import android.os.Build -import android.view.* -import android.view.ContextMenu.ContextMenuInfo -import android.view.View.OnCreateContextMenuListener -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.RelativeLayout -import android.widget.TextView -import androidx.annotation.DrawableRes -import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.Glide -import com.bumptech.glide.load.resource.bitmap.FitCenter -import com.bumptech.glide.load.resource.bitmap.RoundedCorners -import com.bumptech.glide.request.RequestOptions -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.activity.PreferenceActivity -import ac.mdiq.podcini.core.storage.NavDrawerData.* -import ac.mdiq.podcini.fragment.* -import ac.mdiq.podcini.storage.preferences.UserPreferences -import ac.mdiq.podcini.storage.preferences.UserPreferences.episodeCacheSize -import ac.mdiq.podcini.storage.preferences.UserPreferences.hiddenDrawerItems -import ac.mdiq.podcini.storage.preferences.UserPreferences.isEnableAutodownload -import ac.mdiq.podcini.storage.preferences.UserPreferences.subscriptionsFilter -import ac.mdiq.podcini.ui.home.HomeFragment -import androidx.media3.common.util.UnstableApi -import org.apache.commons.lang3.ArrayUtils -import java.lang.ref.WeakReference -import java.text.NumberFormat -import java.util.* -import kotlin.math.abs - -/** - * BaseAdapter for the navigation drawer - */ -class NavListAdapter(private val itemAccess: ItemAccess, context: Activity) : - RecyclerView.Adapter(), OnSharedPreferenceChangeListener { - - private val fragmentTags: MutableList = ArrayList() - private val titles: Array = context.resources.getStringArray(R.array.nav_drawer_titles) - private val activity = WeakReference(context) - @JvmField - var showSubscriptionList: Boolean = true - - init { - loadItems() - - val prefs = PreferenceManager.getDefaultSharedPreferences(context) - prefs.registerOnSharedPreferenceChangeListener(this) - } - - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { - if (UserPreferences.PREF_HIDDEN_DRAWER_ITEMS == key) { - loadItems() - } - } - - private fun loadItems() { - val newTags: MutableList = ArrayList(listOf(*NavDrawerFragment.NAV_DRAWER_TAGS)) - val hiddenFragments = hiddenDrawerItems - newTags.removeAll(hiddenFragments!!) - - if (newTags.contains(SUBSCRIPTION_LIST_TAG)) { - // we never want SUBSCRIPTION_LIST_TAG to be in 'tags' - // since it doesn't actually correspond to a position in the list, but is - // a placeholder that indicates if we should show the subscription list in the - // nav drawer at all. - showSubscriptionList = true - newTags.remove(SUBSCRIPTION_LIST_TAG) - } else { - showSubscriptionList = false - } - - fragmentTags.clear() - fragmentTags.addAll(newTags) - notifyDataSetChanged() - } - - fun getLabel(tag: String?): String { - val index = ArrayUtils.indexOf(NavDrawerFragment.NAV_DRAWER_TAGS, tag) - return titles[index] - } - - @UnstableApi @DrawableRes - private fun getDrawable(tag: String?): Int { - return when (tag) { - HomeFragment.TAG -> R.drawable.ic_home - QueueFragment.TAG -> R.drawable.ic_playlist_play - InboxFragment.TAG -> R.drawable.ic_inbox - AllEpisodesFragment.TAG -> R.drawable.ic_feed - CompletedDownloadsFragment.TAG -> R.drawable.ic_download - PlaybackHistoryFragment.TAG -> R.drawable.ic_history - SubscriptionFragment.TAG -> R.drawable.ic_subscriptions - AddFeedFragment.TAG -> R.drawable.ic_add - else -> 0 - } - } - - fun getFragmentTags(): List { - return Collections.unmodifiableList(fragmentTags) - } - - override fun getItemCount(): Int { - var baseCount = subscriptionOffset - if (showSubscriptionList) { - baseCount += itemAccess.count - } - return baseCount - } - - override fun getItemId(position: Int): Long { - val viewType = getItemViewType(position) - return when (viewType) { - VIEW_TYPE_SUBSCRIPTION -> { - itemAccess.getItem(position - subscriptionOffset)?.id?:0 - } - VIEW_TYPE_NAV -> { - (-abs(fragmentTags[position].hashCode().toLong().toDouble()) - 1).toLong() // Folder IDs are >0 - } - else -> { - 0 - } - } - } - - override fun getItemViewType(position: Int): Int { - return if (0 <= position && position < fragmentTags.size) { - VIEW_TYPE_NAV - } else if (position < subscriptionOffset) { - VIEW_TYPE_SECTION_DIVIDER - } else { - VIEW_TYPE_SUBSCRIPTION - } - } - - val subscriptionOffset: Int - get() = if (fragmentTags.size > 0) fragmentTags.size + 1 else 0 - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { - val inflater = LayoutInflater.from(activity.get()) - return when (viewType) { - VIEW_TYPE_NAV -> { - NavHolder(inflater.inflate(R.layout.nav_listitem, parent, false)) - } - VIEW_TYPE_SECTION_DIVIDER -> { - DividerHolder(inflater.inflate(R.layout.nav_section_item, parent, false)) - } - else -> { - FeedHolder(inflater.inflate(R.layout.nav_listitem, parent, false)) - } - } - } - - @UnstableApi override fun onBindViewHolder(holder: Holder, position: Int) { - val viewType = getItemViewType(position) - - holder.itemView.setOnCreateContextMenuListener(null) - when (viewType) { - VIEW_TYPE_NAV -> { - bindNavView(getLabel(fragmentTags[position]), position, holder as NavHolder) - } - VIEW_TYPE_SECTION_DIVIDER -> { - bindSectionDivider(holder as DividerHolder) - } - else -> { - val itemPos = position - subscriptionOffset - val item = itemAccess.getItem(itemPos) - if (item != null) { - bindListItem(item, holder as FeedHolder) - if (item.type == DrawerItem.Type.FEED) { - bindFeedView(item as FeedDrawerItem, holder) - } else { - bindTagView(item as TagDrawerItem, holder) - } - } - holder.itemView.setOnCreateContextMenuListener(itemAccess) - } - } - if (viewType != VIEW_TYPE_SECTION_DIVIDER) { - holder.itemView.isSelected = itemAccess.isSelected(position) - holder.itemView.setOnClickListener { v: View? -> itemAccess.onItemClick(position) } - holder.itemView.setOnLongClickListener { v: View? -> itemAccess.onItemLongClick(position) } - holder.itemView.setOnTouchListener { v: View?, e: MotionEvent -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (e.isFromSource(InputDevice.SOURCE_MOUSE) - && e.buttonState == MotionEvent.BUTTON_SECONDARY) { - itemAccess.onItemLongClick(position) - return@setOnTouchListener false - } - } - false - } - } - } - - @UnstableApi private fun bindNavView(title: String, position: Int, holder: NavHolder) { - val context = activity.get() ?: return - holder.title.text = title - - // reset for re-use - holder.count.visibility = View.GONE - holder.count.setOnClickListener(null) - holder.count.isClickable = false - - val tag = fragmentTags[position] - when { - tag == QueueFragment.TAG -> { - val queueSize = itemAccess.queueSize - if (queueSize > 0) { - holder.count.text = NumberFormat.getInstance().format(queueSize.toLong()) - holder.count.visibility = View.VISIBLE - } - } - tag == InboxFragment.TAG -> { - val unreadItems = itemAccess.numberOfNewItems - if (unreadItems > 0) { - holder.count.text = NumberFormat.getInstance().format(unreadItems.toLong()) - holder.count.visibility = View.VISIBLE - } - } - tag == SubscriptionFragment.TAG -> { - val sum = itemAccess.feedCounterSum - if (sum > 0) { - holder.count.text = NumberFormat.getInstance().format(sum.toLong()) - holder.count.visibility = View.VISIBLE - } - } - tag == CompletedDownloadsFragment.TAG && isEnableAutodownload -> { - val epCacheSize = episodeCacheSize - // don't count episodes that can be reclaimed - val spaceUsed = (itemAccess.numberOfDownloadedItems - - itemAccess.reclaimableItems) - if (epCacheSize in 1..spaceUsed) { - holder.count.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_disc_alert, 0) - holder.count.visibility = View.VISIBLE - holder.count.setOnClickListener { v: View? -> - MaterialAlertDialogBuilder(context) - .setTitle(R.string.episode_cache_full_title) - .setMessage(R.string.episode_cache_full_message) - .setPositiveButton(android.R.string.ok, null) - .setNeutralButton(R.string.open_autodownload_settings) { dialog: DialogInterface?, which: Int -> - val intent = Intent(context, PreferenceActivity::class.java) - intent.putExtra(PreferenceActivity.OPEN_AUTO_DOWNLOAD_SETTINGS, true) - context.startActivity(intent) - } - .show() - } - } - } - } - - holder.image.setImageResource(getDrawable(fragmentTags[position])) - } - - private fun bindSectionDivider(holder: DividerHolder) { - val context = activity.get() ?: return - - if (subscriptionsFilter.isEnabled && showSubscriptionList) { - holder.itemView.isEnabled = true - holder.feedsFilteredMsg.visibility = View.VISIBLE - } else { - holder.itemView.isEnabled = false - holder.feedsFilteredMsg.visibility = View.GONE - } - } - - private fun bindListItem(item: DrawerItem, holder: FeedHolder) { - if (item.counter > 0) { - holder.count.visibility = View.VISIBLE - holder.count.text = NumberFormat.getInstance().format(item.counter.toLong()) - } else { - holder.count.visibility = View.GONE - } - holder.title.text = item.title - val padding = (activity.get()!!.resources.getDimension(R.dimen.thumbnail_length_navlist) / 2).toInt() - holder.itemView.setPadding(item.layer * padding, 0, 0, 0) - } - - private fun bindFeedView(drawerItem: FeedDrawerItem, holder: FeedHolder) { - val feed = drawerItem.feed - val context = activity.get() ?: return - - Glide.with(context) - .load(feed.imageUrl) - .apply(RequestOptions() - .placeholder(R.color.light_gray) - .error(R.color.light_gray) - .transform(FitCenter(), - RoundedCorners((4 * context.resources.displayMetrics.density).toInt())) - .dontAnimate()) - .into(holder.image) - - if (feed.hasLastUpdateFailed()) { - val p = holder.title.layoutParams as RelativeLayout.LayoutParams - p.addRule(RelativeLayout.LEFT_OF, R.id.itxtvFailure) - holder.failure.visibility = View.VISIBLE - } else { - val p = holder.title.layoutParams as RelativeLayout.LayoutParams - p.addRule(RelativeLayout.LEFT_OF, R.id.txtvCount) - holder.failure.visibility = View.GONE - } - } - - private fun bindTagView(tag: TagDrawerItem, holder: FeedHolder) { - val context = activity.get() ?: return - if (tag.isOpen) { - holder.count.visibility = View.GONE - } - Glide.with(context).clear(holder.image) - holder.image.setImageResource(R.drawable.ic_tag) - holder.failure.visibility = View.GONE - } - - open class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) - - internal class DividerHolder(itemView: View) : Holder(itemView) { - val feedsFilteredMsg: LinearLayout = itemView.findViewById(R.id.nav_feeds_filtered_message) - } - - internal class NavHolder(itemView: View) : Holder(itemView) { - val image: ImageView = itemView.findViewById(R.id.imgvCover) - val title: TextView = itemView.findViewById(R.id.txtvTitle) - val count: TextView = itemView.findViewById(R.id.txtvCount) - } - - internal class FeedHolder(itemView: View) : Holder(itemView) { - val image: ImageView = itemView.findViewById(R.id.imgvCover) - val title: TextView = itemView.findViewById(R.id.txtvTitle) - val failure: ImageView = itemView.findViewById(R.id.itxtvFailure) - val count: TextView = itemView.findViewById(R.id.txtvCount) - } - - interface ItemAccess : OnCreateContextMenuListener { - val count: Int - - fun getItem(position: Int): DrawerItem? - - fun isSelected(position: Int): Boolean - - val queueSize: Int - - val numberOfNewItems: Int - - val numberOfDownloadedItems: Int - - val reclaimableItems: Int - - val feedCounterSum: Int - - fun onItemClick(position: Int) - - fun onItemLongClick(position: Int): Boolean - - override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenuInfo?) - } - - companion object { - const val VIEW_TYPE_NAV: Int = 0 - const val VIEW_TYPE_SECTION_DIVIDER: Int = 1 - private const val VIEW_TYPE_SUBSCRIPTION = 2 - - /** - * a tag used as a placeholder to indicate if the subscription list should be displayed or not - * This tag doesn't correspond to any specific activity. - */ - const val SUBSCRIPTION_LIST_TAG: String = "SubscriptionList" - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/QueueRecyclerAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/QueueRecyclerAdapter.kt deleted file mode 100644 index 26ac335d..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/QueueRecyclerAdapter.kt +++ /dev/null @@ -1,91 +0,0 @@ -package ac.mdiq.podcini.adapter - -import ac.mdiq.podcini.activity.MainActivity -import android.annotation.SuppressLint -import android.util.Log -import android.view.ContextMenu -import android.view.MenuInflater -import android.view.MotionEvent -import android.view.View -import androidx.media3.common.util.UnstableApi -import ac.mdiq.podcini.R -import ac.mdiq.podcini.fragment.swipeactions.SwipeActions -import ac.mdiq.podcini.storage.preferences.UserPreferences -import ac.mdiq.podcini.view.viewholder.EpisodeItemViewHolder - -/** - * List adapter for the queue. - */ -open class QueueRecyclerAdapter(mainActivity: MainActivity, private val swipeActions: SwipeActions) : EpisodeItemListAdapter(mainActivity) { - private var dragDropEnabled: Boolean - - init { - dragDropEnabled = !(UserPreferences.isQueueKeepSorted || UserPreferences.isQueueLocked) - } - - fun updateDragDropEnabled() { - dragDropEnabled = !(UserPreferences.isQueueKeepSorted || UserPreferences.isQueueLocked) - notifyDataSetChanged() - } - - @UnstableApi @SuppressLint("ClickableViewAccessibility") - override fun afterBindViewHolder(holder: EpisodeItemViewHolder, pos: Int) { - if (!dragDropEnabled) { - holder.dragHandle.setVisibility(View.GONE) - holder.dragHandle.setOnTouchListener(null) - holder.coverHolder.setOnTouchListener(null) - } else { - holder.dragHandle.setVisibility(View.VISIBLE) - holder.dragHandle.setOnTouchListener { v1: View?, event: MotionEvent -> - if (event.actionMasked == MotionEvent.ACTION_DOWN) { - Log.d(TAG, "startDrag()") - swipeActions.startDrag(holder) - } - false - } - holder.coverHolder.setOnTouchListener { v1, event -> - if (event.actionMasked == MotionEvent.ACTION_DOWN) { - val isLtr = holder.itemView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR - val factor = (if (isLtr) 1 else -1).toFloat() - if (factor * event.x < factor * 0.5 * v1.width) { - Log.d(TAG, "startDrag()") - swipeActions.startDrag(holder) - } else { - Log.d(TAG, "Ignoring drag in right half of the image") - } - } - false - } - } - if (inActionMode()) { - holder.dragHandle.setOnTouchListener(null) - holder.coverHolder.setOnTouchListener(null) - } - - holder.isInQueue.setVisibility(View.GONE) - } - - @UnstableApi override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { - val inflater: MenuInflater = activity!!.getMenuInflater() - inflater.inflate(R.menu.queue_context, menu) - super.onCreateContextMenu(menu, v, menuInfo) - - if (!inActionMode()) { - menu.findItem(R.id.multi_select).setVisible(true) - val keepSorted: Boolean = UserPreferences.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 { - menu.findItem(R.id.move_to_top_item).setVisible(false) - menu.findItem(R.id.move_to_bottom_item).setVisible(false) - } - } - - companion object { - private const val TAG = "QueueRecyclerAdapter" - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/SelectableAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/SelectableAdapter.kt deleted file mode 100644 index 8809eb93..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/SelectableAdapter.kt +++ /dev/null @@ -1,188 +0,0 @@ -package ac.mdiq.podcini.adapter - -import android.app.Activity -import android.view.ActionMode -import android.view.Menu -import android.view.MenuItem -import androidx.recyclerview.widget.RecyclerView -import ac.mdiq.podcini.R - -/** - * Used by Recyclerviews that need to provide ability to select items. - */ -abstract class SelectableAdapter(private val activity: Activity) : - RecyclerView.Adapter() { - - private var actionMode: ActionMode? = null - private val selectedIds = HashSet() - private var onSelectModeListener: OnSelectModeListener? = null - var shouldSelectLazyLoadedItems: Boolean = false - private var totalNumberOfItems = COUNT_AUTOMATICALLY - - fun startSelectMode(pos: Int) { - if (inActionMode()) { - endSelectMode() - } - onSelectModeListener?.onStartSelectMode() - - shouldSelectLazyLoadedItems = false - selectedIds.clear() - selectedIds.add(getItemId(pos)) - notifyDataSetChanged() - - actionMode = activity.startActionMode(object : ActionMode.Callback { - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - val inflater = mode.menuInflater - inflater.inflate(R.menu.multi_select_options, menu) - return true - } - - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - updateTitle() - toggleSelectAllIcon(menu.findItem(R.id.select_toggle), false) - return false - } - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - if (item.itemId == R.id.select_toggle) { - val selectAll = selectedIds.size != itemCount - shouldSelectLazyLoadedItems = selectAll - setSelected(0, itemCount, selectAll) - toggleSelectAllIcon(item, selectAll) - updateTitle() - return true - } - return false - } - - override fun onDestroyActionMode(mode: ActionMode) { - callOnEndSelectMode() - actionMode = null - shouldSelectLazyLoadedItems = false - selectedIds.clear() - notifyDataSetChanged() - } - }) - updateTitle() - } - - /** - * End action mode if currently in select mode, otherwise do nothing - */ - fun endSelectMode() { - if (inActionMode()) { - callOnEndSelectMode() - actionMode?.finish() - } - } - - fun isSelected(pos: Int): Boolean { - return selectedIds.contains(getItemId(pos)) - } - - /** - * Set the selected state of item at given position - * - * @param pos the position to select - * @param selected true for selected state and false for unselected - */ - open fun setSelected(pos: Int, selected: Boolean) { - if (selected) { - selectedIds.add(getItemId(pos)) - } else { - selectedIds.remove(getItemId(pos)) - } - updateTitle() - } - - /** - * Set the selected state of item for a given range - * - * @param startPos start position of range, inclusive - * @param endPos end position of range, inclusive - * @param selected indicates the selection state - * @throws IllegalArgumentException if start and end positions are not valid - */ - @Throws(IllegalArgumentException::class) - fun setSelected(startPos: Int, endPos: Int, selected: Boolean) { - var i = startPos - while (i < endPos && i < itemCount) { - setSelected(i, selected) - i++ - } - notifyItemRangeChanged(startPos, (endPos - startPos)) - } - - protected fun toggleSelection(pos: Int) { - setSelected(pos, !isSelected(pos)) - notifyItemChanged(pos) - - if (selectedIds.size == 0) { - endSelectMode() - } - } - - fun inActionMode(): Boolean { - return actionMode != null - } - - val selectedCount: Int - get() = selectedIds.size - - private fun toggleSelectAllIcon(selectAllItem: MenuItem, allSelected: Boolean) { - if (allSelected) { - selectAllItem.setIcon(R.drawable.ic_select_none) - selectAllItem.setTitle(R.string.deselect_all_label) - } else { - selectAllItem.setIcon(R.drawable.ic_select_all) - selectAllItem.setTitle(R.string.select_all_label) - } - } - - fun updateTitle() { - if (actionMode == null) { - return - } - var totalCount = itemCount - var selectedCount = selectedIds.size - if (totalNumberOfItems != COUNT_AUTOMATICALLY) { - totalCount = totalNumberOfItems - if (shouldSelectLazyLoadedItems) { - selectedCount += (totalNumberOfItems - itemCount) - } - } - actionMode!!.title = activity.resources - .getQuantityString(R.plurals.num_selected_label, selectedIds.size, - selectedCount, totalCount) - } - - fun setOnSelectModeListener(onSelectModeListener: OnSelectModeListener?) { - this.onSelectModeListener = onSelectModeListener - } - - private fun callOnEndSelectMode() { - onSelectModeListener?.onEndSelectMode() - } - - fun shouldSelectLazyLoadedItems(): Boolean { - return shouldSelectLazyLoadedItems - } - - /** - * Sets the total number of items that could be lazy-loaded. - * Can also be set to [.COUNT_AUTOMATICALLY] to simply use [.getItemCount] - */ - fun setTotalNumberOfItems(totalNumberOfItems: Int) { - this.totalNumberOfItems = totalNumberOfItems - } - - interface OnSelectModeListener { - fun onStartSelectMode() - - fun onEndSelectMode() - } - - companion object { - const val COUNT_AUTOMATICALLY: Int = -1 - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/SimpleChipAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/SimpleChipAdapter.kt deleted file mode 100644 index 008808a4..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/SimpleChipAdapter.kt +++ /dev/null @@ -1,40 +0,0 @@ -package ac.mdiq.podcini.adapter - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.chip.Chip -import ac.mdiq.podcini.R - -abstract class SimpleChipAdapter(private val context: Context) : RecyclerView.Adapter() { - init { - setHasStableIds(true) - } - - protected abstract fun getChips(): List - - protected abstract fun onRemoveClicked(position: Int) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val chip = Chip(context) - chip.isCloseIconVisible = true - chip.setCloseIconResource(R.drawable.ic_delete) - return ViewHolder(chip) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.chip.text = getChips()[position] - holder.chip.setOnCloseIconClickListener { v: View? -> onRemoveClicked(position) } - } - - override fun getItemCount(): Int { - return getChips().size - } - - override fun getItemId(position: Int): Long { - return getChips()[position].hashCode().toLong() - } - - class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(chip) -} \ No newline at end of file diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/SimpleIconListAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/SimpleIconListAdapter.kt deleted file mode 100644 index 520c129f..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/SimpleIconListAdapter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package ac.mdiq.podcini.adapter - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import android.widget.ImageView -import android.widget.TextView -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.request.RequestOptions -import ac.mdiq.podcini.R - -/** - * Displays a list of items that have a subtitle and an icon. - */ -class SimpleIconListAdapter(private val context: Context, - private val listItems: List -) : ArrayAdapter(context, R.layout.simple_icon_list_item, listItems) { - - override fun getView(position: Int, view: View?, parent: ViewGroup): View { - var view = view - if (view == null) { - view = View.inflate(context, R.layout.simple_icon_list_item, null) - } - - val item: ListItem = listItems[position] - (view!!.findViewById(R.id.title) as TextView).text = item.title - (view.findViewById(R.id.subtitle) as TextView).text = item.subtitle - Glide.with(context) - .load(item.imageUrl) - .apply(RequestOptions() - .diskCacheStrategy(DiskCacheStrategy.NONE) - .fitCenter() - .dontAnimate()) - .into(((view.findViewById(R.id.icon) as ImageView))) - return view - } - - open class ListItem(val title: String, val subtitle: String, val imageUrl: String) -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/SubscriptionsRecyclerAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/SubscriptionsRecyclerAdapter.kt deleted file mode 100644 index fdd36973..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/SubscriptionsRecyclerAdapter.kt +++ /dev/null @@ -1,272 +0,0 @@ -package ac.mdiq.podcini.adapter - -import ac.mdiq.podcini.activity.MainActivity -import android.content.Context -import android.graphics.Rect -import android.graphics.drawable.Drawable -import android.os.Build -import android.view.* -import android.widget.* -import androidx.appcompat.content.res.AppCompatResources -import androidx.cardview.widget.CardView -import androidx.fragment.app.Fragment -import androidx.media3.common.util.UnstableApi -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.elevation.SurfaceColors -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.storage.NavDrawerData -import ac.mdiq.podcini.fragment.FeedItemlistFragment -import ac.mdiq.podcini.fragment.SubscriptionFragment -import ac.mdiq.podcini.model.feed.Feed -import ac.mdiq.podcini.storage.preferences.UserPreferences -import java.lang.ref.WeakReference -import java.text.NumberFormat - -/** - * Adapter for subscriptions - */ -open class SubscriptionsRecyclerAdapter(mainActivity: MainActivity) : - SelectableAdapter(mainActivity), - View.OnCreateContextMenuListener { - - private val mainActivityRef: WeakReference = WeakReference(mainActivity) - private var listItems: List - private var selectedItem: NavDrawerData.DrawerItem? = null - var longPressedPosition: Int = 0 // used to init actionMode - private var columnCount = 3 - - init { - this.listItems = ArrayList() - setHasStableIds(true) - } - - fun setColumnCount(columnCount: Int) { - this.columnCount = columnCount - } - - fun getItem(position: Int): Any { - return listItems[position] - } - - fun getSelectedItem(): NavDrawerData.DrawerItem? { - return selectedItem - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder { - val itemView: View = - LayoutInflater.from(mainActivityRef.get()).inflate(R.layout.subscription_item, parent, false) - itemView.findViewById(R.id.titleLabel).visibility = if (viewType == COVER_WITH_TITLE) View.VISIBLE else View.GONE - return SubscriptionViewHolder(itemView) - } - - @UnstableApi override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) { - val drawerItem: NavDrawerData.DrawerItem = listItems[position] - val isFeed = drawerItem.type == NavDrawerData.DrawerItem.Type.FEED - holder.bind(drawerItem) - holder.itemView.setOnCreateContextMenuListener(this) - if (inActionMode()) { - if (isFeed) { - holder.selectCheckbox.visibility = View.VISIBLE - holder.selectView.visibility = View.VISIBLE - } - holder.selectCheckbox.setChecked((isSelected(position))) - holder.selectCheckbox.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean -> - setSelected(holder.bindingAdapterPosition, - isChecked) - } - if (holder.coverImage != null) holder.coverImage.alpha = 0.6f - holder.count.visibility = View.GONE - } else { - holder.selectView.visibility = View.GONE - if (holder.coverImage != null) holder.coverImage.alpha = 1.0f - } - - holder.itemView.setOnLongClickListener { v: View? -> - if (!inActionMode()) { - if (isFeed) { - longPressedPosition = holder.bindingAdapterPosition - } - selectedItem = drawerItem - } - false - } - - holder.itemView.setOnTouchListener { v: View?, e: MotionEvent -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (e.isFromSource(InputDevice.SOURCE_MOUSE) - && e.buttonState == MotionEvent.BUTTON_SECONDARY) { - if (!inActionMode()) { - if (isFeed) { - longPressedPosition = holder.bindingAdapterPosition - } - selectedItem = drawerItem - } - } - } - false - } - holder.itemView.setOnClickListener { v: View? -> - if (isFeed) { - if (inActionMode()) { - holder.selectCheckbox.setChecked(!isSelected(holder.bindingAdapterPosition)) - } else { - val fragment: Fragment = FeedItemlistFragment - .newInstance((drawerItem as NavDrawerData.FeedDrawerItem).feed.id) - mainActivityRef.get()?.loadChildFragment(fragment) - } - } else if (!inActionMode()) { - val fragment: Fragment = SubscriptionFragment.newInstance(drawerItem.title) - mainActivityRef.get()?.loadChildFragment(fragment) - } - } - } - - override fun getItemCount(): Int { - return listItems.size - } - - override fun getItemId(position: Int): Long { - if (position >= listItems.size) { - return RecyclerView.NO_ID // Dummy views - } - return listItems[position].id - } - - override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { - if (inActionMode() || selectedItem == null) { - return - } - val inflater: MenuInflater = mainActivityRef.get()!!.menuInflater - if (selectedItem?.type == NavDrawerData.DrawerItem.Type.FEED) { - inflater.inflate(R.menu.nav_feed_context, menu) - menu.findItem(R.id.multi_select).setVisible(true) - } else { - inflater.inflate(R.menu.nav_folder_context, menu) - } - menu.setHeaderTitle(selectedItem?.title) - } - - fun onContextItemSelected(item: MenuItem): Boolean { - if (item.itemId == R.id.multi_select) { - startSelectMode(longPressedPosition) - return true - } - return false - } - - val selectedItems: List - get() { - val items = ArrayList() - for (i in 0 until itemCount) { - if (isSelected(i)) { - val drawerItem: NavDrawerData.DrawerItem = listItems[i] - if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) { - val feed: Feed = (drawerItem as NavDrawerData.FeedDrawerItem).feed - items.add(feed) - } - } - } - return items - } - - fun setItems(listItems: List) { - this.listItems = listItems - notifyDataSetChanged() - } - - override fun setSelected(pos: Int, selected: Boolean) { - val drawerItem: NavDrawerData.DrawerItem = listItems[pos] - if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) { - super.setSelected(pos, selected) - } - } - - override fun getItemViewType(position: Int): Int { - return if (UserPreferences.shouldShowSubscriptionTitle()) COVER_WITH_TITLE else 0 - } - - inner class SubscriptionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - private val title = itemView.findViewById(R.id.titleLabel) - val coverImage: ImageView? = itemView.findViewById(R.id.coverImage) - val count: TextView = itemView.findViewById(R.id.countViewPill) - private val fallbackTitle: TextView = itemView.findViewById(R.id.fallbackTitleLabel) - val selectView: FrameLayout = itemView.findViewById(R.id.selectContainer) - val selectCheckbox: CheckBox = itemView.findViewById(R.id.selectCheckBox) - private val card: CardView = itemView.findViewById(R.id.outerContainer) - private val errorIcon: View = itemView.findViewById(R.id.errorIcon) - - fun bind(drawerItem: NavDrawerData.DrawerItem) { - val drawable: Drawable? = AppCompatResources.getDrawable(selectView.context, - R.drawable.ic_checkbox_background) - selectView.background = drawable // Setting this in XML crashes API <= 21 - title.text = drawerItem.title - fallbackTitle.text = drawerItem.title - if (coverImage != null) coverImage.contentDescription = drawerItem.title - if (drawerItem.counter > 0) { - count.text = NumberFormat.getInstance().format(drawerItem.counter.toLong()) - count.visibility = View.VISIBLE - } else { - count.visibility = View.GONE - } - - val coverLoader = CoverLoader(mainActivityRef.get()!!) - val textAndImageCombined: Boolean - if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) { - val feed: Feed = (drawerItem as NavDrawerData.FeedDrawerItem).feed - textAndImageCombined = feed.isLocalFeed && feed.imageUrl != null && feed.imageUrl!!.startsWith(Feed.PREFIX_GENERATIVE_COVER) - coverLoader.withUri(feed.imageUrl) - errorIcon.visibility = if (feed.hasLastUpdateFailed()) View.VISIBLE else View.GONE - } else { - textAndImageCombined = true - coverLoader.withResource(R.drawable.ic_tag) - errorIcon.visibility = View.GONE - } - if (UserPreferences.shouldShowSubscriptionTitle()) { - // No need for fallback title when already showing title - fallbackTitle.visibility = View.GONE - } else { - coverLoader.withPlaceholderView(fallbackTitle, textAndImageCombined) - } - if (coverImage != null) coverLoader.withCoverView(coverImage) - coverLoader.load() - - val density: Float = mainActivityRef.get()!!.resources.displayMetrics.density - card.setCardBackgroundColor(SurfaceColors.getColorForElevation(mainActivityRef.get()!!, 1 * density)) - - val textPadding = if (columnCount <= 3) 16 else 8 - title.setPadding(textPadding, textPadding, textPadding, textPadding) - fallbackTitle.setPadding(textPadding, textPadding, textPadding, textPadding) - - var textSize = 14 - if (columnCount == 3) { - textSize = 15 - } else if (columnCount == 2) { - textSize = 16 - } - title.textSize = textSize.toFloat() - fallbackTitle.textSize = textSize.toFloat() - } - } - - class GridDividerItemDecorator : RecyclerView.ItemDecoration() { - - override fun getItemOffsets(outRect: Rect, - view: View, - parent: RecyclerView, - state: RecyclerView.State - ) { - super.getItemOffsets(outRect, view, parent, state) - val context = parent.context - val insetOffset = convertDpToPixel(context, 1f).toInt() - outRect[insetOffset, insetOffset, insetOffset] = insetOffset - } - } - - companion object { - private const val COVER_WITH_TITLE = 1 - - fun convertDpToPixel(context: Context, dp: Float): Float { - return dp * context.resources.displayMetrics.density - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/CancelDownloadActionButton.kt b/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/CancelDownloadActionButton.kt deleted file mode 100644 index d3715345..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/CancelDownloadActionButton.kt +++ /dev/null @@ -1,32 +0,0 @@ -package ac.mdiq.podcini.adapter.actionbutton - -import android.content.Context -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.media3.common.util.UnstableApi -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.storage.DBWriter -import ac.mdiq.podcini.model.feed.FeedItem -import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface -import ac.mdiq.podcini.storage.preferences.UserPreferences.isEnableAutodownload - -class CancelDownloadActionButton(item: FeedItem) : ItemActionButton(item) { - @StringRes - override fun getLabel(): Int { - return R.string.cancel_download_label - } - - @DrawableRes - override fun getDrawable(): Int { - return R.drawable.ic_cancel - } - - @UnstableApi override fun onClick(context: Context) { - val media = item.media - if (media != null) DownloadServiceInterface.get()?.cancel(context, media) - if (isEnableAutodownload) { - item.disableAutoDownload() - DBWriter.setFeedItem(item) - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/DeleteActionButton.kt b/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/DeleteActionButton.kt deleted file mode 100644 index 21e9fc9b..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/DeleteActionButton.kt +++ /dev/null @@ -1,32 +0,0 @@ -package ac.mdiq.podcini.adapter.actionbutton - -import android.content.Context -import android.view.View -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.storage.DBWriter -import ac.mdiq.podcini.model.feed.FeedItem -import ac.mdiq.podcini.view.LocalDeleteModal.showLocalFeedDeleteWarningIfNecessary -import androidx.media3.common.util.UnstableApi - -class DeleteActionButton(item: FeedItem) : ItemActionButton(item) { - override fun getLabel(): Int { - return R.string.delete_label - } - override fun getDrawable(): Int { - return R.drawable.ic_delete - } - @UnstableApi override fun onClick(context: Context) { - val media = item.media ?: return - - showLocalFeedDeleteWarningIfNecessary(context, listOf(item)) { DBWriter.deleteFeedMediaOfItem(context, media.id) } - } - - override val visibility: Int - get() { - if (item.media != null && (item.media!!.isDownloaded() || item.feed?.isLocalFeed == true)) { - return View.VISIBLE - } - - return View.INVISIBLE - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/DownloadActionButton.kt b/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/DownloadActionButton.kt deleted file mode 100644 index 6ff94d43..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/DownloadActionButton.kt +++ /dev/null @@ -1,60 +0,0 @@ -package ac.mdiq.podcini.adapter.actionbutton - -import android.content.Context -import android.content.DialogInterface -import android.view.View -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.preferences.UsageStatistics -import ac.mdiq.podcini.core.preferences.UsageStatistics.logAction -import ac.mdiq.podcini.core.util.NetworkUtils.isEpisodeDownloadAllowed -import ac.mdiq.podcini.core.util.NetworkUtils.isNetworkRestricted -import ac.mdiq.podcini.core.util.NetworkUtils.isVpnOverWifi -import ac.mdiq.podcini.model.feed.FeedItem -import ac.mdiq.podcini.model.feed.FeedMedia -import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface - -class DownloadActionButton(item: FeedItem) : ItemActionButton(item) { - 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 - if (media == null || shouldNotDownload(media)) { - return - } - - logAction(UsageStatistics.ACTION_DOWNLOAD) - - if (isEpisodeDownloadAllowed) { - DownloadServiceInterface.get()?.downloadNow(context, item, false) - } else { - val builder = MaterialAlertDialogBuilder(context) - .setTitle(R.string.confirm_mobile_download_dialog_title) - .setPositiveButton(R.string.confirm_mobile_download_dialog_download_later - ) { d: DialogInterface?, w: Int -> DownloadServiceInterface.get()?.downloadNow(context, item, false) } - .setNeutralButton(R.string.confirm_mobile_download_dialog_allow_this_time - ) { d: DialogInterface?, w: Int -> DownloadServiceInterface.get()?.downloadNow(context, item, true) } - .setNegativeButton(R.string.cancel_label, null) - if (isNetworkRestricted && isVpnOverWifi) { - builder.setMessage(R.string.confirm_mobile_download_dialog_message_vpn) - } else { - builder.setMessage(R.string.confirm_mobile_download_dialog_message) - } - - builder.show() - } - } - - private fun shouldNotDownload(media: FeedMedia): Boolean { - if (media.download_url == null) return true - val isDownloading = DownloadServiceInterface.get()?.isDownloadingEpisode(media.download_url!!)?:false - return isDownloading || media.isDownloaded() - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/ItemActionButton.kt b/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/ItemActionButton.kt deleted file mode 100644 index 0f2f3442..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/ItemActionButton.kt +++ /dev/null @@ -1,59 +0,0 @@ -package ac.mdiq.podcini.adapter.actionbutton - -import android.content.Context -import android.view.View -import android.widget.ImageView -import androidx.media3.common.util.UnstableApi -import ac.mdiq.podcini.core.util.PlaybackStatus.isCurrentlyPlaying -import ac.mdiq.podcini.model.feed.FeedItem -import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface -import ac.mdiq.podcini.storage.preferences.UserPreferences.isStreamOverDownload - -abstract class ItemActionButton internal constructor(@JvmField var item: FeedItem) { - abstract fun getLabel(): Int - - abstract fun getDrawable(): Int - - abstract fun onClick(context: Context) - - open val visibility: Int - get() = View.VISIBLE - - fun configure(button: View, icon: ImageView, context: Context) { - button.visibility = visibility - button.contentDescription = context.getString(getLabel()) - button.setOnClickListener { view: View? -> onClick(context) } - icon.setImageResource(getDrawable()) - } - - @UnstableApi companion object { - fun forItem(item: FeedItem): ItemActionButton { - val media = item.media ?: return MarkAsPlayedActionButton(item) - - val isDownloadingMedia = when (media.download_url) { - null -> false - else -> DownloadServiceInterface.get()?.isDownloadingEpisode(media.download_url!!)?:false - } - return when { - isCurrentlyPlaying(media) -> { - PauseActionButton(item) - } - item.feed != null && item.feed!!.isLocalFeed -> { - PlayLocalActionButton(item) - } - media.isDownloaded() -> { - PlayActionButton(item) - } - isDownloadingMedia -> { - CancelDownloadActionButton(item) - } - isStreamOverDownload -> { - StreamActionButton(item) - } - else -> { - DownloadActionButton(item) - } - } - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/MarkAsPlayedActionButton.kt b/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/MarkAsPlayedActionButton.kt deleted file mode 100644 index ecfda4f5..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/MarkAsPlayedActionButton.kt +++ /dev/null @@ -1,25 +0,0 @@ -package ac.mdiq.podcini.adapter.actionbutton - -import android.content.Context -import android.view.View -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.storage.DBWriter -import ac.mdiq.podcini.model.feed.FeedItem -import androidx.media3.common.util.UnstableApi - -class MarkAsPlayedActionButton(item: FeedItem) : ItemActionButton(item) { - override fun getLabel(): Int { - return (if (item.hasMedia()) 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()) { - DBWriter.markItemPlayed(item, FeedItem.PLAYED, true) - } - } - - override val visibility: Int - get() = if (item.isPlayed()) View.INVISIBLE else View.VISIBLE -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/PauseActionButton.kt b/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/PauseActionButton.kt deleted file mode 100644 index 5860b11d..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/PauseActionButton.kt +++ /dev/null @@ -1,25 +0,0 @@ -package ac.mdiq.podcini.adapter.actionbutton - -import android.content.Context -import android.view.KeyEvent -import androidx.media3.common.util.UnstableApi -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.receiver.MediaButtonReceiver.Companion.createIntent -import ac.mdiq.podcini.core.util.PlaybackStatus.isCurrentlyPlaying -import ac.mdiq.podcini.model.feed.FeedItem - -class PauseActionButton(item: FeedItem) : ItemActionButton(item) { - override fun getLabel(): Int { - return R.string.pause_label - } - override fun getDrawable(): Int { - return R.drawable.ic_pause - } - @UnstableApi override fun onClick(context: Context) { - val media = item.media ?: return - - if (isCurrentlyPlaying(media)) { - context.sendBroadcast(createIntent(context, KeyEvent.KEYCODE_MEDIA_PAUSE)) - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/PlayActionButton.kt b/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/PlayActionButton.kt deleted file mode 100644 index 74300d04..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/PlayActionButton.kt +++ /dev/null @@ -1,33 +0,0 @@ -package ac.mdiq.podcini.adapter.actionbutton - -import android.content.Context -import androidx.media3.common.util.UnstableApi -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.service.playback.PlaybackService.Companion.getPlayerActivityIntent -import ac.mdiq.podcini.core.storage.DBTasks -import ac.mdiq.podcini.core.util.playback.PlaybackServiceStarter -import ac.mdiq.podcini.model.feed.FeedItem -import ac.mdiq.podcini.model.playback.MediaType - -class PlayActionButton(item: FeedItem) : ItemActionButton(item) { - override fun getLabel(): Int { - return R.string.play_label - } - override fun getDrawable(): Int { - return R.drawable.ic_play_24dp - } - @UnstableApi override fun onClick(context: Context) { - val media = item.media ?: return - if (!media.fileExists()) { - DBTasks.notifyMissingFeedMediaFile(context, media) - return - } - PlaybackServiceStarter(context, media) - .callEvenIfRunning(true) - .start() - - if (media.getMediaType() == MediaType.VIDEO) { - context.startActivity(getPlayerActivityIntent(context, media)) - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/PlayLocalActionButton.kt b/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/PlayLocalActionButton.kt deleted file mode 100644 index abba22c8..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/PlayLocalActionButton.kt +++ /dev/null @@ -1,29 +0,0 @@ -package ac.mdiq.podcini.adapter.actionbutton - -import android.content.Context -import androidx.media3.common.util.UnstableApi -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.service.playback.PlaybackService.Companion.getPlayerActivityIntent -import ac.mdiq.podcini.core.util.playback.PlaybackServiceStarter -import ac.mdiq.podcini.model.feed.FeedItem -import ac.mdiq.podcini.model.playback.MediaType - -class PlayLocalActionButton(item: FeedItem?) : ItemActionButton(item!!) { - override fun getLabel(): Int { - return R.string.play_label - } - override fun getDrawable(): Int { - return R.drawable.ic_play_24dp - } - @UnstableApi override fun onClick(context: Context) { - val media = item.media ?: return - - PlaybackServiceStarter(context, media) - .callEvenIfRunning(true) - .start() - - if (media.getMediaType() == MediaType.VIDEO) { - context.startActivity(getPlayerActivityIntent(context, media)) - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/StreamActionButton.kt b/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/StreamActionButton.kt deleted file mode 100644 index bce7bfd9..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/StreamActionButton.kt +++ /dev/null @@ -1,38 +0,0 @@ -package ac.mdiq.podcini.adapter.actionbutton - -import android.content.Context -import androidx.media3.common.util.UnstableApi -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.preferences.UsageStatistics -import ac.mdiq.podcini.core.preferences.UsageStatistics.logAction -import ac.mdiq.podcini.core.service.playback.PlaybackService.Companion.getPlayerActivityIntent -import ac.mdiq.podcini.core.util.NetworkUtils.isStreamingAllowed -import ac.mdiq.podcini.core.util.playback.PlaybackServiceStarter -import ac.mdiq.podcini.dialog.StreamingConfirmationDialog -import ac.mdiq.podcini.model.feed.FeedItem -import ac.mdiq.podcini.model.playback.MediaType - -class StreamActionButton(item: FeedItem) : ItemActionButton(item) { - override fun getLabel(): Int { - return R.string.stream_label - } - override fun getDrawable(): Int { - return R.drawable.ic_stream - } - @UnstableApi override fun onClick(context: Context) { - val media = item.media ?: return - logAction(UsageStatistics.ACTION_STREAM) - - if (!isStreamingAllowed) { - StreamingConfirmationDialog(context, media).show() - return - } - PlaybackServiceStarter(context, media) - .callEvenIfRunning(true) - .start() - - if (media.getMediaType() == MediaType.VIDEO) { - context.startActivity(getPlayerActivityIntent(context, media)) - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/VisitWebsiteActionButton.kt b/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/VisitWebsiteActionButton.kt deleted file mode 100644 index 9705297d..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/actionbutton/VisitWebsiteActionButton.kt +++ /dev/null @@ -1,22 +0,0 @@ -package ac.mdiq.podcini.adapter.actionbutton - -import android.content.Context -import android.view.View -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.util.IntentUtils.openInBrowser -import ac.mdiq.podcini.model.feed.FeedItem - -class VisitWebsiteActionButton(item: FeedItem) : ItemActionButton(item) { - 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!= null) openInBrowser(context, item.link!!) - } - - override val visibility: Int - get() = if (item.link == null) View.INVISIBLE else View.VISIBLE -} diff --git a/app/src/main/java/ac/mdiq/podcini/adapter/itunes/ItunesAdapter.kt b/app/src/main/java/ac/mdiq/podcini/adapter/itunes/ItunesAdapter.kt deleted file mode 100644 index 88a986f0..00000000 --- a/app/src/main/java/ac/mdiq/podcini/adapter/itunes/ItunesAdapter.kt +++ /dev/null @@ -1,93 +0,0 @@ -package ac.mdiq.podcini.adapter.itunes - -import ac.mdiq.podcini.activity.MainActivity -import android.content.Context -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import android.widget.ImageView -import android.widget.TextView -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.resource.bitmap.FitCenter -import com.bumptech.glide.load.resource.bitmap.RoundedCorners -import com.bumptech.glide.request.RequestOptions -import ac.mdiq.podcini.R -import ac.mdiq.podcini.net.discovery.PodcastSearchResult -import androidx.media3.common.util.UnstableApi - -class ItunesAdapter( - /** - * Related Context - */ - private val context: Context, objects: List -) : ArrayAdapter(context, 0, objects) { - /** - * List holding the podcasts found in the search - */ - private val data: List = objects - - @UnstableApi override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - //Current podcast - val podcast: PodcastSearchResult = data[position] - - //ViewHolder - val viewHolder: PodcastViewHolder - - //Resulting view - val view: View - - //Handle view holder stuff - if (convertView == null) { - view = (context as MainActivity).layoutInflater.inflate(R.layout.itunes_podcast_listitem, parent, false) - viewHolder = PodcastViewHolder(view) - view.tag = viewHolder - } else { - view = convertView - viewHolder = view.tag as PodcastViewHolder - } - - // Set the title - viewHolder.titleView.text = podcast.title - if (podcast.author != null && podcast.author!!.trim { it <= ' ' }.isNotEmpty()) { - viewHolder.authorView.text = podcast.author - viewHolder.authorView.visibility = View.VISIBLE - } else if (podcast.feedUrl != null && !podcast.feedUrl!!.contains("itunes.apple.com")) { - viewHolder.authorView.text = podcast.feedUrl - viewHolder.authorView.visibility = View.VISIBLE - } else { - viewHolder.authorView.visibility = View.GONE - } - - //Update the empty imageView with the image from the feed - Glide.with(context) - .load(podcast.imageUrl) - .apply(RequestOptions() - .placeholder(R.color.light_gray) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .transform(FitCenter(), - RoundedCorners((4 * context.resources.displayMetrics.density).toInt())) - .dontAnimate()) - .into(viewHolder.coverView) - - //Feed the grid view - return view - } - - /** - * View holder object for the GridView - */ - internal class PodcastViewHolder(view: View) { - /** - * ImageView holding the Podcast image - */ - val coverView: ImageView = view.findViewById(R.id.imgvCover) - - /** - * TextView holding the Podcast title - */ - val titleView: TextView = view.findViewById(R.id.txtvTitle) - - val authorView: TextView = view.findViewById(R.id.txtvAuthor) - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/asynctask/DocumentFileExportWorker.kt b/app/src/main/java/ac/mdiq/podcini/asynctask/DocumentFileExportWorker.kt deleted file mode 100644 index b45793db..00000000 --- a/app/src/main/java/ac/mdiq/podcini/asynctask/DocumentFileExportWorker.kt +++ /dev/null @@ -1,56 +0,0 @@ -package ac.mdiq.podcini.asynctask - -import android.content.Context -import android.net.Uri -import androidx.documentfile.provider.DocumentFile -import ac.mdiq.podcini.core.export.ExportWriter -import ac.mdiq.podcini.core.storage.DBReader -import io.reactivex.Observable -import io.reactivex.ObservableEmitter -import java.io.IOException -import java.io.OutputStream -import java.io.OutputStreamWriter -import java.nio.charset.Charset - -/** - * Writes an OPML file into the user selected export directory in the background. - */ -class DocumentFileExportWorker(private val exportWriter: ExportWriter, - private val context: Context, - private val outputFileUri: Uri -) { - fun exportObservable(): Observable { - val output = DocumentFile.fromSingleUri(context, outputFileUri) - return Observable.create { subscriber: ObservableEmitter -> - var outputStream: OutputStream? = null - var writer: OutputStreamWriter? = null - try { - if (output == null) throw IOException() - val uri = output.uri - outputStream = context.contentResolver.openOutputStream(uri, "wt") - if (outputStream == null) throw IOException() - writer = OutputStreamWriter(outputStream, Charset.forName("UTF-8")) - exportWriter.writeDocument(DBReader.getFeedList(), writer, context) - subscriber.onNext(output) - } catch (e: IOException) { - subscriber.onError(e) - } finally { - if (writer != null) { - try { - writer.close() - } catch (e: IOException) { - subscriber.onError(e) - } - } - if (outputStream != null) { - try { - outputStream.close() - } catch (e: IOException) { - subscriber.onError(e) - } - } - subscriber.onComplete() - } - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/asynctask/ExportWorker.kt b/app/src/main/java/ac/mdiq/podcini/asynctask/ExportWorker.kt deleted file mode 100644 index 02c9b5a6..00000000 --- a/app/src/main/java/ac/mdiq/podcini/asynctask/ExportWorker.kt +++ /dev/null @@ -1,57 +0,0 @@ -package ac.mdiq.podcini.asynctask - -import android.content.Context -import android.util.Log -import ac.mdiq.podcini.core.export.ExportWriter -import ac.mdiq.podcini.core.storage.DBReader -import ac.mdiq.podcini.storage.preferences.UserPreferences.getDataFolder -import io.reactivex.Observable -import io.reactivex.ObservableEmitter -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.io.OutputStreamWriter -import java.nio.charset.Charset - -/** - * Writes an OPML file into the export directory in the background. - */ -class ExportWorker private constructor(private val exportWriter: ExportWriter, - private val output: File, - private val context: Context -) { - constructor(exportWriter: ExportWriter, context: Context) : this(exportWriter, File(getDataFolder(EXPORT_DIR), - DEFAULT_OUTPUT_NAME + "." + exportWriter.fileExtension()), context) - - fun exportObservable(): Observable { - if (output.exists()) { - val success = output.delete() - Log.w(TAG, "Overwriting previously exported file: $success") - } - return Observable.create { subscriber: ObservableEmitter -> - var writer: OutputStreamWriter? = null - try { - writer = OutputStreamWriter(FileOutputStream(output), Charset.forName("UTF-8")) - exportWriter.writeDocument(DBReader.getFeedList(), writer, context) - subscriber.onNext(output) - } catch (e: IOException) { - subscriber.onError(e) - } finally { - if (writer != null) { - try { - writer.close() - } catch (e: IOException) { - subscriber.onError(e) - } - } - subscriber.onComplete() - } - } - } - - companion object { - private const val EXPORT_DIR = "export/" - private const val TAG = "ExportWorker" - private const val DEFAULT_OUTPUT_NAME = "podcini-feeds" - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/config/ApplicationCallbacksImpl.kt b/app/src/main/java/ac/mdiq/podcini/config/ApplicationCallbacksImpl.kt deleted file mode 100644 index 2d56f9e3..00000000 --- a/app/src/main/java/ac/mdiq/podcini/config/ApplicationCallbacksImpl.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ac.mdiq.podcini.config - -import android.app.Application -import ac.mdiq.podcini.PodciniApp -import ac.mdiq.podcini.core.ApplicationCallbacks - - -class ApplicationCallbacksImpl : ApplicationCallbacks { - override fun getApplicationInstance(): Application { - return PodciniApp.getInstance() - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/dialog/AllEpisodesFilterDialog.kt b/app/src/main/java/ac/mdiq/podcini/dialog/AllEpisodesFilterDialog.kt deleted file mode 100644 index b2e5753b..00000000 --- a/app/src/main/java/ac/mdiq/podcini/dialog/AllEpisodesFilterDialog.kt +++ /dev/null @@ -1,22 +0,0 @@ -package ac.mdiq.podcini.dialog - -import android.os.Bundle -import ac.mdiq.podcini.model.feed.FeedItemFilter -import org.greenrobot.eventbus.EventBus - -class AllEpisodesFilterDialog : ItemFilterDialog() { - override fun onFilterChanged(newFilterValues: Set) { - EventBus.getDefault().post(AllEpisodesFilterChangedEvent(newFilterValues)) - } - - class AllEpisodesFilterChangedEvent(val filterValues: Set?) - companion object { - fun newInstance(filter: FeedItemFilter?): AllEpisodesFilterDialog { - val dialog = AllEpisodesFilterDialog() - val arguments = Bundle() - arguments.putSerializable(ARGUMENT_FILTER, filter) - dialog.arguments = arguments - return dialog - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/dialog/AuthenticationDialog.kt b/app/src/main/java/ac/mdiq/podcini/dialog/AuthenticationDialog.kt deleted file mode 100644 index da53f91d..00000000 --- a/app/src/main/java/ac/mdiq/podcini/dialog/AuthenticationDialog.kt +++ /dev/null @@ -1,57 +0,0 @@ -package ac.mdiq.podcini.dialog - -import android.content.Context -import android.content.DialogInterface -import android.text.method.HideReturnsTransformationMethod -import android.text.method.PasswordTransformationMethod -import android.view.LayoutInflater -import android.view.View -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.databinding.AuthenticationDialogBinding - -/** - * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences. - */ -abstract class AuthenticationDialog(context: Context?, titleRes: Int, enableUsernameField: Boolean, - usernameInitialValue: String?, passwordInitialValue: String? -) : MaterialAlertDialogBuilder( - context!!) { - var passwordHidden: Boolean = true - - init { - setTitle(titleRes) - val viewBinding = AuthenticationDialogBinding.inflate(LayoutInflater.from(context)) - setView(viewBinding.root) - - viewBinding.usernameEditText.isEnabled = enableUsernameField - if (usernameInitialValue != null) { - viewBinding.usernameEditText.setText(usernameInitialValue) - } - if (passwordInitialValue != null) { - viewBinding.passwordEditText.setText(passwordInitialValue) - } - viewBinding.showPasswordButton.setOnClickListener { v: View? -> - if (passwordHidden) { - viewBinding.passwordEditText.transformationMethod = HideReturnsTransformationMethod.getInstance() - viewBinding.showPasswordButton.alpha = 1.0f - } else { - viewBinding.passwordEditText.transformationMethod = PasswordTransformationMethod.getInstance() - viewBinding.showPasswordButton.alpha = 0.6f - } - passwordHidden = !passwordHidden - } - - setOnCancelListener { dialog: DialogInterface? -> onCancelled() } - setNegativeButton(R.string.cancel_label) { dialog: DialogInterface?, which: Int -> onCancelled() } - setPositiveButton(R.string.confirm_label) { dialog: DialogInterface?, which: Int -> - onConfirmed(viewBinding.usernameEditText.text.toString(), - viewBinding.passwordEditText.text.toString()) - } - } - - protected open fun onCancelled() { - } - - protected abstract fun onConfirmed(username: String, password: String) -} diff --git a/app/src/main/java/ac/mdiq/podcini/dialog/ChooseDataFolderDialog.kt b/app/src/main/java/ac/mdiq/podcini/dialog/ChooseDataFolderDialog.kt deleted file mode 100644 index 1dcdedce..00000000 --- a/app/src/main/java/ac/mdiq/podcini/dialog/ChooseDataFolderDialog.kt +++ /dev/null @@ -1,33 +0,0 @@ -package ac.mdiq.podcini.dialog - -import android.content.Context -import android.view.View -import androidx.core.util.Consumer -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.adapter.DataFolderAdapter - -object ChooseDataFolderDialog { - fun showDialog(context: Context?, handlerFunc: Consumer) { - val content = View.inflate(context, R.layout.choose_data_folder_dialog, null) - val dialog = MaterialAlertDialogBuilder(context!!) - .setView(content) - .setTitle(R.string.choose_data_directory) - .setMessage(R.string.choose_data_directory_message) - .setNegativeButton(R.string.cancel_label, null) - .create() - val recyclerView = content.findViewById(R.id.recyclerView) - recyclerView.layoutManager = LinearLayoutManager(context) - val adapter = DataFolderAdapter(context) { path: String? -> - dialog.dismiss() - handlerFunc.accept(path) - } - recyclerView.adapter = adapter - - if (adapter.itemCount != 0) { - dialog.show() - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/dialog/DownloadLogDetailsDialog.kt b/app/src/main/java/ac/mdiq/podcini/dialog/DownloadLogDetailsDialog.kt deleted file mode 100644 index 93ea471a..00000000 --- a/app/src/main/java/ac/mdiq/podcini/dialog/DownloadLogDetailsDialog.kt +++ /dev/null @@ -1,60 +0,0 @@ -package ac.mdiq.podcini.dialog - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.os.Build -import android.view.View -import android.widget.TextView -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.storage.DBReader -import ac.mdiq.podcini.core.util.DownloadErrorLabel.from -import ac.mdiq.podcini.event.MessageEvent -import ac.mdiq.podcini.model.download.DownloadResult -import ac.mdiq.podcini.model.feed.Feed -import ac.mdiq.podcini.model.feed.FeedMedia -import org.greenrobot.eventbus.EventBus - -class DownloadLogDetailsDialog(context: Context, status: DownloadResult) : MaterialAlertDialogBuilder(context) { - init { - var url = "unknown" - if (status.feedfileType == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - val media = DBReader.getFeedMedia(status.feedfileId) - if (media != null) { - url = media.download_url?:"" - } - } else if (status.feedfileType == Feed.FEEDFILETYPE_FEED) { - val feed = DBReader.getFeed(status.feedfileId) - if (feed != null) { - url = feed.download_url?:"" - } - } - - var message = context.getString(R.string.download_successful) - if (!status.isSuccessful) { - message = status.reasonDetailed - } - - val messageFull = context.getString(R.string.download_log_details_message, - context.getString(from(status.reason)), message, url) - setTitle(R.string.download_error_details) - setMessage(messageFull) - setPositiveButton("OK", null) - setNeutralButton(R.string.copy_to_clipboard) { dialog, which -> - val clipboard = getContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText(context.getString(R.string.download_error_details), messageFull) - clipboard.setPrimaryClip(clip) - if (Build.VERSION.SDK_INT < 32) { - EventBus.getDefault().post(MessageEvent(context.getString(R.string.copied_to_clipboard))) - } - } - } - - override fun show(): AlertDialog { - val dialog = super.show() - (dialog.findViewById(R.id.message) as? TextView)?.setTextIsSelectable(true) - return dialog - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/dialog/DrawerPreferencesDialog.kt b/app/src/main/java/ac/mdiq/podcini/dialog/DrawerPreferencesDialog.kt deleted file mode 100644 index 06d97029..00000000 --- a/app/src/main/java/ac/mdiq/podcini/dialog/DrawerPreferencesDialog.kt +++ /dev/null @@ -1,47 +0,0 @@ -package ac.mdiq.podcini.dialog - -import android.content.Context -import android.content.DialogInterface -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.fragment.NavDrawerFragment -import ac.mdiq.podcini.storage.preferences.UserPreferences -import ac.mdiq.podcini.storage.preferences.UserPreferences.defaultPage -import ac.mdiq.podcini.storage.preferences.UserPreferences.hiddenDrawerItems - -object DrawerPreferencesDialog { - fun show(context: Context, callback: Runnable?) { - val hiddenDrawerItems = hiddenDrawerItems?.toMutableList()?: mutableListOf() - val navTitles = context.resources.getStringArray(R.array.nav_drawer_titles) - val checked = BooleanArray(NavDrawerFragment.NAV_DRAWER_TAGS.size) - for (i in NavDrawerFragment.NAV_DRAWER_TAGS.indices) { - val tag = NavDrawerFragment.NAV_DRAWER_TAGS[i] - if (!hiddenDrawerItems.contains(tag)) { - checked[i] = true - } - } - val builder = MaterialAlertDialogBuilder(context) - builder.setTitle(R.string.drawer_preferences) - builder.setMultiChoiceItems(navTitles, checked) { dialog: DialogInterface?, which: Int, isChecked: Boolean -> - if (isChecked) { - hiddenDrawerItems.remove(NavDrawerFragment.NAV_DRAWER_TAGS[which]) - } else { - hiddenDrawerItems.add(NavDrawerFragment.NAV_DRAWER_TAGS[which]) - } - } - builder.setPositiveButton(R.string.confirm_label) { dialog: DialogInterface?, which: Int -> - UserPreferences.hiddenDrawerItems = hiddenDrawerItems - if (hiddenDrawerItems.contains(defaultPage)) { - for (tag in NavDrawerFragment.NAV_DRAWER_TAGS) { - if (!hiddenDrawerItems.contains(tag)) { - defaultPage = tag - break - } - } - } - callback?.run() - } - builder.setNegativeButton(R.string.cancel_label, null) - builder.create().show() - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/dialog/EditUrlSettingsDialog.kt b/app/src/main/java/ac/mdiq/podcini/dialog/EditUrlSettingsDialog.kt deleted file mode 100644 index c23526f9..00000000 --- a/app/src/main/java/ac/mdiq/podcini/dialog/EditUrlSettingsDialog.kt +++ /dev/null @@ -1,82 +0,0 @@ -package ac.mdiq.podcini.dialog - -import android.app.Activity -import android.content.DialogInterface -import android.os.CountDownTimer -import android.view.LayoutInflater -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.storage.DBWriter -import ac.mdiq.podcini.core.util.download.FeedUpdateManager.runOnce -import ac.mdiq.podcini.databinding.EditTextDialogBinding -import ac.mdiq.podcini.model.feed.Feed -import androidx.media3.common.util.UnstableApi -import java.lang.ref.WeakReference -import java.util.* -import java.util.concurrent.ExecutionException - - @UnstableApi - abstract class EditUrlSettingsDialog(activity: Activity, private val feed: Feed) { - private val activityRef = WeakReference(activity) - - fun show() { - val activity = activityRef.get() ?: return - - val binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity)) - - binding.urlEditText.setText(feed.download_url) - - MaterialAlertDialogBuilder(activity) - .setView(binding.root) - .setTitle(R.string.edit_url_menu) - .setPositiveButton(android.R.string.ok) { d: DialogInterface?, input: Int -> showConfirmAlertDialog(binding.urlEditText.text.toString()) } - .setNegativeButton(R.string.cancel_label, null) - .show() - } - - @UnstableApi private fun onConfirmed(original: String, updated: String) { - try { - DBWriter.updateFeedDownloadURL(original, updated).get() - feed.download_url = updated - runOnce(activityRef.get()!!, feed) - } catch (e: ExecutionException) { - throw RuntimeException(e) - } catch (e: InterruptedException) { - throw RuntimeException(e) - } - } - - @UnstableApi private fun showConfirmAlertDialog(url: String) { - val activity = activityRef.get() - - val alertDialog = MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.edit_url_menu) - .setMessage(R.string.edit_url_confirmation_msg) - .setPositiveButton(android.R.string.ok) { d: DialogInterface?, input: Int -> - onConfirmed(feed.download_url?:"", url) - setUrl(url) - } - .setNegativeButton(R.string.cancel_label, null) - .show() - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false - - object : CountDownTimer(15000, 1000) { - override fun onTick(millisUntilFinished: Long) { - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).text = String.format(Locale.getDefault(), "%s (%d)", - activity.getString(android.R.string.ok), millisUntilFinished / 1000 + 1) - } - - override fun onFinish() { - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(android.R.string.ok) - } - }.start() - } - - protected abstract fun setUrl(url: String?) - - companion object { - const val TAG: String = "EditUrlSettingsDialog" - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/dialog/EpisodeFilterDialog.kt b/app/src/main/java/ac/mdiq/podcini/dialog/EpisodeFilterDialog.kt deleted file mode 100644 index 8e652944..00000000 --- a/app/src/main/java/ac/mdiq/podcini/dialog/EpisodeFilterDialog.kt +++ /dev/null @@ -1,111 +0,0 @@ -package ac.mdiq.podcini.dialog - -import android.content.Context -import android.content.DialogInterface -import android.text.TextUtils -import android.view.LayoutInflater -import android.view.View -import android.widget.CompoundButton -import androidx.recyclerview.widget.GridLayoutManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.adapter.SimpleChipAdapter -import ac.mdiq.podcini.databinding.EpisodeFilterDialogBinding -import ac.mdiq.podcini.model.feed.FeedFilter -import ac.mdiq.podcini.view.ItemOffsetDecoration - -/** - * Displays a dialog with a text box for filtering episodes and two radio buttons for exclusion/inclusion - */ -abstract class EpisodeFilterDialog(context: Context?, filter: FeedFilter) : MaterialAlertDialogBuilder( - context!!) { - private val viewBinding = EpisodeFilterDialogBinding.inflate(LayoutInflater.from(context)) - private val termList: MutableList - - init { - setTitle(R.string.episode_filters_label) - setView(viewBinding.root) - - viewBinding.durationCheckBox.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean -> - viewBinding.episodeFilterDurationText.isEnabled = isChecked - } - if (filter.hasMinimalDurationFilter()) { - viewBinding.durationCheckBox.isChecked = true - // Store minimal duration in seconds, show in minutes - viewBinding.episodeFilterDurationText - .setText((filter.minimalDurationFilter / 60).toString()) - } else { - viewBinding.episodeFilterDurationText.isEnabled = false - } - - if (filter.excludeOnly()) { - termList = filter.getExcludeFilter().toMutableList() - viewBinding.excludeRadio.isChecked = true - } else { - termList = filter.getIncludeFilter().toMutableList() - viewBinding.includeRadio.isChecked = true - } - setupWordsList() - - setNegativeButton(R.string.cancel_label, null) - setPositiveButton(R.string.confirm_label) { dialog: DialogInterface, which: Int -> - this.onConfirmClick(dialog, - which) - } - } - - private fun setupWordsList() { - viewBinding.termsRecycler.layoutManager = GridLayoutManager(context, 2) - viewBinding.termsRecycler.addItemDecoration(ItemOffsetDecoration(context, 4)) - val adapter: SimpleChipAdapter = object : SimpleChipAdapter(context) { - override fun getChips(): List { - return termList - } - - override fun onRemoveClicked(position: Int) { - termList.removeAt(position) - notifyDataSetChanged() - } - } - viewBinding.termsRecycler.adapter = adapter - viewBinding.termsTextInput.setEndIconOnClickListener { v: View? -> - val newWord = viewBinding.termsTextInput.editText!!.text.toString().replace("\"", "").trim { it <= ' ' } - if (TextUtils.isEmpty(newWord) || termList.contains(newWord)) { - return@setEndIconOnClickListener - } - termList.add(newWord) - viewBinding.termsTextInput.editText!!.setText("") - adapter.notifyDataSetChanged() - } - } - - protected abstract fun onConfirmed(filter: FeedFilter) - - private fun onConfirmClick(dialog: DialogInterface, which: Int) { - var minimalDuration = -1 - if (viewBinding.durationCheckBox.isChecked) { - try { - // Store minimal duration in seconds - minimalDuration = viewBinding.episodeFilterDurationText.text.toString().toInt() * 60 - } catch (e: NumberFormatException) { - // Do not change anything on error - } - } - var excludeFilter = "" - var includeFilter = "" - if (viewBinding.includeRadio.isChecked) { - includeFilter = toFilterString(termList) - } else { - excludeFilter = toFilterString(termList) - } - onConfirmed(FeedFilter(includeFilter, excludeFilter, minimalDuration)) - } - - private fun toFilterString(words: List?): String { - val result = StringBuilder() - for (word in words!!) { - result.append("\"").append(word).append("\" ") - } - return result.toString() - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/dialog/FeedItemFilterDialog.kt b/app/src/main/java/ac/mdiq/podcini/dialog/FeedItemFilterDialog.kt deleted file mode 100644 index 93b90a5f..00000000 --- a/app/src/main/java/ac/mdiq/podcini/dialog/FeedItemFilterDialog.kt +++ /dev/null @@ -1,25 +0,0 @@ -package ac.mdiq.podcini.dialog - -import android.os.Bundle -import ac.mdiq.podcini.core.storage.DBWriter -import ac.mdiq.podcini.model.feed.Feed - -class FeedItemFilterDialog : ItemFilterDialog() { - override fun onFilterChanged(newFilterValues: Set) { - val feedId = requireArguments().getLong(ARGUMENT_FEED_ID) - DBWriter.setFeedItemsFilter(feedId, newFilterValues) - } - - companion object { - private const val ARGUMENT_FEED_ID = "feedId" - - fun newInstance(feed: Feed): FeedItemFilterDialog { - val dialog = FeedItemFilterDialog() - val arguments = Bundle() - arguments.putSerializable(ARGUMENT_FILTER, feed.itemFilter) - arguments.putLong(ARGUMENT_FEED_ID, feed.id) - dialog.arguments = arguments - return dialog - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/dialog/FeedPreferenceSkipDialog.kt b/app/src/main/java/ac/mdiq/podcini/dialog/FeedPreferenceSkipDialog.kt deleted file mode 100644 index 9fa9f1ee..00000000 --- a/app/src/main/java/ac/mdiq/podcini/dialog/FeedPreferenceSkipDialog.kt +++ /dev/null @@ -1,44 +0,0 @@ -package ac.mdiq.podcini.dialog - -import android.content.Context -import android.content.DialogInterface -import android.view.View -import android.widget.EditText -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R - -/** - * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences. - */ -abstract class FeedPreferenceSkipDialog(context: Context?, skipIntroInitialValue: Int, - skipEndInitialValue: Int -) : MaterialAlertDialogBuilder(context!!) { - init { - setTitle(R.string.pref_feed_skip) - val rootView = View.inflate(context, R.layout.feed_pref_skip_dialog, null) - setView(rootView) - - val etxtSkipIntro = rootView.findViewById(R.id.etxtSkipIntro) - val etxtSkipEnd = rootView.findViewById(R.id.etxtSkipEnd) - - etxtSkipIntro.setText(skipIntroInitialValue.toString()) - etxtSkipEnd.setText(skipEndInitialValue.toString()) - - setNegativeButton(R.string.cancel_label, null) - setPositiveButton(R.string.confirm_label) { dialog: DialogInterface?, which: Int -> - var skipIntro = try { - etxtSkipIntro.text.toString().toInt() - } catch (e: NumberFormatException) { - 0 - } - var skipEnding = try { - etxtSkipEnd.text.toString().toInt() - } catch (e: NumberFormatException) { - 0 - } - onConfirmed(skipIntro, skipEnding) - } - } - - protected abstract fun onConfirmed(skipIntro: Int, skipEndig: Int) -} diff --git a/app/src/main/java/ac/mdiq/podcini/dialog/FeedSortDialog.kt b/app/src/main/java/ac/mdiq/podcini/dialog/FeedSortDialog.kt deleted file mode 100644 index b4fe5c02..00000000 --- a/app/src/main/java/ac/mdiq/podcini/dialog/FeedSortDialog.kt +++ /dev/null @@ -1,34 +0,0 @@ -package ac.mdiq.podcini.dialog - -import android.content.Context -import android.content.DialogInterface -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.event.UnreadItemsUpdateEvent -import ac.mdiq.podcini.storage.preferences.UserPreferences.feedOrder -import ac.mdiq.podcini.storage.preferences.UserPreferences.setFeedOrder -import org.greenrobot.eventbus.EventBus - -object FeedSortDialog { - fun showDialog(context: Context) { - val dialog = MaterialAlertDialogBuilder(context) - dialog.setTitle(context.getString(R.string.pref_nav_drawer_feed_order_title)) - dialog.setNegativeButton(android.R.string.cancel) { d: DialogInterface, listener: Int -> d.dismiss() } - - val selected = feedOrder - val entryValues = - listOf(*context.resources.getStringArray(R.array.nav_drawer_feed_order_values)) - val selectedIndex = entryValues.indexOf("" + selected) - - val items = context.resources.getStringArray(R.array.nav_drawer_feed_order_options) - dialog.setSingleChoiceItems(items, selectedIndex) { d: DialogInterface, which: Int -> - if (selectedIndex != which) { - setFeedOrder(entryValues[which]) - //Update subscriptions - EventBus.getDefault().post(UnreadItemsUpdateEvent()) - } - d.dismiss() - } - dialog.show() - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/dialog/ItemFilterDialog.kt b/app/src/main/java/ac/mdiq/podcini/dialog/ItemFilterDialog.kt deleted file mode 100644 index 83693d12..00000000 --- a/app/src/main/java/ac/mdiq/podcini/dialog/ItemFilterDialog.kt +++ /dev/null @@ -1,116 +0,0 @@ -package ac.mdiq.podcini.dialog - -import android.app.Dialog -import android.content.DialogInterface -import android.os.Bundle -import android.text.TextUtils -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.FrameLayout -import android.widget.LinearLayout -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.google.android.material.button.MaterialButtonToggleGroup -import ac.mdiq.podcini.R -import ac.mdiq.podcini.core.feed.FeedItemFilterGroup -import ac.mdiq.podcini.databinding.FilterDialogBinding -import ac.mdiq.podcini.databinding.FilterDialogRowBinding -import ac.mdiq.podcini.model.feed.FeedItemFilter - -abstract class ItemFilterDialog : BottomSheetDialogFragment() { - private var rows: LinearLayout? = null - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val layout = inflater.inflate(R.layout.filter_dialog, null, false) - val binding = FilterDialogBinding.bind(layout) - rows = binding.filterRows - val filter = requireArguments().getSerializable(ARGUMENT_FILTER) as FeedItemFilter? - - //add filter rows - for (item in FeedItemFilterGroup.entries) { - val rowBinding = FilterDialogRowBinding.inflate(inflater) - rowBinding.root.addOnButtonCheckedListener { group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean -> - onFilterChanged(newFilterValues) - } - rowBinding.filterButton1.setText(item.values[0].displayName) - rowBinding.filterButton1.tag = item.values[0].filterId - rowBinding.filterButton2.setText(item.values[1].displayName) - rowBinding.filterButton2.tag = item.values[1].filterId - rowBinding.filterButton1.maxLines = 3 - rowBinding.filterButton1.isSingleLine = false - rowBinding.filterButton2.maxLines = 3 - rowBinding.filterButton2.isSingleLine = false - rows!!.addView(rowBinding.root, rows!!.childCount - 1) - } - - binding.confirmFiltermenu.setOnClickListener { view1: View? -> dismiss() } - binding.resetFiltermenu.setOnClickListener { view1: View? -> - onFilterChanged(emptySet()) - for (i in 0 until rows!!.childCount) { - if (rows!!.getChildAt(i) is MaterialButtonToggleGroup) { - (rows!!.getChildAt(i) as MaterialButtonToggleGroup).clearChecked() - } - } - } - - for (filterId in filter!!.values) { - if (!TextUtils.isEmpty(filterId)) { - val button = layout.findViewWithTag