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 static android.media.AudioAttributes.USAGE_MEDIA;
20 import static android.media.AudioManager.GET_DEVICES_INPUTS;
21 
22 import android.annotation.IntDef;
23 import android.content.Context;
24 import android.hardware.radio.ProgramSelector;
25 import android.hardware.radio.RadioManager;
26 import android.hardware.radio.RadioTuner;
27 import android.media.AudioAttributes;
28 import android.media.AudioDeviceInfo;
29 import android.media.AudioManager;
30 import android.media.HwAudioSource;
31 import android.os.Bundle;
32 import android.os.Handler;
33 import android.os.Looper;
34 import android.util.Log;
35 import android.util.SparseArray;
36 import android.view.LayoutInflater;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.widget.Button;
40 import android.widget.RadioButton;
41 import android.widget.RadioGroup;
42 import android.widget.TextView;
43 
44 import androidx.fragment.app.Fragment;
45 import androidx.viewpager2.widget.ViewPager2;
46 
47 import com.google.android.car.kitchensink.R;
48 import com.google.android.material.tabs.TabLayout;
49 import com.google.android.material.tabs.TabLayoutMediator;
50 
51 import java.lang.annotation.Retention;
52 import java.lang.annotation.RetentionPolicy;
53 import java.util.ArrayList;
54 import java.util.List;
55 import java.util.Map;
56 
57 public final class RadioTestFragment extends Fragment {
58 
59     public static final String FRAGMENT_NAME = "Radio";
60 
61     private static final int TUNER_TYPE_INVALID = 0;
62     private static final int TUNER_TYPE_AM_FM = 1;
63     private static final int TUNER_TYPE_DAB = 2;
64     @IntDef(prefix = { "TUNER_TYPE_" }, value = {
65             TUNER_TYPE_INVALID ,
66             TUNER_TYPE_AM_FM,
67             TUNER_TYPE_DAB,
68     })
69     @Retention(RetentionPolicy.SOURCE)
70     private @interface TunerType {}
71 
72     private static final String TAG = RadioTestFragment.class.getSimpleName();
73     private static final int INVALID_MODULE_ID = -1;
74 
75     private Handler mHandler;
76     private Context mContext;
77 
78     private AudioAttributes mMusicAudioAttribute;
79     private HwAudioSource mHwAudioSource;
80 
81     private RadioManager mRadioManager;
82     private RadioManager.BandConfig mFmBandConfig;
83     private RadioManager.BandConfig mAmBandConfig;
84     private SparseArray<String> mDabFrequencyToLabelMap;
85     private RadioTuner mFmAmRadioTuner;
86     private RadioTuner mDabRadioTuner;
87 
88     private List<RadioManager.ModuleProperties> mModules;
89     private int mFirstAmFmModuleId = INVALID_MODULE_ID;
90     private int mFirstDabModuleId = INVALID_MODULE_ID;
91 
92     private TextView mOpenTunerWarning;
93     private TextView mPlayingStatus;
94     private TabLayout mTunerTabLayout;
95     private TabLayoutMediator mTabLayoutMediator;
96     private ViewPager2 mTunerViewPager;
97     private RadioTunerTabAdapter mTunerTabAdapter;
98 
99     @Override
onCreate(Bundle savedInstanceState)100     public void onCreate(Bundle savedInstanceState) {
101         super.onCreate(savedInstanceState);
102         mContext = getContext();
103         mHandler = new Handler(Looper.getMainLooper());
104     }
105 
106     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle)107     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
108         Log.i(TAG, "onCreateView");
109         View view = inflater.inflate(R.layout.radio, container, /* attachToRoot= */ false);
110 
111         RadioGroup tunerTypeSelection = view.findViewById(R.id.selection_tuner_type);
112         RadioButton amFmTunerButton = view.findViewById(R.id.button_am_fm_type_tuner);
113         RadioButton dabTunerButton = view.findViewById(R.id.button_dab_type_tuner);
114         Button openTunerButton = view.findViewById(R.id.button_radio_open);
115         mOpenTunerWarning = view.findViewById(R.id.warning_open_tuner);
116 
117         mTunerTabLayout = view.findViewById(R.id.tabs_tadio_tuner);
118         mTunerViewPager = view.findViewById(R.id.view_pager_radio_tuner);
119         mTunerTabAdapter = new RadioTunerTabAdapter(this);
120         mTunerViewPager.setAdapter(mTunerTabAdapter);
121 
122         initializeRadioPlayer();
123         View hwAudioSourceNotFound = view.findViewById(R.id.hw_audio_source_not_found);
124         View hwAudioSourceStart = view.findViewById(R.id.hw_audio_source_start);
125         View hwAudioSourceStop = view.findViewById(R.id.hw_audio_source_stop);
126         mPlayingStatus = view.findViewById(R.id.text_radio_playing_state);
127         if (mHwAudioSource == null) {
128             hwAudioSourceNotFound.setVisibility(View.VISIBLE);
129             hwAudioSourceStart.setVisibility(View.GONE);
130             hwAudioSourceStop.setVisibility(View.GONE);
131         } else {
132             hwAudioSourceNotFound.setVisibility(View.GONE);
133             hwAudioSourceStart.setVisibility(View.VISIBLE);
134             hwAudioSourceStop.setVisibility(View.VISIBLE);
135             view.findViewById(R.id.hw_audio_source_start).setOnClickListener(
136                     v -> handleHwAudioSourceStart());
137             view.findViewById(R.id.hw_audio_source_stop).setOnClickListener(
138                     v -> handleHwAudioSourceStop());
139         }
140 
141         connectRadio();
142 
143         if (mFirstDabModuleId != INVALID_MODULE_ID) {
144             dabTunerButton.setVisibility(View.VISIBLE);
145             tunerTypeSelection.check(R.id.button_dab_type_tuner);
146         }
147         if (mFirstAmFmModuleId != INVALID_MODULE_ID) {
148             amFmTunerButton.setVisibility(View.VISIBLE);
149             tunerTypeSelection.check(R.id.button_am_fm_type_tuner);
150         } else if (mFirstDabModuleId == INVALID_MODULE_ID) {
151             openTunerButton.setVisibility(View.INVISIBLE);
152         }
153 
154         openTunerButton.setOnClickListener(v -> {
155             mOpenTunerWarning.setText(getString(R.string.empty));
156             int selectedButtonId = tunerTypeSelection.getCheckedRadioButtonId();
157             String tabTitle;
158             RadioTunerFragment tunerFragment;
159             if (selectedButtonId == R.id.button_am_fm_type_tuner) {
160                 tabTitle = getString(R.string.radio_fm_am_tuner);
161                 if (mFmAmRadioTuner != null) {
162                     mOpenTunerWarning.setText(getString(R.string.radio_error,
163                             "FM/AM tuner exists, cannot open a new one"));
164                     return;
165                 }
166                 tunerFragment = new AmFmTunerFragment(mRadioManager,
167                         mModules.get(mFirstAmFmModuleId).getId(), mFmBandConfig, mAmBandConfig,
168                         mHandler, new TunerListener(tabTitle, TUNER_TYPE_AM_FM));
169                 mFmAmRadioTuner = tunerFragment.getRadioTuner();
170                 if (mFmAmRadioTuner == null) {
171                     mOpenTunerWarning.setText(getString(R.string.radio_error,
172                             "Cannot open new AM/FM tuner"));
173                     return;
174                 }
175             } else if (selectedButtonId == R.id.button_dab_type_tuner) {
176                 tabTitle = getString(R.string.radio_dab_tuner);
177                 if (mDabRadioTuner != null) {
178                     mOpenTunerWarning.setText(getString(R.string.radio_error,
179                             "DAB tuner exists, cannot open a new one"));
180                     return;
181                 }
182                 tunerFragment = new DabTunerFragment(mRadioManager, mDabFrequencyToLabelMap,
183                         mModules.get(mFirstDabModuleId).getId(), mHandler,
184                         new TunerListener(tabTitle, TUNER_TYPE_DAB));
185                 mDabRadioTuner = tunerFragment.getRadioTuner();
186                 if (mDabRadioTuner == null) {
187                     mOpenTunerWarning.setText(getString(R.string.radio_error,
188                             "Cannot open new DAB tuner"));
189                     return;
190                 }
191             } else {
192                 return;
193             }
194 
195             mTunerTabAdapter.addFragment(tunerFragment, tabTitle);
196             mTunerTabAdapter.notifyDataSetChanged();
197             mTabLayoutMediator = new TabLayoutMediator(mTunerTabLayout, mTunerViewPager,
198                     (tab, pos) -> tab.setText(mTunerTabAdapter.getPageTitle(pos)));
199             mTabLayoutMediator.attach();
200         });
201 
202         return view;
203     }
204 
205     @Override
onDestroyView()206     public void onDestroyView() {
207         Log.i(TAG, "onDestroyView");
208         super.onDestroyView();
209     }
210 
connectRadio()211     private void connectRadio() {
212         mRadioManager = mContext.getSystemService(RadioManager.class);
213         if (mRadioManager == null) {
214             mOpenTunerWarning.setText(getString(R.string.radio_error,
215                     "RadioManager is not found"));
216             return;
217         }
218 
219         mModules = new ArrayList<>();
220         int status = mRadioManager.listModules(mModules);
221         if (status != RadioManager.STATUS_OK) {
222             mOpenTunerWarning.setText(getString(R.string.radio_error,
223                     "Couldn't get radio module list"));
224             return;
225         }
226 
227         if (mModules.size() == 0) {
228             mOpenTunerWarning.setText(getString(R.string.radio_error,
229                     "No radio modules on this device"));
230             return;
231         }
232 
233         RadioManager.AmBandDescriptor amBandDescriptor = null;
234         RadioManager.FmBandDescriptor fmBandDescriptor = null;
235         for (int moduleIndex = 0; moduleIndex < mModules.size(); moduleIndex++) {
236             RadioManager.ModuleProperties moduleProperties = mModules.get(moduleIndex);
237             if (mFirstAmFmModuleId == INVALID_MODULE_ID && (moduleProperties
238                     .isProgramIdentifierSupported(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)
239                     || moduleProperties.isProgramIdentifierSupported(
240                             ProgramSelector.IDENTIFIER_TYPE_RDS_PI)
241                     || moduleProperties.isProgramIdentifierSupported(
242                             ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT))) {
243                 for (RadioManager.BandDescriptor band : moduleProperties.getBands()) {
244                     int bandType = band.getType();
245                     if (bandType == RadioManager.BAND_AM || bandType == RadioManager.BAND_AM_HD) {
246                         amBandDescriptor = (RadioManager.AmBandDescriptor) band;
247                     }
248                     if (bandType == RadioManager.BAND_FM || bandType == RadioManager.BAND_FM_HD) {
249                         fmBandDescriptor = (RadioManager.FmBandDescriptor) band;
250                     }
251                 }
252                 if (amBandDescriptor != null && fmBandDescriptor != null) {
253                     mFirstAmFmModuleId = moduleIndex;
254                     mFmBandConfig = new RadioManager.FmBandConfig.Builder(fmBandDescriptor).build();
255                     mAmBandConfig = new RadioManager.AmBandConfig.Builder(amBandDescriptor).build();
256                 }
257             }
258             if (mFirstDabModuleId == INVALID_MODULE_ID
259                     && (moduleProperties.isProgramIdentifierSupported(
260                             ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT)
261                     || moduleProperties.isProgramIdentifierSupported(
262                             ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT))) {
263                 Map<String, Integer> dabFrequencyTable = moduleProperties.getDabFrequencyTable();
264                 if (dabFrequencyTable != null) {
265                     mDabFrequencyToLabelMap = RadioTestFragmentUtils.getDabFrequencyToLabelMap(
266                             dabFrequencyTable);
267                     mFirstDabModuleId = moduleIndex;
268                 }
269             }
270         }
271     }
272 
initializeRadioPlayer()273     private void initializeRadioPlayer() {
274         mMusicAudioAttribute = new AudioAttributes.Builder().setUsage(USAGE_MEDIA).build();
275         AudioDeviceInfo tuner = findTunerDevice(mContext);
276         if (tuner != null) {
277             mHwAudioSource = new HwAudioSource.Builder().setAudioAttributes(mMusicAudioAttribute)
278                     .setAudioDeviceInfo(findTunerDevice(mContext)).build();
279         }
280     }
281 
findTunerDevice(Context context)282     private AudioDeviceInfo findTunerDevice(Context context) {
283         AudioManager am = context.getSystemService(AudioManager.class);
284         AudioDeviceInfo[] devices = am.getDevices(GET_DEVICES_INPUTS);
285         for (AudioDeviceInfo device : devices) {
286             if (device.getType() == AudioDeviceInfo.TYPE_FM_TUNER) {
287                 return device;
288             }
289         }
290         return null;
291     }
292 
handleHwAudioSourceStart()293     private void handleHwAudioSourceStart() {
294         if (mHwAudioSource != null && !mHwAudioSource.isPlaying()) {
295             mHwAudioSource.start();
296             mPlayingStatus.setText(getString(R.string.radio_play));
297         }
298     }
299 
handleHwAudioSourceStop()300     private void handleHwAudioSourceStop() {
301         if (mHwAudioSource != null && mHwAudioSource.isPlaying()) {
302             mHwAudioSource.stop();
303             mPlayingStatus.setText(getString(R.string.radio_stop));
304         }
305     }
306 
307     final class TunerListener {
308         private final String mTunerTabTitle;
309         @TunerType
310         private final int mTunerType;
TunerListener(String tunerTabTitle, @TunerType int tunerType)311         TunerListener(String tunerTabTitle, @TunerType int tunerType) {
312             mTunerTabTitle = tunerTabTitle;
313             mTunerType = tunerType;
314         }
onTunerClosed()315         public void onTunerClosed() {
316             if (mTunerTabAdapter != null) {
317                 mTunerTabAdapter.removeFragment(mTunerTabTitle);
318                 mTunerTabAdapter.notifyDataSetChanged();
319                 switch (mTunerType) {
320                     case TUNER_TYPE_AM_FM:
321                         mFmAmRadioTuner = null;
322                         break;
323                     case TUNER_TYPE_DAB:
324                         mDabRadioTuner = null;
325                         break;
326                     default:
327                         Log.e(TAG, "Unsupported tuner type " + mTunerType);
328                         break;
329                 }
330                 handleHwAudioSourceStop();
331                 mOpenTunerWarning.setText(getString(R.string.empty));
332             }
333         }
334 
onTunerPlay()335         public void onTunerPlay() {
336             handleHwAudioSourceStart();
337         }
338     }
339 }
340