1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.tv.settings.device.displaysound 17 18 import android.app.tvsettings.TvSettingsEnums 19 import android.content.ContentResolver 20 import android.content.Context 21 import android.hardware.display.DisplayManager 22 import android.hardware.hdmi.HdmiControlManager 23 import android.media.AudioManager 24 import android.os.Bundle 25 import android.provider.Settings 26 import android.text.TextUtils 27 import android.view.Display 28 import android.view.LayoutInflater 29 import android.view.View 30 import android.view.ViewGroup 31 import androidx.annotation.Keep 32 import androidx.annotation.VisibleForTesting 33 import androidx.lifecycle.Lifecycle 34 import androidx.lifecycle.lifecycleScope 35 import androidx.lifecycle.repeatOnLifecycle 36 import androidx.preference.Preference 37 import androidx.preference.SwitchPreference 38 import androidx.preference.TwoStatePreference 39 import com.android.tv.settings.R 40 import com.android.tv.settings.SettingsPreferenceFragment 41 import com.android.tv.settings.device.util.DeviceUtils 42 import com.android.tv.settings.device.displaysound.PreferredDynamicRangeInfo.MatchContentDynamicRangeInfoFragment 43 import com.android.tv.settings.overlay.FlavorUtils 44 import com.android.tv.settings.util.InstrumentationUtils 45 import com.android.tv.settings.util.ResolutionSelectionUtils 46 import com.android.tv.settings.util.SliceUtils 47 import com.android.tv.settings.util.SliceUtilsKt 48 import com.android.tv.twopanelsettings.slices.SlicePreference 49 import kotlinx.coroutines.launch 50 51 /** 52 * The "Display & sound" screen in TV Settings. 53 */ 54 @Keep 55 class DisplaySoundFragment : SettingsPreferenceFragment(), DisplayManager.DisplayListener { 56 lateinit var mAudioManager: AudioManager 57 lateinit var mHdmiControlManager: HdmiControlManager 58 lateinit var mDisplayManager: DisplayManager 59 private var mCurrentDeviceName: String? = null 60 private var mCurrentMode: Display.Mode? = null 61 onAttachnull62 override fun onAttach(context: Context) { 63 mAudioManager = context.getSystemService(AudioManager::class.java) as AudioManager 64 mHdmiControlManager = 65 context.getSystemService(HdmiControlManager::class.java) as HdmiControlManager 66 mDisplayManager = displayManager 67 super.onAttach(context) 68 } 69 70 private val preferenceScreenResId: Int 71 get() = when (FlavorUtils.getFlavor(context)) { 72 FlavorUtils.FLAVOR_CLASSIC, FlavorUtils.FLAVOR_TWO_PANEL -> R.xml.display_sound 73 FlavorUtils.FLAVOR_X, FlavorUtils.FLAVOR_VENDOR -> R.xml.display_sound_x 74 else -> R.xml.display_sound 75 } 76 onCreatePreferencesnull77 override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 78 setPreferencesFromResource(preferenceScreenResId, null) 79 } 80 onCreateViewnull81 override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 82 savedInstanceState: Bundle?): View { 83 findPreference<TwoStatePreference>(KEY_SOUND_EFFECTS)?.isChecked = soundEffectsEnabled 84 updateCecPreference() 85 86 mCurrentDeviceName = DeviceUtils.getDeviceName(context) 87 updateVolumeChangePreference() 88 89 val display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY) 90 if (display.systemPreferredDisplayMode != null) { 91 mDisplayManager.registerDisplayListener(this, null) 92 mCurrentMode = mDisplayManager.globalUserPreferredDisplayMode 93 updateResolutionTitleDescription(ResolutionSelectionUtils.modeToString( 94 mCurrentMode, context)) 95 } else { 96 removePreference(findPreference(KEY_RESOLUTION_TITLE)) 97 } 98 val dynamicRangePreference = findPreference<SwitchPreference>(KEY_DYNAMIC_RANGE) 99 if (mDisplayManager.supportedHdrOutputTypes.isEmpty()) { 100 removePreference(dynamicRangePreference) 101 } else if (FlavorUtils.getFlavor(context) != FlavorUtils.FLAVOR_CLASSIC) { 102 createInfoFragments() 103 } 104 viewLifecycleOwner.lifecycleScope.launch { 105 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { 106 updateDefaultAudioOutputSettings() 107 } 108 } 109 return checkNotNull(super.onCreateView(inflater, container, savedInstanceState)) 110 } 111 onDestroynull112 override fun onDestroy() { 113 super.onDestroy() 114 if (this::mDisplayManager.isInitialized) { 115 mDisplayManager.unregisterDisplayListener(this) 116 } 117 } 118 onResumenull119 override fun onResume() { 120 super.onResume() 121 // Update the subtitle of CEC setting when navigating back to this page. 122 updateCecPreference() 123 findPreference<SwitchPreference>(KEY_DYNAMIC_RANGE)?.isChecked = 124 DisplaySoundUtils.getMatchContentDynamicRangeStatus(mDisplayManager) 125 } 126 onPreferenceTreeClicknull127 override fun onPreferenceTreeClick(preference: Preference): Boolean { 128 if (TextUtils.equals(preference.key, KEY_SOUND_EFFECTS)) { 129 val soundPref = preference as TwoStatePreference 130 InstrumentationUtils 131 .logToggleInteracted( 132 TvSettingsEnums.DISPLAY_SOUND_SYSTEM_SOUNDS, soundPref.isChecked) 133 soundEffectsEnabled = soundPref.isChecked 134 } else if (TextUtils.equals(preference.key, KEY_DYNAMIC_RANGE)) { 135 val dynamicPref = preference as SwitchPreference 136 DisplaySoundUtils 137 .setMatchContentDynamicRangeStatus(context, mDisplayManager, dynamicPref.isChecked) 138 } 139 return super.onPreferenceTreeClick(preference) 140 } 141 142 private var soundEffectsEnabled: Boolean 143 get() = getSoundEffectsEnabled(requireActivity().contentResolver) 144 private set(enabled) { 145 if (enabled) { 146 mAudioManager.loadSoundEffects() 147 } else { 148 mAudioManager.unloadSoundEffects() 149 } 150 Settings.System.putInt(requireActivity().contentResolver, 151 Settings.System.SOUND_EFFECTS_ENABLED, if (enabled) 1 else 0) 152 } 153 updateCecPreferencenull154 private fun updateCecPreference() { 155 findPreference<Preference>(KEY_CEC)?.apply{ 156 if (this is SlicePreference && SliceUtils.isSliceProviderValid( 157 context, this.uri)) { 158 val cecEnabled = (mHdmiControlManager.getHdmiCecEnabled() 159 == HdmiControlManager.HDMI_CEC_CONTROL_ENABLED) 160 setSummary(if (cecEnabled) R.string.enabled else R.string.disabled) 161 isVisible = true 162 } else { 163 isVisible = false 164 } 165 } 166 } 167 updateDefaultAudioOutputSettingsnull168 private suspend fun updateDefaultAudioOutputSettings() { 169 findPreference<SlicePreference>(KEY_DEFAULT_AUDIO_OUTPUT_SETTINGS_SLICE)?.apply { 170 isVisible = SliceUtils.isSliceProviderValid(context, 171 this.uri) 172 && SliceUtilsKt.isSettingsSliceEnabled(context, 173 this.uri, null) 174 } 175 } 176 updateVolumeChangePreferencenull177 private fun updateVolumeChangePreference() { 178 findPreference<Preference>(VOLUME_CHANGE)?.apply { 179 setTitle(requireContext().resources.getString(R.string.volume_change_settings_title, 180 mCurrentDeviceName)) 181 isVisible = requireContext().resources.getBoolean(R.bool.config_volume_change) 182 } 183 } 184 getPageIdnull185 override fun getPageId(): Int { 186 return TvSettingsEnums.DISPLAY_SOUND 187 } 188 onDisplayAddednull189 override fun onDisplayAdded(displayId: Int) {} onDisplayRemovednull190 override fun onDisplayRemoved(displayId: Int) {} onDisplayChangednull191 override fun onDisplayChanged(displayId: Int) { 192 val newMode = mDisplayManager.globalUserPreferredDisplayMode 193 if (mCurrentMode != newMode) { 194 updateResolutionTitleDescription( 195 ResolutionSelectionUtils.modeToString(newMode, context)) 196 mCurrentMode = newMode 197 } 198 } 199 200 @get:VisibleForTesting 201 val displayManager: DisplayManager 202 get() = requireContext().getSystemService(DisplayManager::class.java) as DisplayManager 203 updateResolutionTitleDescriptionnull204 private fun updateResolutionTitleDescription(summary: String) { 205 findPreference<Preference>(KEY_RESOLUTION_TITLE)?.summary = summary 206 } 207 removePreferencenull208 private fun removePreference(preference: Preference?) { 209 if (preference != null) { 210 preferenceScreen.removePreference(preference) 211 } 212 } 213 createInfoFragmentsnull214 private fun createInfoFragments() { 215 findPreference<Preference>(KEY_DYNAMIC_RANGE)?.fragment = 216 MatchContentDynamicRangeInfoFragment::class.java.name 217 } 218 219 companion object { 220 const val KEY_SOUND_EFFECTS = "sound_effects" 221 private const val KEY_CEC = "cec" 222 private const val KEY_DEFAULT_AUDIO_OUTPUT_SETTINGS_SLICE = "default_audio_output_settings" 223 private const val KEY_RESOLUTION_TITLE = "resolution_selection" 224 private const val KEY_DYNAMIC_RANGE = "match_content_dynamic_range" 225 226 private const val VOLUME_CHANGE = "volume_change" 227 newInstancenull228 fun newInstance(): DisplaySoundFragment { 229 return DisplaySoundFragment() 230 } 231 getSoundEffectsEnablednull232 fun getSoundEffectsEnabled(contentResolver: ContentResolver?): Boolean { 233 return (Settings.System.getInt(contentResolver, Settings.System.SOUND_EFFECTS_ENABLED, 1) 234 != 0) 235 } 236 } 237 }