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