1 /* 2 * Copyright (C) 2023 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 17 package com.google.android.car.kitchensink.radio; 18 19 import android.annotation.Nullable; 20 import android.hardware.radio.Flags; 21 import android.hardware.radio.ProgramList; 22 import android.hardware.radio.ProgramSelector; 23 import android.hardware.radio.RadioManager; 24 import android.hardware.radio.RadioMetadata; 25 import android.hardware.radio.RadioTuner; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.util.Log; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.Button; 33 import android.widget.CheckBox; 34 import android.widget.ListView; 35 import android.widget.TextView; 36 37 import androidx.fragment.app.Fragment; 38 39 import com.android.car.broadcastradio.support.platform.ProgramInfoExt; 40 41 import com.google.android.car.kitchensink.R; 42 43 import java.util.Comparator; 44 import java.util.List; 45 import java.util.Objects; 46 47 public class RadioTunerFragment extends Fragment { 48 49 private static final String TAG = RadioTunerFragment.class.getSimpleName(); 50 protected static final CharSequence NULL_TUNER_WARNING = "Tuner cannot be null"; 51 protected static final CharSequence TUNING_TEXT = "Tuning..."; 52 private static final CharSequence TUNING_COMPLETION_TEXT = "Tuning completes"; 53 54 protected final RadioTuner mRadioTuner; 55 protected final RadioTestFragment.TunerListener mListener; 56 private final ProgramList mProgramList; 57 protected boolean mViewCreated = false; 58 59 protected ProgramInfoAdapter mProgramInfoAdapter; 60 61 private CheckBox mSeekChannelCheckBox; 62 protected TextView mTuningTextView; 63 private TextView mCurrentStationTextView; 64 protected TextView mCurrentChannelTextView; 65 private TextView mCurrentSongTitleTextView; 66 private TextView mCurrentArtistTextView; 67 RadioTunerFragment(RadioManager radioManager, int moduleId, Handler handler, RadioTestFragment.TunerListener tunerListener)68 RadioTunerFragment(RadioManager radioManager, int moduleId, Handler handler, 69 RadioTestFragment.TunerListener tunerListener) { 70 mRadioTuner = radioManager.openTuner(moduleId, /* config= */ null, /* withAudio= */ true, 71 new RadioTunerCallbackImpl(), handler); 72 mListener = Objects.requireNonNull(tunerListener, "Tuner listener can not be null"); 73 if (mRadioTuner == null) { 74 mProgramList = null; 75 } else { 76 mProgramList = mRadioTuner.getDynamicProgramList(/* filter= */ null); 77 } 78 } 79 getRadioTuner()80 RadioTuner getRadioTuner() { 81 return mRadioTuner; 82 } 83 84 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)85 public View onCreateView(LayoutInflater inflater, ViewGroup container, 86 Bundle savedInstanceState) { 87 Log.i(TAG, "onCreateView"); 88 View view = inflater.inflate(R.layout.radio_tuner_fragment, container, 89 /* attachToRoot= */ false); 90 Button closeButton = view.findViewById(R.id.button_radio_close); 91 Button cancelButton = view.findViewById(R.id.button_radio_cancel); 92 mTuningTextView = view.findViewById(R.id.text_tuning_status); 93 mSeekChannelCheckBox = view.findViewById(R.id.selection_seek_skip_subchannels); 94 Button seekUpButton = view.findViewById(R.id.button_radio_seek_up); 95 Button seekDownButton = view.findViewById(R.id.button_radio_seek_down); 96 ListView programListView = view.findViewById(R.id.radio_program_list); 97 mCurrentStationTextView = view.findViewById(R.id.radio_current_station_info); 98 mCurrentChannelTextView = view.findViewById(R.id.radio_current_channel_info); 99 mCurrentSongTitleTextView = view.findViewById(R.id.radio_current_song_info); 100 mCurrentArtistTextView = view.findViewById(R.id.radio_current_artist_info); 101 102 registerProgramListListener(); 103 104 closeButton.setOnClickListener((v) -> handleClose()); 105 cancelButton.setOnClickListener((v) -> handleCancel()); 106 seekUpButton.setOnClickListener((v) -> handleSeek(RadioTuner.DIRECTION_UP)); 107 seekDownButton.setOnClickListener((v) -> handleSeek(RadioTuner.DIRECTION_DOWN)); 108 109 setupTunerView(view); 110 programListView.setAdapter(mProgramInfoAdapter); 111 112 mViewCreated = true; 113 Log.i(TAG, "onCreateView done"); 114 return view; 115 } 116 setupTunerView(View view)117 void setupTunerView(View view) { 118 mProgramInfoAdapter = new ProgramInfoAdapter(getContext(), R.layout.program_info_item, 119 new RadioManager.ProgramInfo[]{}, this); 120 } 121 122 @Override onDestroyView()123 public void onDestroyView() { 124 Log.i(TAG, "onDestroyView"); 125 handleClose(); 126 super.onDestroyView(); 127 } 128 registerProgramListListener()129 private void registerProgramListListener() { 130 if (mProgramList == null) { 131 Log.e(TAG, "Can not get program list"); 132 return; 133 } 134 OnCompleteListenerImpl onCompleteListener = new OnCompleteListenerImpl(); 135 mProgramList.addOnCompleteListener(getContext().getMainExecutor(), onCompleteListener); 136 } 137 handleTune(ProgramSelector sel)138 void handleTune(ProgramSelector sel) { 139 if (mRadioTuner == null) { 140 mTuningTextView.setText(getString(R.string.radio_error, NULL_TUNER_WARNING)); 141 return; 142 } 143 mTuningTextView.setText(getString(R.string.radio_status, TUNING_TEXT)); 144 try { 145 mRadioTuner.tune(sel); 146 } catch (Exception e) { 147 mTuningTextView.setText(getString(R.string.radio_error, e.getMessage())); 148 } 149 mListener.onTunerPlay(); 150 } 151 handleSeek(int direction)152 private void handleSeek(int direction) { 153 if (mRadioTuner == null) { 154 mTuningTextView.setText(getString(R.string.radio_error, NULL_TUNER_WARNING)); 155 return; 156 } 157 mTuningTextView.setText(getString(R.string.radio_status, TUNING_TEXT)); 158 try { 159 mRadioTuner.seek(direction, mSeekChannelCheckBox.isChecked()); 160 } catch (Exception e) { 161 mTuningTextView.setText(getString(R.string.radio_error, e.getMessage())); 162 } 163 mListener.onTunerPlay(); 164 } 165 handleClose()166 private void handleClose() { 167 if (mRadioTuner == null) { 168 mTuningTextView.setText(getString(R.string.radio_error, NULL_TUNER_WARNING)); 169 return; 170 } 171 mTuningTextView.setText(getString(R.string.empty)); 172 try { 173 mRadioTuner.close(); 174 mListener.onTunerClosed(); 175 } catch (Exception e) { 176 mTuningTextView.setText(getString(R.string.radio_error, e.getMessage())); 177 } 178 } 179 handleCancel()180 private void handleCancel() { 181 if (mRadioTuner == null) { 182 mTuningTextView.setText(getString(R.string.radio_error, NULL_TUNER_WARNING)); 183 return; 184 } 185 try { 186 mRadioTuner.cancel(); 187 } catch (Exception e) { 188 mTuningTextView.setText(getString(R.string.radio_error, e.getMessage())); 189 } 190 mTuningTextView.setText(getString(R.string.radio_status, "Canceled")); 191 } 192 setTuningStatus(RadioManager.ProgramInfo info)193 private void setTuningStatus(RadioManager.ProgramInfo info) { 194 if (!mViewCreated) { 195 return; 196 } 197 if (info == null) { 198 mTuningTextView.setText(getString(R.string.radio_error, "Program info is null")); 199 return; 200 } else if (info.getSelector().getPrimaryId().getType() 201 != ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT) { 202 if (mTuningTextView.getText().toString().contains(TUNING_TEXT)) { 203 mTuningTextView.setText(getString(R.string.radio_status, TUNING_COMPLETION_TEXT)); 204 } 205 return; 206 } 207 if (Flags.hdRadioImproved()) { 208 if (info.isSignalAcquired()) { 209 if (!info.isHdSisAvailable()) { 210 mTuningTextView.setText(getString(R.string.radio_status, 211 "Signal is acquired")); 212 } else { 213 if (!info.isHdAudioAvailable()) { 214 mTuningTextView.setText(getString(R.string.radio_status, 215 "HD SIS is available")); 216 } else { 217 mTuningTextView.setText(getString(R.string.radio_status, 218 TUNING_COMPLETION_TEXT)); 219 } 220 } 221 } 222 } else { 223 mTuningTextView.setText(getString(R.string.radio_status, TUNING_COMPLETION_TEXT)); 224 } 225 } 226 setProgramInfo(RadioManager.ProgramInfo info)227 private void setProgramInfo(RadioManager.ProgramInfo info) { 228 if (!mViewCreated) { 229 return; 230 } 231 CharSequence channelText = getChannelName(info); 232 mCurrentChannelTextView.setText(getString(R.string.radio_current_channel_info, 233 channelText)); 234 mCurrentStationTextView.setText(getString(R.string.radio_current_station_info, 235 getMetadataText(info, RadioMetadata.METADATA_KEY_RDS_PS))); 236 mCurrentArtistTextView.setText(getString(R.string.radio_current_song_info, 237 getMetadataText(info, RadioMetadata.METADATA_KEY_TITLE))); 238 mCurrentSongTitleTextView.setText(getString(R.string.radio_current_artist_info, 239 getMetadataText(info, RadioMetadata.METADATA_KEY_ARTIST))); 240 } 241 getChannelName(RadioManager.ProgramInfo info)242 CharSequence getChannelName(RadioManager.ProgramInfo info) { 243 return ""; 244 } 245 getMetadataText(RadioManager.ProgramInfo info, String metadataType)246 CharSequence getMetadataText(RadioManager.ProgramInfo info, String metadataType) { 247 String naText = getString(R.string.radio_na); 248 if (info == null || info.getMetadata() == null) { 249 return naText; 250 } 251 CharSequence metadataText = info.getMetadata().getString(metadataType); 252 return metadataText == null ? naText : metadataText; 253 } 254 updateConfigFlag(int flag, boolean value)255 void updateConfigFlag(int flag, boolean value) { 256 } 257 258 private final class RadioTunerCallbackImpl extends RadioTuner.Callback { 259 @Override onProgramInfoChanged(RadioManager.ProgramInfo info)260 public void onProgramInfoChanged(RadioManager.ProgramInfo info) { 261 setProgramInfo(info); 262 setTuningStatus(info); 263 } 264 265 @Override onConfigFlagUpdated(int flag, boolean value)266 public void onConfigFlagUpdated(int flag, boolean value) { 267 if (!mViewCreated) { 268 return; 269 } 270 updateConfigFlag(flag, value); 271 } 272 273 @Override onTuneFailed(int result, @Nullable ProgramSelector selector)274 public void onTuneFailed(int result, @Nullable ProgramSelector selector) { 275 if (!mViewCreated) { 276 return; 277 } 278 String warning = "onTuneFailed:"; 279 if (selector != null) { 280 warning += " for selector " + selector; 281 } 282 mTuningTextView.setText(getString(R.string.radio_error, warning)); 283 } 284 } 285 286 private final class OnCompleteListenerImpl implements ProgramList.OnCompleteListener { 287 @Override onComplete()288 public void onComplete() { 289 if (mProgramList == null) { 290 Log.e(TAG, "Program list is null"); 291 } 292 List<RadioManager.ProgramInfo> list = mProgramList.toList(); 293 Comparator<RadioManager.ProgramInfo> selectorComparator = 294 new ProgramInfoExt.ProgramInfoComparator(); 295 list.sort(selectorComparator); 296 mProgramInfoAdapter.updateProgramInfos(list.toArray(new RadioManager.ProgramInfo[0])); 297 } 298 } 299 } 300