From 46ebf21c060f2d117665c3acb43ef5fe9ec98405 Mon Sep 17 00:00:00 2001 From: Michatec Date: Sat, 28 Mar 2026 18:36:50 +0100 Subject: [PATCH] Add support for Android TV and update dependencies * Implement initial Android TV support, including `LEANBACK_LAUNCHER` intent filter, hardware feature declarations, and television-specific layouts for the player, search results, and dialogs. * Add a splash/loading screen for the TV interface and a dedicated `SplashTheme`. * Improve DPAD navigation by adding `OnKeyListener` for station cards and allowing focus on internal elements. * Update `LayoutHolder` and `PlayerFragment` to handle TV layouts and add previous/next station navigation buttons. * Adjust `PreferencesHelper` to disable station editing by default on TV devices. * Update `androidx.media3` to v1.10.0, `work-runtime-ktx` to v2.11.2, and add `androidx.leanback` dependency. * Bump `versionCode` to 144 and `versionName` to 14.4. * Refactor `PlayerService` to simplify sleep timer cancellation logic. * Remove stale copyright headers and license comments from several Kotlin files. --- app/build.gradle | 5 +- app/src/main/AndroidManifest.xml | 12 +- app/src/main/java/com/michatec/radio/Keys.kt | 14 -- .../java/com/michatec/radio/MainActivity.kt | 14 ++ .../java/com/michatec/radio/PlayerFragment.kt | 21 +- .../java/com/michatec/radio/PlayerService.kt | 7 +- .../com/michatec/radio/SettingsFragment.kt | 20 +- .../radio/collection/CollectionAdapter.kt | 37 ++- .../radio/dialogs/AddStationDialog.kt | 33 ++- .../radio/dialogs/FindStationDialog.kt | 40 +++- .../radio/helpers/PreferencesHelper.kt | 11 +- .../com/michatec/radio/ui/LayoutHolder.kt | 219 ++++++++---------- .../color/selector_card_station_stroke.xml | 5 + .../selector_card_station_background.xml | 5 + .../drawable/selector_card_station_stroke.xml | 5 + .../selector_generic_button_focus.xml | 11 + .../drawable/selector_search_result_item.xml | 3 +- .../shape_player_button_small_selected.xml | 4 +- app/src/main/res/drawable/splash_screen.xml | 12 + .../bottom_sheet_playback_controls.xml | 135 +++++++++++ .../layout-television/dialog_add_station.xml | 54 +++++ .../layout-television/dialog_find_station.xml | 88 +++++++ .../dialog_generic_with_details.xml | 57 +++++ .../element_search_result.xml | 61 +++++ .../res/layout-television/fragment_player.xml | 206 ++++++++++++++++ app/src/main/res/layout/activity_main.xml | 45 ++++ .../main/res/layout/card_add_new_station.xml | 4 + app/src/main/res/values-night-v31/styles.xml | 8 +- app/src/main/res/values-night/colors.xml | 1 - app/src/main/res/values-television/styles.xml | 8 + app/src/main/res/values/colors.xml | 2 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/styles.xml | 29 ++- gradle/libs.versions.toml | 8 +- 34 files changed, 990 insertions(+), 196 deletions(-) create mode 100644 app/src/main/res/color/selector_card_station_stroke.xml create mode 100644 app/src/main/res/drawable/selector_card_station_background.xml create mode 100644 app/src/main/res/drawable/selector_card_station_stroke.xml create mode 100644 app/src/main/res/drawable/selector_generic_button_focus.xml create mode 100644 app/src/main/res/drawable/splash_screen.xml create mode 100644 app/src/main/res/layout-television/bottom_sheet_playback_controls.xml create mode 100644 app/src/main/res/layout-television/dialog_add_station.xml create mode 100644 app/src/main/res/layout-television/dialog_find_station.xml create mode 100644 app/src/main/res/layout-television/dialog_generic_with_details.xml create mode 100644 app/src/main/res/layout-television/element_search_result.xml create mode 100644 app/src/main/res/layout-television/fragment_player.xml create mode 100644 app/src/main/res/values-television/styles.xml diff --git a/app/build.gradle b/app/build.gradle index 88341dc..9a63b06 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -19,8 +19,8 @@ android { applicationId 'com.michatec.radio' minSdk 28 targetSdk 36 - versionCode 143 - versionName '14.3' + versionCode 144 + versionName '14.4' resourceConfigurations += ['en', 'de', 'el', 'nl', 'pl', 'ru','uk', 'ja', 'da', 'fr'] } @@ -71,6 +71,7 @@ dependencies { implementation libs.navigation.fragment.ktx implementation libs.navigation.ui.ktx implementation libs.work.runtime.ktx + implementation libs.leanback implementation libs.freedroidwarn diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e3460ec..6f04711 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,13 @@ + + + @@ -10,6 +17,7 @@ - + + diff --git a/app/src/main/java/com/michatec/radio/Keys.kt b/app/src/main/java/com/michatec/radio/Keys.kt index 63ceec4..fa14716 100644 --- a/app/src/main/java/com/michatec/radio/Keys.kt +++ b/app/src/main/java/com/michatec/radio/Keys.kt @@ -1,17 +1,3 @@ -/* - * Keys.kt - * Implements the keys used throughout the app - * This object hosts all keys used to control Radio state - * - * This file is part of - * TRANSISTOR - Radio App for Android - * - * Copyright (c) 2015-22 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - */ - - package com.michatec.radio import java.util.* diff --git a/app/src/main/java/com/michatec/radio/MainActivity.kt b/app/src/main/java/com/michatec/radio/MainActivity.kt index 45533f6..eef2d37 100644 --- a/app/src/main/java/com/michatec/radio/MainActivity.kt +++ b/app/src/main/java/com/michatec/radio/MainActivity.kt @@ -15,7 +15,11 @@ package com.michatec.radio import android.content.SharedPreferences +import android.content.pm.PackageManager import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.navigation.fragment.NavHostFragment @@ -38,6 +42,7 @@ class MainActivity : AppCompatActivity() { /* Overrides onCreate from AppCompatActivity */ override fun onCreate(savedInstanceState: Bundle?) { + setTheme(R.style.AppTheme) super.onCreate(savedInstanceState) // Free Android @@ -58,6 +63,15 @@ class MainActivity : AppCompatActivity() { NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration) supportActionBar?.hide() + // TV-specific loading logic + if (packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { + Handler(Looper.getMainLooper()).postDelayed({ + findViewById(R.id.loading_layout)?.visibility = View.GONE + }, 1500) + } else { + findViewById(R.id.loading_layout)?.visibility = View.GONE + } + // register listener for changes in shared preferences PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener) } diff --git a/app/src/main/java/com/michatec/radio/PlayerFragment.kt b/app/src/main/java/com/michatec/radio/PlayerFragment.kt index 8d67a68..db8bd85 100644 --- a/app/src/main/java/com/michatec/radio/PlayerFragment.kt +++ b/app/src/main/java/com/michatec/radio/PlayerFragment.kt @@ -462,7 +462,7 @@ class PlayerFragment : Fragment(), swipeToMarkStarredItemTouchHelper.attachToRecyclerView(layout.recyclerView) // set up sleep timer start button - layout.sheetSleepTimerStartButtonView.setOnClickListener { + layout.sheetSleepTimerStartButtonView?.setOnClickListener { when (controller?.isPlaying) { true -> { val timePicker = MaterialTimePicker.Builder() @@ -492,12 +492,28 @@ class PlayerFragment : Fragment(), } // set up sleep timer cancel button - layout.sheetSleepTimerCancelButtonView.setOnClickListener { + layout.sheetSleepTimerCancelButtonView?.setOnClickListener { playerState.sleepTimerRunning = false controller?.cancelSleepTimer() togglePeriodicSleepTimerUpdateRequest() } + // set up TV station navigation + layout.playerPrevButtonView?.setOnClickListener { + val currentPosition = CollectionHelper.getStationPosition(collection, playerState.stationUuid) + if (currentPosition > 0) { + val prevStation = collection.stations[currentPosition - 1] + onPlayButtonTapped(prevStation.uuid) + } + } + layout.playerNextButtonView?.setOnClickListener { + val currentPosition = CollectionHelper.getStationPosition(collection, playerState.stationUuid) + if (currentPosition < collection.stations.size - 1) { + val nextStation = collection.stations[currentPosition + 1] + onPlayButtonTapped(nextStation.uuid) + } + } + } /* Sets up the player */ @@ -740,6 +756,7 @@ class PlayerFragment : Fragment(), layout.showBufferingIndicator(buffering = false) } else { // playback is paused or stopped + layout.updateSleepTimer(activity as Context, 0L) // check if buffering (playback is not active but playWhenReady is true) if (controller?.playWhenReady == true) { // playback is buffering, show the buffering indicator diff --git a/app/src/main/java/com/michatec/radio/PlayerService.kt b/app/src/main/java/com/michatec/radio/PlayerService.kt index 060ed77..caacf5d 100644 --- a/app/src/main/java/com/michatec/radio/PlayerService.kt +++ b/app/src/main/java/com/michatec/radio/PlayerService.kt @@ -210,12 +210,9 @@ class PlayerService : MediaLibraryService() { /* Cancels sleep timer */ private fun cancelSleepTimer() { if (this::sleepTimer.isInitialized) { - if (manuallyCancelledSleepTimer) { - sleepTimerTimeRemaining = 0L - sleepTimer.cancel() - } - manuallyCancelledSleepTimer = false + sleepTimer.cancel() } + sleepTimerTimeRemaining = 0L // store timer state PreferencesHelper.saveSleepTimerRunning(isRunning = false) } diff --git a/app/src/main/java/com/michatec/radio/SettingsFragment.kt b/app/src/main/java/com/michatec/radio/SettingsFragment.kt index 92e8ac9..46dc0d0 100644 --- a/app/src/main/java/com/michatec/radio/SettingsFragment.kt +++ b/app/src/main/java/com/michatec/radio/SettingsFragment.kt @@ -1,17 +1,3 @@ -/* - * SettingsFragment.kt - * Implements the SettingsFragment fragment - * A SettingsFragment displays the user accessible settings of the app - * - * This file is part of - * TRANSISTOR - Radio App for Android - * - * Copyright (c) 2015-22 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - */ - - package com.michatec.radio import android.app.Activity @@ -92,6 +78,8 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList val index: Int = preference.entryValues.indexOf(newValue) preferenceThemeSelection.summary = "${getString(R.string.pref_theme_selection_summary)} ${preference.entries[index]}" + + AppThemeHelper.setTheme(newValue as String) return@setOnPreferenceChangeListener true } else { return@setOnPreferenceChangeListener false @@ -193,7 +181,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList preferenceEnableEditingStreamUri.key = Keys.PREF_EDIT_STREAMS_URIS preferenceEnableEditingStreamUri.summaryOn = getString(R.string.pref_edit_station_stream_summary_enabled) preferenceEnableEditingStreamUri.summaryOff = getString(R.string.pref_edit_station_stream_summary_disabled) - preferenceEnableEditingStreamUri.setDefaultValue(PreferencesHelper.loadEditStreamUrisEnabled()) + preferenceEnableEditingStreamUri.setDefaultValue(PreferencesHelper.loadEditStreamUrisEnabled(context)) // set up "Edit Stations" preference @@ -203,7 +191,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList preferenceEnableEditingGeneral.key = Keys.PREF_EDIT_STATIONS preferenceEnableEditingGeneral.summaryOn = getString(R.string.pref_edit_station_summary_enabled) preferenceEnableEditingGeneral.summaryOff = getString(R.string.pref_edit_station_summary_disabled) - preferenceEnableEditingGeneral.setDefaultValue(PreferencesHelper.loadEditStationsEnabled()) + preferenceEnableEditingGeneral.setDefaultValue(PreferencesHelper.loadEditStationsEnabled(context)) preferenceEnableEditingGeneral.setOnPreferenceChangeListener { _, newValue -> when (newValue) { true -> { diff --git a/app/src/main/java/com/michatec/radio/collection/CollectionAdapter.kt b/app/src/main/java/com/michatec/radio/collection/CollectionAdapter.kt index c59c6e3..25667fb 100644 --- a/app/src/main/java/com/michatec/radio/collection/CollectionAdapter.kt +++ b/app/src/main/java/com/michatec/radio/collection/CollectionAdapter.kt @@ -19,6 +19,7 @@ import android.content.Context import android.content.SharedPreferences import android.text.Editable import android.text.TextWatcher +import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -62,8 +63,8 @@ class CollectionAdapter( /* Main class variables */ private lateinit var collectionViewModel: CollectionViewModel private var collection: Collection = Collection() - private var editStationsEnabled: Boolean = PreferencesHelper.loadEditStationsEnabled() - private var editStationStreamsEnabled: Boolean = PreferencesHelper.loadEditStreamUrisEnabled() + private var editStationsEnabled: Boolean = PreferencesHelper.loadEditStationsEnabled(context) + private var editStationStreamsEnabled: Boolean = PreferencesHelper.loadEditStreamUrisEnabled(context) private var expandedStationUuid: String = PreferencesHelper.loadStationListStreamUuid() private var expandedStationPosition: Int = -1 var isExpandedForEdit: Boolean = false @@ -214,6 +215,8 @@ class CollectionAdapter( stationViewHolder.stationNameEditView.imeOptions = EditorInfo.IME_ACTION_DONE } + // Allow internal focus + stationViewHolder.stationCardView.descendantFocusability = ViewGroup.FOCUS_AFTER_DESCENDANTS } // hide edit views else -> { @@ -222,6 +225,8 @@ class CollectionAdapter( stationViewHolder.stationStarredView.isVisible = station.starred stationViewHolder.editViews.isGone = true stationViewHolder.stationUriEditView.isGone = true + // Block internal focus + stationViewHolder.stationCardView.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS } } } @@ -387,6 +392,7 @@ class CollectionAdapter( false -> stationViewHolder.playButtonView.visibility = View.INVISIBLE } stationViewHolder.stationCardView.setOnClickListener { + if (expandedStationPosition == stationViewHolder.bindingAdapterPosition) return@setOnClickListener collectionAdapterListener.onPlayButtonTapped(station.uuid) } stationViewHolder.playButtonView.setOnClickListener { @@ -401,6 +407,29 @@ class CollectionAdapter( stationViewHolder.stationImageView.setOnClickListener { collectionAdapterListener.onPlayButtonTapped(station.uuid) } + + // TV improvement: Allow opening edit view with DPAD_LEFT + stationViewHolder.stationCardView.setOnKeyListener { _, keyCode, event -> + if (event.action == KeyEvent.ACTION_DOWN) { + when (keyCode) { + KeyEvent.KEYCODE_DPAD_LEFT -> { + if (editStationsEnabled && expandedStationPosition != stationViewHolder.bindingAdapterPosition) { + val position: Int = stationViewHolder.bindingAdapterPosition + toggleEditViews(position, station.uuid) + return@setOnKeyListener true + } + } + KeyEvent.KEYCODE_BACK -> { + if (expandedStationPosition == stationViewHolder.bindingAdapterPosition) { + toggleEditViews(stationViewHolder.bindingAdapterPosition, station.uuid) + return@setOnKeyListener true + } + } + } + } + false + } + stationViewHolder.playButtonView.setOnLongClickListener { if (editStationsEnabled) { val position: Int = stationViewHolder.bindingAdapterPosition @@ -649,9 +678,9 @@ class CollectionAdapter( SharedPreferences.OnSharedPreferenceChangeListener { _, key -> when (key) { Keys.PREF_EDIT_STATIONS -> editStationsEnabled = - PreferencesHelper.loadEditStationsEnabled() + PreferencesHelper.loadEditStationsEnabled(context) Keys.PREF_EDIT_STREAMS_URIS -> editStationStreamsEnabled = - PreferencesHelper.loadEditStreamUrisEnabled() + PreferencesHelper.loadEditStreamUrisEnabled(context) } } /* diff --git a/app/src/main/java/com/michatec/radio/dialogs/AddStationDialog.kt b/app/src/main/java/com/michatec/radio/dialogs/AddStationDialog.kt index d7412eb..3859d90 100644 --- a/app/src/main/java/com/michatec/radio/dialogs/AddStationDialog.kt +++ b/app/src/main/java/com/michatec/radio/dialogs/AddStationDialog.kt @@ -16,7 +16,9 @@ package com.michatec.radio.dialogs import android.content.Context import android.view.LayoutInflater +import android.widget.Button import androidx.appcompat.app.AlertDialog +import androidx.core.view.isGone import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -45,6 +47,8 @@ class AddStationDialog ( /* Main class variables */ private lateinit var dialog: AlertDialog private lateinit var stationSearchResultList: RecyclerView + private var customPositiveButton: Button? = null + private var customNegativeButton: Button? = null private lateinit var searchResultAdapter: SearchResultAdapter private var station: Station = Station() @@ -73,6 +77,10 @@ class AddStationDialog ( // set up list of search results setupRecyclerView(context) + // find custom buttons (for TV layout) + customPositiveButton = view.findViewById(R.id.dialog_positive_button) + customNegativeButton = view.findViewById(R.id.dialog_negative_button) + // add okay ("Add") button builder.setPositiveButton(R.string.dialog_find_station_button_add) { _, _ -> // listen for click on add button @@ -88,6 +96,17 @@ class AddStationDialog ( searchResultAdapter.stopPrePlayback() } + // set up custom buttons if they exist (TV layout) + customPositiveButton?.setOnClickListener { + listener.onAddStationDialog(station) + searchResultAdapter.stopPrePlayback() + dialog.dismiss() + } + customNegativeButton?.setOnClickListener { + searchResultAdapter.stopPrePlayback() + dialog.dismiss() + } + // set dialog view builder.setView(view) @@ -95,8 +114,16 @@ class AddStationDialog ( dialog = builder.create() dialog.show() - // initially disable "Add" button - dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false + // handle button visibility and state + if (customPositiveButton != null) { + // hide default buttons if custom ones are used + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isGone = true + dialog.getButton(AlertDialog.BUTTON_NEGATIVE).isGone = true + customPositiveButton?.isEnabled = false + } else { + // initially disable default "Add" button + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false + } } @@ -117,12 +144,14 @@ class AddStationDialog ( /* Implement activateAddButton to enable the "Add" button */ override fun activateAddButton() { dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true + customPositiveButton?.isEnabled = true } /* Implement deactivateAddButton to disable the "Add" button */ override fun deactivateAddButton() { dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false + customPositiveButton?.isEnabled = false } diff --git a/app/src/main/java/com/michatec/radio/dialogs/FindStationDialog.kt b/app/src/main/java/com/michatec/radio/dialogs/FindStationDialog.kt index 88c62dd..fb80ff5 100644 --- a/app/src/main/java/com/michatec/radio/dialogs/FindStationDialog.kt +++ b/app/src/main/java/com/michatec/radio/dialogs/FindStationDialog.kt @@ -21,6 +21,7 @@ import android.os.Handler import android.os.Looper import android.view.LayoutInflater import android.view.inputmethod.InputMethodManager +import android.widget.Button import android.widget.ProgressBar import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView @@ -63,6 +64,8 @@ class FindStationDialog ( private lateinit var searchRequestProgressIndicator: ProgressBar private lateinit var noSearchResultsTextView: MaterialTextView private lateinit var stationSearchResultList: RecyclerView + private var customPositiveButton: Button? = null + private var customNegativeButton: Button? = null private lateinit var searchResultAdapter: SearchResultAdapter private lateinit var radioBrowserSearch: RadioBrowserSearch private lateinit var directInputCheck: DirectInputCheck @@ -134,6 +137,10 @@ class FindStationDialog ( // set up list of search results setupRecyclerView(context) + // find custom buttons (for TV layout) + customPositiveButton = view.findViewById(R.id.dialog_positive_button) + customNegativeButton = view.findViewById(R.id.dialog_negative_button) + // add okay ("Add") button builder.setPositiveButton(R.string.dialog_find_station_button_add) { _, _ -> // listen for click on add button @@ -152,6 +159,18 @@ class FindStationDialog ( searchResultAdapter.stopPrePlayback() } + // set up custom buttons if they exist (TV layout) + customPositiveButton?.setOnClickListener { + listener.onFindStationDialog(station) + searchResultAdapter.stopPrePlayback() + dialog.dismiss() + } + customNegativeButton?.setOnClickListener { + radioBrowserSearch.stopSearchRequest() + searchResultAdapter.stopPrePlayback() + dialog.dismiss() + } + // listen for input stationSearchBoxView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextChange(query: String): Boolean { @@ -174,10 +193,18 @@ class FindStationDialog ( dialog = builder.create() dialog.show() - // initially disable "Add" button - dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false - dialog.getButton(AlertDialog.BUTTON_POSITIVE).isAllCaps = true - dialog.getButton(AlertDialog.BUTTON_NEGATIVE).isAllCaps = true + // handle button visibility and state + if (customPositiveButton != null) { + // hide default buttons if custom ones are used + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isGone = true + dialog.getButton(AlertDialog.BUTTON_NEGATIVE).isGone = true + customPositiveButton?.isEnabled = false + } else { + // initially disable default "Add" button + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isAllCaps = true + dialog.getButton(AlertDialog.BUTTON_NEGATIVE).isAllCaps = true + } } @@ -242,12 +269,14 @@ class FindStationDialog ( /* Makes the "Add" button clickable */ override fun activateAddButton() { dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true + customPositiveButton?.isEnabled = true searchRequestProgressIndicator.isGone = true noSearchResultsTextView.isGone = true } override fun deactivateAddButton() { dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false + customPositiveButton?.isEnabled = false searchRequestProgressIndicator.isGone = true noSearchResultsTextView.isGone = true } @@ -256,6 +285,7 @@ class FindStationDialog ( /* Resets the dialog layout to default state */ private fun resetLayout(clearAdapter: Boolean = false) { dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false + customPositiveButton?.isEnabled = false searchRequestProgressIndicator.isGone = true noSearchResultsTextView.isGone = true searchResultAdapter.resetSelection(clearAdapter) @@ -265,6 +295,7 @@ class FindStationDialog ( /* Display the "No Results" error - hide other unneeded views */ private fun showNoResultsError() { dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false + customPositiveButton?.isEnabled = false searchRequestProgressIndicator.isGone = true noSearchResultsTextView.isVisible = true } @@ -273,6 +304,7 @@ class FindStationDialog ( /* Display the "No Results" error - hide other unneeded views */ private fun showProgressIndicator() { dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false + customPositiveButton?.isEnabled = false searchRequestProgressIndicator.isVisible = true noSearchResultsTextView.isGone = true } diff --git a/app/src/main/java/com/michatec/radio/helpers/PreferencesHelper.kt b/app/src/main/java/com/michatec/radio/helpers/PreferencesHelper.kt index 9818f4e..b36f409 100644 --- a/app/src/main/java/com/michatec/radio/helpers/PreferencesHelper.kt +++ b/app/src/main/java/com/michatec/radio/helpers/PreferencesHelper.kt @@ -16,6 +16,7 @@ package com.michatec.radio.helpers import android.content.Context import android.content.SharedPreferences +import android.content.pm.PackageManager import android.util.Log import androidx.core.content.edit import androidx.preference.PreferenceManager @@ -223,13 +224,15 @@ object PreferencesHelper { /* Loads value of the option: Edit Stations */ - fun loadEditStationsEnabled(): Boolean { - return sharedPreferences.getBoolean(Keys.PREF_EDIT_STATIONS, true) + fun loadEditStationsEnabled(context: Context): Boolean { + val defaultValue = !context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + return sharedPreferences.getBoolean(Keys.PREF_EDIT_STATIONS, defaultValue) } /* Loads value of the option: Edit Station Streams */ - fun loadEditStreamUrisEnabled(): Boolean { - return sharedPreferences.getBoolean(Keys.PREF_EDIT_STREAMS_URIS, true) + fun loadEditStreamUrisEnabled(context: Context): Boolean { + val defaultValue = !context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + return sharedPreferences.getBoolean(Keys.PREF_EDIT_STREAMS_URIS, defaultValue) } diff --git a/app/src/main/java/com/michatec/radio/ui/LayoutHolder.kt b/app/src/main/java/com/michatec/radio/ui/LayoutHolder.kt index 2cf6b51..c7e5c6c 100644 --- a/app/src/main/java/com/michatec/radio/ui/LayoutHolder.kt +++ b/app/src/main/java/com/michatec/radio/ui/LayoutHolder.kt @@ -1,17 +1,3 @@ -/* - * LayoutHolder.kt - * Implements the LayoutHolder class - * A LayoutHolder hold references to the main views - * - * This file is part of - * TRANSISTOR - Radio App for Android - * - * Copyright (c) 2015-22 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - */ - - package com.michatec.radio.ui import android.annotation.SuppressLint @@ -54,30 +40,32 @@ data class LayoutHolder(var rootView: View) { /* Main class variables */ var recyclerView: RecyclerView = rootView.findViewById(R.id.station_list) val layoutManager: LinearLayoutManager - private var bottomSheet: ConstraintLayout = rootView.findViewById(R.id.bottom_sheet) + private var bottomSheet: ConstraintLayout? = rootView.findViewById(R.id.bottom_sheet) //private var sheetMetadataViews: Group - private var sleepTimerRunningViews: Group = rootView.findViewById(R.id.sleep_timer_running_views) - private var downloadProgressIndicator: ProgressBar = rootView.findViewById(R.id.download_progress_indicator) - private var stationImageView: ImageView = rootView.findViewById(R.id.station_icon) - private var stationNameView: TextView = rootView.findViewById(R.id.player_station_name) - private var metadataView: TextView = rootView.findViewById(R.id.player_station_metadata) + private var sleepTimerRunningViews: Group? = rootView.findViewById(R.id.sleep_timer_running_views) + private var downloadProgressIndicator: ProgressBar? = rootView.findViewById(R.id.download_progress_indicator) + private var stationImageView: ImageView? = rootView.findViewById(R.id.station_icon) + private var stationNameView: TextView? = rootView.findViewById(R.id.player_station_name) + private var metadataView: TextView? = rootView.findViewById(R.id.player_station_metadata) var playButtonView: ImageButton = rootView.findViewById(R.id.player_play_button) + var playerPrevButtonView: ImageButton? = rootView.findViewById(R.id.player_prev_button) + var playerNextButtonView: ImageButton? = rootView.findViewById(R.id.player_next_button) private var bufferingIndicator: ProgressBar = rootView.findViewById(R.id.player_buffering_indicator) - private var sheetStreamingLinkHeadline: TextView = rootView.findViewById(R.id.sheet_streaming_link_headline) - private var sheetStreamingLinkView: TextView = rootView.findViewById(R.id.sheet_streaming_link) - private var sheetMetadataHistoryHeadline: TextView = rootView.findViewById(R.id.sheet_metadata_headline) - private var sheetMetadataHistoryView: TextView = rootView.findViewById(R.id.sheet_metadata_history) - private var sheetNextMetadataView: ImageButton = rootView.findViewById(R.id.sheet_next_metadata_button) - private var sheetPreviousMetadataView: ImageButton = rootView.findViewById(R.id.sheet_previous_metadata_button) - private var sheetCopyMetadataButtonView: ImageButton = rootView.findViewById(R.id.copy_station_metadata_button) - private var sheetShareLinkButtonView: ImageView = rootView.findViewById(R.id.sheet_share_link_button) - private var sheetBitrateView: TextView = rootView.findViewById(R.id.sheet_bitrate_view) - var sheetSleepTimerStartButtonView: ImageButton = rootView.findViewById(R.id.sleep_timer_start_button) - var sheetSleepTimerCancelButtonView: ImageButton = rootView.findViewById(R.id.sleep_timer_cancel_button) + private var sheetStreamingLinkHeadline: TextView? = rootView.findViewById(R.id.sheet_streaming_link_headline) + private var sheetStreamingLinkView: TextView? = rootView.findViewById(R.id.sheet_streaming_link) + private var sheetMetadataHistoryHeadline: TextView? = rootView.findViewById(R.id.sheet_metadata_headline) + private var sheetMetadataHistoryView: TextView? = rootView.findViewById(R.id.sheet_metadata_history) + private var sheetNextMetadataView: ImageButton? = rootView.findViewById(R.id.sheet_next_metadata_button) + private var sheetPreviousMetadataView: ImageButton? = rootView.findViewById(R.id.sheet_previous_metadata_button) + private var sheetCopyMetadataButtonView: ImageButton? = rootView.findViewById(R.id.copy_station_metadata_button) + private var sheetShareLinkButtonView: ImageView? = rootView.findViewById(R.id.sheet_share_link_button) + private var sheetBitrateView: TextView? = rootView.findViewById(R.id.sheet_bitrate_view) + var sheetSleepTimerStartButtonView: ImageButton? = rootView.findViewById(R.id.sleep_timer_start_button) + var sheetSleepTimerCancelButtonView: ImageButton? = rootView.findViewById(R.id.sleep_timer_cancel_button) private var sheetSleepTimerRemainingTimeView: TextView = rootView.findViewById(R.id.sleep_timer_remaining_time) private var onboardingLayout: ConstraintLayout = rootView.findViewById(R.id.onboarding_layout) - private var bottomSheetBehavior: BottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) + private var bottomSheetBehavior: BottomSheetBehavior? = bottomSheet?.let { BottomSheetBehavior.from(it) } private var metadataHistory: MutableList private var metadataHistoryPosition: Int private var isBuffering: Boolean @@ -97,31 +85,31 @@ data class LayoutHolder(var rootView: View) { recyclerView.itemAnimator = DefaultItemAnimator() // set up metadata history next and previous buttons - sheetPreviousMetadataView.setOnClickListener { + sheetPreviousMetadataView?.setOnClickListener { if (metadataHistory.isNotEmpty()) { if (metadataHistoryPosition > 0) { metadataHistoryPosition -= 1 } else { metadataHistoryPosition = metadataHistory.size - 1 } - sheetMetadataHistoryView.text = metadataHistory[metadataHistoryPosition] + sheetMetadataHistoryView?.text = metadataHistory[metadataHistoryPosition] } } - sheetNextMetadataView.setOnClickListener { + sheetNextMetadataView?.setOnClickListener { if (metadataHistory.isNotEmpty()) { if (metadataHistoryPosition < metadataHistory.size - 1) { metadataHistoryPosition += 1 } else { metadataHistoryPosition = 0 } - sheetMetadataHistoryView.text = metadataHistory[metadataHistoryPosition] + sheetMetadataHistoryView?.text = metadataHistory[metadataHistoryPosition] } } - sheetMetadataHistoryView.setOnLongClickListener { + sheetMetadataHistoryView?.setOnLongClickListener { copyMetadataHistoryToClipboard() return@setOnLongClickListener true } - sheetMetadataHistoryHeadline.setOnLongClickListener { + sheetMetadataHistoryHeadline?.setOnLongClickListener { copyMetadataHistoryToClipboard() return@setOnLongClickListener true } @@ -137,29 +125,29 @@ data class LayoutHolder(var rootView: View) { // set default metadata views, when playback has stopped if (!isPlaying) { - metadataView.text = station.name - sheetMetadataHistoryView.text = station.name + metadataView?.text = station.name + sheetMetadataHistoryView?.text = station.name // sheetMetadataHistoryView.isSelected = true } // update name - stationNameView.text = station.name + stationNameView?.text = station.name // toggle text scrolling (marquee) if necessary - stationNameView.isSelected = isPlaying + stationNameView?.isSelected = isPlaying // reduce the shadow left and right because of scrolling (Marquee) - stationNameView.setFadingEdgeLength(8) + stationNameView?.setFadingEdgeLength(8) // update cover if (station.imageColor != -1) { - stationImageView.setBackgroundColor(station.imageColor) + stationImageView?.setBackgroundColor(station.imageColor) } - stationImageView.setImageBitmap(ImageHelper.getStationImage(context, station.smallImage)) - stationImageView.contentDescription = "${context.getString(R.string.descr_player_station_image)}: ${station.name}" + stationImageView?.setImageBitmap(ImageHelper.getStationImage(context, station.smallImage)) + stationImageView?.contentDescription = "${context.getString(R.string.descr_player_station_image)}: ${station.name}" // update streaming link - sheetStreamingLinkView.text = station.getStreamUri() + sheetStreamingLinkView?.text = station.getStreamUri() val bitrateText: CharSequence = if (station.codec.isNotEmpty()) { if (station.bitrate == 0) { @@ -188,50 +176,50 @@ data class LayoutHolder(var rootView: View) { } // update bitrate - sheetBitrateView.text = bitrateText + sheetBitrateView?.text = bitrateText // update click listeners - sheetStreamingLinkHeadline.setOnClickListener { + sheetStreamingLinkHeadline?.setOnClickListener { copyToClipboard( context, - sheetStreamingLinkView.text + sheetStreamingLinkView?.text ?: "" ) } - sheetStreamingLinkView.setOnClickListener { + sheetStreamingLinkView?.setOnClickListener { copyToClipboard( context, - sheetStreamingLinkView.text + sheetStreamingLinkView?.text ?: "" ) } - sheetMetadataHistoryHeadline.setOnClickListener { + sheetMetadataHistoryHeadline?.setOnClickListener { copyToClipboard( context, - sheetMetadataHistoryView.text + sheetMetadataHistoryView?.text ?: "" ) } - sheetMetadataHistoryView.setOnClickListener { + sheetMetadataHistoryView?.setOnClickListener { copyToClipboard( context, - sheetMetadataHistoryView.text + sheetMetadataHistoryView?.text ?: "" ) } - sheetCopyMetadataButtonView.setOnClickListener { + sheetCopyMetadataButtonView?.setOnClickListener { copyToClipboard( context, - sheetMetadataHistoryView.text + sheetMetadataHistoryView?.text ?: "" ) } - sheetBitrateView.setOnClickListener { + sheetBitrateView?.setOnClickListener { copyToClipboard( context, - sheetBitrateView.text + sheetBitrateView?.text ?: "" ) } - sheetShareLinkButtonView.setOnClickListener { + sheetShareLinkButtonView?.setOnClickListener { val share = Intent.createChooser(Intent().apply { action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TITLE, stationNameView.text) - putExtra(Intent.EXTRA_TEXT, sheetStreamingLinkView.text) + putExtra(Intent.EXTRA_TITLE, stationNameView?.text) + putExtra(Intent.EXTRA_TEXT, sheetStreamingLinkView?.text ?: "") type = "text/plain" }, null) context.startActivity(share) @@ -264,11 +252,11 @@ data class LayoutHolder(var rootView: View) { fun updateMetadata(metadataHistoryList: MutableList?) { if (!metadataHistoryList.isNullOrEmpty()) { metadataHistory = metadataHistoryList - if (metadataHistory.last() != metadataView.text) { + if (metadataHistory.last() != metadataView?.text) { metadataHistoryPosition = metadataHistory.size - 1 val metadataString = metadataHistory[metadataHistoryPosition] - metadataView.text = metadataString - sheetMetadataHistoryView.text = metadataString + metadataView?.text = metadataString + sheetMetadataHistoryView?.text = metadataString } } } @@ -278,14 +266,16 @@ data class LayoutHolder(var rootView: View) { fun updateSleepTimer(context: Context, timeRemaining: Long = 0L) { when (timeRemaining) { 0L -> { - sleepTimerRunningViews.isGone = true + sleepTimerRunningViews?.isGone = true + sheetSleepTimerRemainingTimeView.isVisible = false } else -> { - sleepTimerRunningViews.isVisible = true + sleepTimerRunningViews?.isVisible = true + sheetSleepTimerRemainingTimeView.isVisible = true val sleepTimerTimeRemaining = DateTimeHelper.convertToHoursMinutesSeconds(timeRemaining) sheetSleepTimerRemainingTimeView.text = sleepTimerTimeRemaining sheetSleepTimerRemainingTimeView.contentDescription = "${context.getString(R.string.descr_expanded_player_sleep_timer_remaining_time)}: $sleepTimerTimeRemaining" - stationNameView.isSelected = false + stationNameView?.isSelected = false } } } @@ -297,11 +287,11 @@ data class LayoutHolder(var rootView: View) { playButtonView.setImageResource(R.drawable.ic_audio_waves_animated) val animatedVectorDrawable = playButtonView.drawable as? AnimatedVectorDrawable animatedVectorDrawable?.start() - sheetSleepTimerStartButtonView.isVisible = true + sheetSleepTimerStartButtonView?.isVisible = true // bufferingIndicator.isVisible = false } else { playButtonView.setImageResource(R.drawable.ic_player_play_symbol_42dp) - sheetSleepTimerStartButtonView.isVisible = false + sheetSleepTimerStartButtonView?.isVisible = false // bufferingIndicator.isVisible = isBuffering } } @@ -316,8 +306,8 @@ data class LayoutHolder(var rootView: View) { /* Toggles visibility of the download progress indicator */ fun toggleDownloadProgressIndicator() { when (PreferencesHelper.loadActiveDownloads()) { - Keys.ACTIVE_DOWNLOADS_EMPTY -> downloadProgressIndicator.isGone = true - else -> downloadProgressIndicator.isVisible = true + Keys.ACTIVE_DOWNLOADS_EMPTY -> downloadProgressIndicator?.isGone = true + else -> downloadProgressIndicator?.isVisible = true } } @@ -338,27 +328,20 @@ data class LayoutHolder(var rootView: View) { /* Initiates the rotation animation of the play button */ fun animatePlaybackButtonStateTransition(context: Context, isPlaying: Boolean) { - when (isPlaying) { - true -> { - val rotateClockwise = AnimationUtils.loadAnimation(context, R.anim.rotate_clockwise_slow) - rotateClockwise.setAnimationListener(createAnimationListener(true)) - playButtonView.startAnimation(rotateClockwise) - } - false -> { - val rotateCounterClockwise = AnimationUtils.loadAnimation(context, R.anim.rotate_counterclockwise_fast) - rotateCounterClockwise.setAnimationListener(createAnimationListener(false)) - playButtonView.startAnimation(rotateCounterClockwise) - } - - } + // Toggle play button immediately for snappier feel + togglePlayButton(isPlaying) } /* Shows player */ fun showPlayer(context: Context): Boolean { - UiHelper.setViewMargins(context, recyclerView, 0, 0, 0, Keys.BOTTOM_SHEET_PEEK_HEIGHT) - if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_HIDDEN && onboardingLayout.isGone) { - bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + if (bottomSheetBehavior != null) { + UiHelper.setViewMargins(context, recyclerView, 0, 0, 0, Keys.BOTTOM_SHEET_PEEK_HEIGHT) + if (bottomSheetBehavior?.state == BottomSheetBehavior.STATE_HIDDEN && onboardingLayout.isGone) { + bottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + } + } else { + UiHelper.setViewMargins(context, recyclerView, 0, 0, 0, 0) } return true } @@ -367,15 +350,15 @@ data class LayoutHolder(var rootView: View) { /* Hides player */ private fun hidePlayer(context: Context): Boolean { UiHelper.setViewMargins(context, recyclerView, 0, 0, 0, 0) - bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN + bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN return true } /* Minimizes player sheet if expanded */ fun minimizePlayerIfExpanded(): Boolean { - return if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED) { - bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + return if (bottomSheetBehavior?.state == BottomSheetBehavior.STATE_EXPANDED) { + bottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED true } else { false @@ -399,38 +382,40 @@ data class LayoutHolder(var rootView: View) { /* Sets up the player (BottomSheet) */ private fun setupBottomSheet() { - // show / hide the small player - bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED - bottomSheetBehavior.addBottomSheetCallback(object : - BottomSheetBehavior.BottomSheetCallback() { - override fun onSlide(view: View, slideOffset: Float) { - } - - override fun onStateChanged(view: View, state: Int) { - when (state) { - BottomSheetBehavior.STATE_COLLAPSED -> Unit // do nothing - BottomSheetBehavior.STATE_DRAGGING -> Unit // do nothing - BottomSheetBehavior.STATE_EXPANDED -> Unit // do nothing - BottomSheetBehavior.STATE_HALF_EXPANDED -> Unit // do nothing - BottomSheetBehavior.STATE_SETTLING -> Unit // do nothing - BottomSheetBehavior.STATE_HIDDEN -> showPlayer(rootView.context) + if (bottomSheetBehavior != null) { + // show / hide the small player + bottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + bottomSheetBehavior?.addBottomSheetCallback(object : + BottomSheetBehavior.BottomSheetCallback() { + override fun onSlide(view: View, slideOffset: Float) { } - } - }) - // toggle collapsed state on tap - bottomSheet.setOnClickListener { toggleBottomSheetState() } - stationImageView.setOnClickListener { toggleBottomSheetState() } - stationNameView.setOnClickListener { toggleBottomSheetState() } - metadataView.setOnClickListener { toggleBottomSheetState() } + + override fun onStateChanged(view: View, state: Int) { + when (state) { + BottomSheetBehavior.STATE_COLLAPSED -> Unit // do nothing + BottomSheetBehavior.STATE_DRAGGING -> Unit // do nothing + BottomSheetBehavior.STATE_EXPANDED -> Unit // do nothing + BottomSheetBehavior.STATE_HALF_EXPANDED -> Unit // do nothing + BottomSheetBehavior.STATE_SETTLING -> Unit // do nothing + BottomSheetBehavior.STATE_HIDDEN -> showPlayer(rootView.context) + } + } + }) + // toggle collapsed state on tap + bottomSheet?.setOnClickListener { toggleBottomSheetState() } + stationImageView?.setOnClickListener { toggleBottomSheetState() } + stationNameView?.setOnClickListener { toggleBottomSheetState() } + metadataView?.setOnClickListener { toggleBottomSheetState() } + } } /* Toggle expanded/collapsed state of bottom sheet */ private fun toggleBottomSheetState() { - when (bottomSheetBehavior.state) { - BottomSheetBehavior.STATE_COLLAPSED -> bottomSheetBehavior.state = + when (bottomSheetBehavior?.state) { + BottomSheetBehavior.STATE_COLLAPSED -> bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED - else -> bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + else -> bottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED } } diff --git a/app/src/main/res/color/selector_card_station_stroke.xml b/app/src/main/res/color/selector_card_station_stroke.xml new file mode 100644 index 0000000..8099fff --- /dev/null +++ b/app/src/main/res/color/selector_card_station_stroke.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_card_station_background.xml b/app/src/main/res/drawable/selector_card_station_background.xml new file mode 100644 index 0000000..dd29f16 --- /dev/null +++ b/app/src/main/res/drawable/selector_card_station_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_card_station_stroke.xml b/app/src/main/res/drawable/selector_card_station_stroke.xml new file mode 100644 index 0000000..8099fff --- /dev/null +++ b/app/src/main/res/drawable/selector_card_station_stroke.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_generic_button_focus.xml b/app/src/main/res/drawable/selector_generic_button_focus.xml new file mode 100644 index 0000000..b312104 --- /dev/null +++ b/app/src/main/res/drawable/selector_generic_button_focus.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_search_result_item.xml b/app/src/main/res/drawable/selector_search_result_item.xml index 40bf35c..b605d2a 100644 --- a/app/src/main/res/drawable/selector_search_result_item.xml +++ b/app/src/main/res/drawable/selector_search_result_item.xml @@ -1,8 +1,9 @@ - + + diff --git a/app/src/main/res/drawable/shape_player_button_small_selected.xml b/app/src/main/res/drawable/shape_player_button_small_selected.xml index c56ca8b..ecb1c3e 100644 --- a/app/src/main/res/drawable/shape_player_button_small_selected.xml +++ b/app/src/main/res/drawable/shape_player_button_small_selected.xml @@ -1,8 +1,8 @@ + android:width="4dp" + android:color="@color/default_neutral_white" /> diff --git a/app/src/main/res/drawable/splash_screen.xml b/app/src/main/res/drawable/splash_screen.xml new file mode 100644 index 0000000..109f5b6 --- /dev/null +++ b/app/src/main/res/drawable/splash_screen.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-television/bottom_sheet_playback_controls.xml b/app/src/main/res/layout-television/bottom_sheet_playback_controls.xml new file mode 100644 index 0000000..0422245 --- /dev/null +++ b/app/src/main/res/layout-television/bottom_sheet_playback_controls.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-television/dialog_add_station.xml b/app/src/main/res/layout-television/dialog_add_station.xml new file mode 100644 index 0000000..ac4c234 --- /dev/null +++ b/app/src/main/res/layout-television/dialog_add_station.xml @@ -0,0 +1,54 @@ + + + + + + + + + +