diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1ed2b69..c102a01 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,14 +5,14 @@ plugins { android { namespace = "com.michatec.radio" - compileSdk = 36 + compileSdk = 37 defaultConfig { applicationId = "com.michatec.radio" minSdk = 28 - targetSdk = 36 - versionCode = 145 - versionName = "14.5" + targetSdk = 37 + versionCode = 146 + versionName = "14.6" } compileOptions { diff --git a/app/src/main/java/com/michatec/radio/AddStationFragment.kt b/app/src/main/java/com/michatec/radio/AddStationFragment.kt index 6047c3d..13a7ebe 100644 --- a/app/src/main/java/com/michatec/radio/AddStationFragment.kt +++ b/app/src/main/java/com/michatec/radio/AddStationFragment.kt @@ -120,7 +120,7 @@ class AddStationFragment : Fragment(), if (searchEditText != null) { searchEditText.requestFocus() val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT) + imm.showSoftInput(searchEditText, 0) } } } diff --git a/app/src/main/java/com/michatec/radio/PlayerService.kt b/app/src/main/java/com/michatec/radio/PlayerService.kt index 4015265..3fe3645 100644 --- a/app/src/main/java/com/michatec/radio/PlayerService.kt +++ b/app/src/main/java/com/michatec/radio/PlayerService.kt @@ -9,7 +9,6 @@ import android.os.Bundle import android.os.CountDownTimer import android.util.Log import androidx.localbroadcastmanager.content.LocalBroadcastManager -import androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT import androidx.media3.cast.CastPlayer import androidx.media3.common.* import androidx.media3.common.util.UnstableApi @@ -29,6 +28,7 @@ import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.michatec.radio.core.Collection +import com.michatec.radio.core.Station import com.michatec.radio.helpers.* import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers.Main @@ -132,7 +132,7 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc context: Context, enableFloatOutput: Boolean, enableAudioTrackPlaybackParams: Boolean - ): AudioSink? { + ): AudioSink { return DefaultAudioSink.Builder(context) .setAudioProcessors(arrayOf(nativeAudioProcessor)) .build() @@ -170,6 +170,14 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc override fun getDuration(): Long { return C.TIME_UNSET // this will hide progress bar for HLS stations in the notification } + + override fun seekToNext() { + playNextStation() + } + + override fun seekToPrevious() { + playPreviousStation() + } } player.addListener(playerListener) } @@ -347,6 +355,37 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc } + /* Switches to the next radio station in collection */ + private fun playNextStation() { + val currentMediaId = player.currentMediaItem?.mediaId ?: PreferencesHelper.loadLastPlayedStationUuid() + val currentPosition = CollectionHelper.getStationPosition(collection, currentMediaId) + if (currentPosition != -1) { + val nextPosition = if (currentPosition < collection.stations.size - 1) currentPosition + 1 else 0 + playStation(collection.stations[nextPosition]) + } + } + + + /* Switches to the previous radio station in collection */ + private fun playPreviousStation() { + val currentMediaId = player.currentMediaItem?.mediaId ?: PreferencesHelper.loadLastPlayedStationUuid() + val currentPosition = CollectionHelper.getStationPosition(collection, currentMediaId) + if (currentPosition != -1) { + val previousPosition = if (currentPosition > 0) currentPosition - 1 else collection.stations.size - 1 + playStation(collection.stations[previousPosition]) + } + } + + + /* Starts playback of a radio station */ + private fun playStation(station: Station) { + val mediaItem = CollectionHelper.buildMediaItem(this, station) + player.setMediaItem(mediaItem) + player.prepare() + player.play() + } + + /* * Custom MediaSession Callback that handles player commands */ @@ -407,8 +446,8 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc browser: MediaSession.ControllerInfo, params: LibraryParams? ): ListenableFuture> { - return if (params?.extras?.containsKey(EXTRA_RECENT) == true) { - // special case: system requested media resumption via EXTRA_RECENT + return if (params?.isRecent == true) { + // special case: system requested media resumption via isRecent playLastStation = true Futures.immediateFuture(LibraryResult.ofItem(CollectionHelper.getRecent(this@PlayerService, collection), params)) } else { @@ -552,8 +591,7 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc stopSelf() } Player.STATE_READY -> { - // Playback is paused. For radio, we can stop the service to remove the notification. - stopSelf() + // Playback is paused. For radio, we keep the service running to allow resumption from headphones. } Player.STATE_BUFFERING -> { // DO NOT stop the service while buffering (especially important for Cast) @@ -562,17 +600,6 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc } } - override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { - super.onPlayWhenReadyChanged(playWhenReady, reason) - if (!playWhenReady) { - // Only stop if not buffering and not ready to play (i.e. truly stopped/paused) - if (player.playbackState != Player.STATE_BUFFERING) { - stopSelf() - } - } - } - - override fun onPlayerError(error: PlaybackException) { super.onPlayerError(error) Log.d(TAG, "PlayerError occurred: ${error.errorCodeName}") diff --git a/app/src/main/java/com/michatec/radio/helpers/ExtrasHelper.kt b/app/src/main/java/com/michatec/radio/helpers/ExtrasHelper.kt index d46f10a..a61ee95 100644 --- a/app/src/main/java/com/michatec/radio/helpers/ExtrasHelper.kt +++ b/app/src/main/java/com/michatec/radio/helpers/ExtrasHelper.kt @@ -11,6 +11,7 @@ import android.widget.FrameLayout import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import com.michatec.radio.R +import androidx.core.view.isEmpty class ExtrasHelper { companion object { @@ -69,7 +70,7 @@ class ExtrasHelper { if (currentParent != container) { currentParent?.removeView(visualizerView) // If we injected into a standard preference, don't clear everything, just add - if (container is FrameLayout || container.childCount == 0) { + if (container is FrameLayout || container.isEmpty()) { container.removeAllViews() } container.addView(visualizerView) diff --git a/app/src/main/java/com/michatec/radio/helpers/LanguageHelper.kt b/app/src/main/java/com/michatec/radio/helpers/LanguageHelper.kt index 8cfd98b..c57ffbc 100644 --- a/app/src/main/java/com/michatec/radio/helpers/LanguageHelper.kt +++ b/app/src/main/java/com/michatec/radio/helpers/LanguageHelper.kt @@ -2,7 +2,6 @@ package com.michatec.radio.helpers import android.app.Activity import android.content.Context -import android.content.res.Configuration import android.util.Log import com.michatec.radio.R import java.util.Locale diff --git a/app/src/main/java/com/michatec/radio/helpers/MarqueeSwitchPreference.kt b/app/src/main/java/com/michatec/radio/helpers/MarqueeSwitchPreference.kt index 5d28241..0c06a86 100644 --- a/app/src/main/java/com/michatec/radio/helpers/MarqueeSwitchPreference.kt +++ b/app/src/main/java/com/michatec/radio/helpers/MarqueeSwitchPreference.kt @@ -16,7 +16,7 @@ class MarqueeSwitchPreference(context: Context) : SwitchPreferenceCompat(context val title = holder.findViewById(android.R.id.title) as? TextView title?.apply { ellipsize = TextUtils.TruncateAt.MARQUEE - setSingleLine(true) + isSingleLine = true marqueeRepeatLimit = -1 // Repeat indefinitely isSelected = true // Required for marquee to start setHorizontallyScrolling(true) diff --git a/app/src/main/java/com/michatec/radio/search/SearchResultAdapter.kt b/app/src/main/java/com/michatec/radio/search/SearchResultAdapter.kt index 3b55b56..f43c619 100644 --- a/app/src/main/java/com/michatec/radio/search/SearchResultAdapter.kt +++ b/app/src/main/java/com/michatec/radio/search/SearchResultAdapter.kt @@ -171,7 +171,7 @@ class SearchResultAdapter( context: Context, enableFloatOutput: Boolean, enableAudioTrackPlaybackParams: Boolean - ): AudioSink? { + ): AudioSink { return DefaultAudioSink.Builder(context) .setAudioProcessors(arrayOf(nativeAudioProcessor)) .build()