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.app.NotificationChannel; 21 import android.content.Context; 22 import android.hardware.radio.Flags; 23 import android.hardware.radio.ProgramList; 24 import android.hardware.radio.ProgramSelector; 25 import android.hardware.radio.RadioAlert; 26 import android.hardware.radio.RadioManager; 27 import android.hardware.radio.RadioMetadata; 28 import android.hardware.radio.RadioTuner; 29 import android.os.Bundle; 30 import android.os.Handler; 31 import android.util.Log; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.widget.Button; 36 import android.widget.CheckBox; 37 import android.widget.ListView; 38 import android.widget.TextView; 39 40 import androidx.core.app.NotificationManagerCompat; 41 import androidx.fragment.app.Fragment; 42 43 import com.android.car.broadcastradio.support.platform.ProgramInfoExt; 44 45 import com.google.android.car.kitchensink.R; 46 47 import java.util.ArrayList; 48 import java.util.Comparator; 49 import java.util.List; 50 import java.util.Objects; 51 52 public class RadioTunerFragment extends Fragment { 53 54 private static final String TAG = RadioTunerFragment.class.getSimpleName(); 55 protected static final CharSequence NULL_TUNER_WARNING = "Tuner cannot be null"; 56 protected static final CharSequence TUNING_TEXT = "Tuning..."; 57 private static final CharSequence TUNING_COMPLETION_TEXT = "Tuning completes"; 58 private static final String RADIO_ALERT_DELIMITER = " · "; 59 60 protected final RadioTuner mRadioTuner; 61 protected final RadioTestFragment.TunerListener mListener; 62 private final ProgramList mProgramList; 63 protected boolean mViewCreated = false; 64 private int mAlertNotificationId = 0; 65 66 protected ProgramInfoAdapter mProgramInfoAdapter; 67 68 protected Context mActivityContext; 69 70 private CheckBox mSeekChannelCheckBox; 71 protected TextView mTuningTextView; 72 private TextView mCurrentStationTextView; 73 protected TextView mCurrentChannelTextView; 74 private TextView mCurrentSongTitleTextView; 75 private TextView mCurrentArtistTextView; 76 RadioTunerFragment(RadioManager radioManager, int moduleId, Handler handler, RadioTestFragment.TunerListener tunerListener)77 RadioTunerFragment(RadioManager radioManager, int moduleId, Handler handler, 78 RadioTestFragment.TunerListener tunerListener) { 79 mRadioTuner = radioManager.openTuner(moduleId, /* config= */ null, /* withAudio= */ true, 80 new RadioTunerCallbackImpl(), handler); 81 mListener = Objects.requireNonNull(tunerListener, "Tuner listener can not be null"); 82 if (mRadioTuner == null) { 83 mProgramList = null; 84 } else { 85 mProgramList = mRadioTuner.getDynamicProgramList(/* filter= */ null); 86 } 87 } 88 getRadioTuner()89 RadioTuner getRadioTuner() { 90 return mRadioTuner; 91 } 92 93 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)94 public View onCreateView(LayoutInflater inflater, ViewGroup container, 95 Bundle savedInstanceState) { 96 Log.i(TAG, "onCreateView"); 97 mActivityContext = getActivity(); 98 99 View view = inflater.inflate(R.layout.radio_tuner_fragment, container, 100 /* attachToRoot= */ false); 101 Button closeButton = view.findViewById(R.id.button_radio_close); 102 Button cancelButton = view.findViewById(R.id.button_radio_cancel); 103 mTuningTextView = view.findViewById(R.id.text_tuning_status); 104 mSeekChannelCheckBox = view.findViewById(R.id.selection_seek_skip_subchannels); 105 Button seekUpButton = view.findViewById(R.id.button_radio_seek_up); 106 Button seekDownButton = view.findViewById(R.id.button_radio_seek_down); 107 ListView programListView = view.findViewById(R.id.radio_program_list); 108 mCurrentStationTextView = view.findViewById(R.id.radio_current_station_info); 109 mCurrentChannelTextView = view.findViewById(R.id.radio_current_channel_info); 110 mCurrentSongTitleTextView = view.findViewById(R.id.radio_current_song_info); 111 mCurrentArtistTextView = view.findViewById(R.id.radio_current_artist_info); 112 113 registerProgramListListener(); 114 115 closeButton.setOnClickListener((v) -> handleClose()); 116 cancelButton.setOnClickListener((v) -> handleCancel()); 117 seekUpButton.setOnClickListener((v) -> handleSeek(RadioTuner.DIRECTION_UP)); 118 seekDownButton.setOnClickListener((v) -> handleSeek(RadioTuner.DIRECTION_DOWN)); 119 120 setupTunerView(view); 121 programListView.setAdapter(mProgramInfoAdapter); 122 123 NotificationManagerCompat notificationManager = 124 NotificationManagerCompat.from(mActivityContext); 125 notificationManager.createNotificationChannel(new NotificationChannel( 126 AlertNotificationHelper.IMPORTANCE_ALERT_ID, "Importance High", 127 NotificationManagerCompat.IMPORTANCE_HIGH)); 128 129 mViewCreated = true; 130 Log.i(TAG, "onCreateView done"); 131 return view; 132 } 133 setupTunerView(View view)134 void setupTunerView(View view) { 135 mProgramInfoAdapter = new ProgramInfoAdapter(getContext(), R.layout.program_info_item, 136 new RadioManager.ProgramInfo[]{}, this); 137 } 138 139 @Override onDestroyView()140 public void onDestroyView() { 141 Log.i(TAG, "onDestroyView"); 142 handleClose(); 143 super.onDestroyView(); 144 } 145 registerProgramListListener()146 private void registerProgramListListener() { 147 if (mProgramList == null) { 148 Log.e(TAG, "Can not get program list"); 149 return; 150 } 151 OnCompleteListenerImpl onCompleteListener = new OnCompleteListenerImpl(); 152 mProgramList.addOnCompleteListener(getContext().getMainExecutor(), onCompleteListener); 153 } 154 handleTune(ProgramSelector sel)155 void handleTune(ProgramSelector sel) { 156 if (mRadioTuner == null) { 157 mTuningTextView.setText(getString(R.string.radio_error, NULL_TUNER_WARNING)); 158 return; 159 } 160 mTuningTextView.setText(getString(R.string.radio_status, TUNING_TEXT)); 161 try { 162 mRadioTuner.tune(sel); 163 } catch (Exception e) { 164 mTuningTextView.setText(getString(R.string.radio_error, e.getMessage())); 165 } 166 mListener.onTunerPlay(); 167 } 168 handleSeek(int direction)169 private void handleSeek(int direction) { 170 if (mRadioTuner == null) { 171 mTuningTextView.setText(getString(R.string.radio_error, NULL_TUNER_WARNING)); 172 return; 173 } 174 mTuningTextView.setText(getString(R.string.radio_status, TUNING_TEXT)); 175 try { 176 mRadioTuner.seek(direction, mSeekChannelCheckBox.isChecked()); 177 } catch (Exception e) { 178 mTuningTextView.setText(getString(R.string.radio_error, e.getMessage())); 179 } 180 mListener.onTunerPlay(); 181 } 182 handleClose()183 private void handleClose() { 184 if (mRadioTuner == null) { 185 mTuningTextView.setText(getString(R.string.radio_error, NULL_TUNER_WARNING)); 186 return; 187 } 188 mTuningTextView.setText(getString(R.string.empty)); 189 try { 190 mRadioTuner.close(); 191 mListener.onTunerClosed(); 192 } catch (Exception e) { 193 mTuningTextView.setText(getString(R.string.radio_error, e.getMessage())); 194 } 195 } 196 handleCancel()197 private void handleCancel() { 198 if (mRadioTuner == null) { 199 mTuningTextView.setText(getString(R.string.radio_error, NULL_TUNER_WARNING)); 200 return; 201 } 202 try { 203 mRadioTuner.cancel(); 204 } catch (Exception e) { 205 mTuningTextView.setText(getString(R.string.radio_error, e.getMessage())); 206 } 207 mTuningTextView.setText(getString(R.string.radio_status, "Canceled")); 208 } 209 setTuningStatus(RadioManager.ProgramInfo info)210 private void setTuningStatus(RadioManager.ProgramInfo info) { 211 if (!mViewCreated) { 212 return; 213 } 214 if (info == null) { 215 mTuningTextView.setText(getString(R.string.radio_error, "Program info is null")); 216 return; 217 } else if (info.getSelector().getPrimaryId().getType() 218 != ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT) { 219 if (mTuningTextView.getText().toString().contains(TUNING_TEXT)) { 220 mTuningTextView.setText(getString(R.string.radio_status, TUNING_COMPLETION_TEXT)); 221 } 222 return; 223 } 224 if (Flags.hdRadioImproved()) { 225 if (info.isSignalAcquired()) { 226 if (!info.isHdSisAvailable()) { 227 mTuningTextView.setText(getString(R.string.radio_status, 228 "Signal is acquired")); 229 } else { 230 if (!info.isHdAudioAvailable()) { 231 mTuningTextView.setText(getString(R.string.radio_status, 232 "HD SIS is available")); 233 } else { 234 mTuningTextView.setText(getString(R.string.radio_status, 235 TUNING_COMPLETION_TEXT)); 236 } 237 } 238 } 239 } else { 240 mTuningTextView.setText(getString(R.string.radio_status, TUNING_COMPLETION_TEXT)); 241 } 242 } 243 setProgramInfo(RadioManager.ProgramInfo info)244 private void setProgramInfo(RadioManager.ProgramInfo info) { 245 if (!mViewCreated) { 246 return; 247 } 248 CharSequence channelText = getChannelName(info); 249 mCurrentChannelTextView.setText(getString(R.string.radio_current_channel_info, 250 channelText)); 251 mCurrentStationTextView.setText(getString(R.string.radio_current_station_info, 252 getMetadataText(info, RadioMetadata.METADATA_KEY_RDS_PS))); 253 mCurrentArtistTextView.setText(getString(R.string.radio_current_song_info, 254 getMetadataText(info, RadioMetadata.METADATA_KEY_TITLE))); 255 mCurrentSongTitleTextView.setText(getString(R.string.radio_current_artist_info, 256 getMetadataText(info, RadioMetadata.METADATA_KEY_ARTIST))); 257 } 258 getChannelName(RadioManager.ProgramInfo info)259 CharSequence getChannelName(RadioManager.ProgramInfo info) { 260 return ""; 261 } 262 getMetadataText(RadioManager.ProgramInfo info, String metadataType)263 CharSequence getMetadataText(RadioManager.ProgramInfo info, String metadataType) { 264 String naText = getString(R.string.radio_na); 265 if (info == null || info.getMetadata() == null) { 266 return naText; 267 } 268 CharSequence metadataText = info.getMetadata().getString(metadataType); 269 return metadataText == null ? naText : metadataText; 270 } 271 updateConfigFlag(int flag, boolean value)272 void updateConfigFlag(int flag, boolean value) { 273 } 274 handleRadioAlert(RadioManager.ProgramInfo info)275 private void handleRadioAlert(RadioManager.ProgramInfo info) { 276 RadioAlert alert = info.getAlert(); 277 if (alert == null || alert.getInfoList().isEmpty()) { 278 return; 279 } 280 281 int notificationId = mAlertNotificationId++; 282 String alertTitle = RadioTestFragmentUtils.alertStatusToString(alert.getStatus()) 283 + RADIO_ALERT_DELIMITER + getChannelName(info); 284 String alertText = getAlertInfoDisplayText(alert.getInfoList().getFirst()); 285 286 AlertNotificationHelper.createRadioAlertNotification(mActivityContext, alertTitle, 287 alertText, System.currentTimeMillis(), notificationId); 288 } 289 getAlertInfoDisplayText(RadioAlert.AlertInfo alertInfo)290 private static String getAlertInfoDisplayText(RadioAlert.AlertInfo alertInfo) { 291 int[] categories = alertInfo.getCategories(); 292 List<String> categoryStringList = new ArrayList<>(categories.length); 293 for (int i = 0; i < categories.length; i++) { 294 categoryStringList.add(RadioTestFragmentUtils.alertCategoryToString(categories[i])); 295 } 296 String categoryText = formatTextWithDelimiter(categoryStringList, ","); 297 List<String> textList = List.of(RadioTestFragmentUtils.alertUrgencyToString( 298 alertInfo.getUrgency()), RadioTestFragmentUtils.alertSeverityToString( 299 alertInfo.getSeverity()), RadioTestFragmentUtils.alertCertaintyToString( 300 alertInfo.getCertainty()), alertInfo.getDescription(), categoryText); 301 302 return formatTextWithDelimiter(textList, RADIO_ALERT_DELIMITER); 303 } 304 formatTextWithDelimiter(List<String> textList, String delimiter)305 private static String formatTextWithDelimiter(List<String> textList, String delimiter) { 306 StringBuilder builder = new StringBuilder(); 307 for (int i = 0; i < textList.size(); i++) { 308 String text = textList.get(i); 309 if (text == null || text.isEmpty()) { 310 continue; 311 } 312 if (!builder.isEmpty()) { 313 builder.append(delimiter); 314 } 315 builder.append(text); 316 } 317 return builder.toString(); 318 } 319 320 private final class RadioTunerCallbackImpl extends RadioTuner.Callback { 321 @Override onProgramInfoChanged(RadioManager.ProgramInfo info)322 public void onProgramInfoChanged(RadioManager.ProgramInfo info) { 323 setProgramInfo(info); 324 setTuningStatus(info); 325 if (Flags.hdRadioEmergencyAlertSystem()) { 326 handleRadioAlert(info); 327 } 328 } 329 330 @Override onConfigFlagUpdated(int flag, boolean value)331 public void onConfigFlagUpdated(int flag, boolean value) { 332 if (!mViewCreated) { 333 return; 334 } 335 updateConfigFlag(flag, value); 336 } 337 338 @Override onTuneFailed(int result, @Nullable ProgramSelector selector)339 public void onTuneFailed(int result, @Nullable ProgramSelector selector) { 340 if (!mViewCreated) { 341 return; 342 } 343 String warning = "onTuneFailed:"; 344 if (selector != null) { 345 warning += " for selector " + selector; 346 } 347 mTuningTextView.setText(getString(R.string.radio_error, warning)); 348 } 349 } 350 351 private final class OnCompleteListenerImpl implements ProgramList.OnCompleteListener { 352 @Override onComplete()353 public void onComplete() { 354 if (mProgramList == null) { 355 Log.e(TAG, "Program list is null"); 356 } 357 List<RadioManager.ProgramInfo> list = mProgramList.toList(); 358 Comparator<RadioManager.ProgramInfo> selectorComparator = 359 new ProgramInfoExt.ProgramInfoComparator(); 360 list.sort(selectorComparator); 361 mProgramInfoAdapter.updateProgramInfos(list.toArray(new RadioManager.ProgramInfo[0])); 362 if (!Flags.hdRadioEmergencyAlertSystem()) { 363 return; 364 } 365 for (int i = 0; i < list.size(); i++) { 366 handleRadioAlert(list.get(i)); 367 } 368 } 369 } 370 } 371