1 /*
2  * Copyright (C) 2016 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.android.car.radio;
18 
19 import android.content.Context;
20 import android.hardware.radio.RadioManager;
21 import android.hardware.radio.RadioMetadata;
22 import android.hardware.radio.RadioTuner;
23 import android.support.annotation.Nullable;
24 import android.util.Log;
25 import com.android.car.radio.service.RadioRds;
26 import com.android.car.radio.service.RadioStation;
27 
28 import java.util.ArrayList;
29 import java.util.List;
30 
31 /**
32  * A class that will scan for valid radio stations in the background and store those stations into
33  * the radio database so that available stations for a particular radio band can be pre-populated
34  * for the user.
35  */
36 public class RadioBackgroundScanner extends RadioTuner.Callback {
37     private static final String TAG = "Em.BackgroundScanner";
38     private static final int INVALID_RADIO_CHANNEL = -1;
39 
40     private RadioTuner mRadioTuner;
41 
42     private final RadioManager mRadioManager;
43     private final RadioManager.AmBandConfig mAmConfig;
44     private final RadioManager.FmBandConfig mFmConfig;
45     private final RadioManager.ModuleProperties mRadioModule;
46     private final RadioStorage mRadioStorage;
47 
48     private int mCurrentChannel;
49     private int mCurrentBand;
50     private int mStartingChannel = INVALID_RADIO_CHANNEL;
51 
52     private List<RadioStation> mScannedStations = new ArrayList<>();
53 
RadioBackgroundScanner(Context context, RadioManager radioManager, RadioManager.AmBandConfig amConfig, RadioManager.FmBandConfig fmConfig, RadioManager.ModuleProperties module)54     public RadioBackgroundScanner(Context context, RadioManager radioManager,
55             RadioManager.AmBandConfig amConfig, RadioManager.FmBandConfig fmConfig,
56             RadioManager.ModuleProperties module) {
57         mRadioManager = radioManager;
58         mAmConfig = amConfig;
59         mFmConfig = fmConfig;
60         mRadioModule = module;
61 
62         mRadioStorage = RadioStorage.getInstance(context);
63     }
64 
65     /**
66      * Notify this {@link RadioBackgroundScanner} that the current radio band has changed and
67      * a new scan should start.
68      */
onRadioBandChanged(int radioBand)69     public void onRadioBandChanged(int radioBand) {
70         mStartingChannel = INVALID_RADIO_CHANNEL;
71         mCurrentBand = radioBand;
72         mScannedStations.clear();
73 
74         RadioManager.BandConfig config = getRadioConfig(radioBand);
75 
76         if (config == null) {
77             Log.w(TAG, "Cannot create config for radio band: " + radioBand);
78             return;
79         }
80 
81         if (mRadioTuner != null) {
82             mRadioTuner.setConfiguration(config);
83         } else {
84             mRadioTuner = mRadioManager.openTuner(mRadioModule.getId(), config, true,
85                     this /* callback */, null /* handler */);
86         }
87     }
88 
89     /**
90      * Returns the proper {@link android.hardware.radio.RadioManager.BandConfig} for the given
91      * radio band. {@code null} is returned if the band is not suppored.
92      */
93     @Nullable
getRadioConfig(int selectedRadioBand)94     private RadioManager.BandConfig getRadioConfig(int selectedRadioBand) {
95         switch (selectedRadioBand) {
96             case RadioManager.BAND_AM:
97                 return mAmConfig;
98             case RadioManager.BAND_FM:
99                 return mFmConfig;
100 
101             // TODO: Support BAND_FM_HD and BAND_AM_HD.
102 
103             default:
104                 return null;
105         }
106     }
107 
108     /**
109      * Replaces the given station in {@link #mScannedStations} or adds it to the end of the list if
110      * it does not exist.
111      */
addOrReplaceInScannedStations(RadioStation station)112     private void addOrReplaceInScannedStations(RadioStation station) {
113         int index = mScannedStations.indexOf(station);
114 
115         if (Log.isLoggable(TAG, Log.VERBOSE)) {
116             Log.v(TAG, "Storing pre-scanned station: " + station);
117         }
118 
119         if (index == -1) {
120             mScannedStations.add(station);
121         } else {
122             mScannedStations.set(index, station);
123         }
124     }
125 
126     @Override
onProgramInfoChanged(RadioManager.ProgramInfo info)127     public void onProgramInfoChanged(RadioManager.ProgramInfo info) {
128         if (Log.isLoggable(TAG, Log.VERBOSE)) {
129             Log.v(TAG, "onProgramInfoChanged(); info: " + info);
130         }
131 
132         if (info == null) {
133             return;
134         }
135 
136         mCurrentChannel = info.getChannel();
137 
138         if (mStartingChannel == INVALID_RADIO_CHANNEL) {
139             mStartingChannel = mCurrentChannel;
140 
141             if (Log.isLoggable(TAG, Log.DEBUG)) {
142                 Log.d(TAG, "Starting scan from channel: " + mStartingChannel);
143             }
144         }
145 
146         // Stop scanning if we have looped back to the starting station.
147         if (mStartingChannel == mCurrentChannel) {
148             if (Log.isLoggable(TAG, Log.DEBUG)) {
149                 Log.d(TAG, String.format("Looped back around to starting channel %s; storing "
150                         + "%d pre-scanned stations", mStartingChannel, mScannedStations.size()));
151             }
152 
153             if (Log.isLoggable(TAG, Log.VERBOSE)) {
154                 for (RadioStation station : mScannedStations) {
155                     Log.v(TAG, station.toString());
156                 }
157             }
158 
159             mStartingChannel = INVALID_RADIO_CHANNEL;
160 
161             // Close the RadioTuner so that this class no longer receives any callbacks and store
162             // all scanned statiosn into the database.
163             mRadioTuner.close();
164             mRadioTuner = null;
165             mRadioStorage.storePreScannedStations(mCurrentBand, mScannedStations);
166             return;
167         }
168 
169         RadioMetadata metadata = info.getMetadata();
170 
171         // If there is no metadata, then directly store the radio information into the database.
172         // Otherwise, onMetadataChanged() can handle the storage.
173         if (metadata != null) {
174             onMetadataChanged(metadata);
175         } else {
176             RadioStation station = new RadioStation(mCurrentChannel, 0 /* subChannelNumber */,
177                     mCurrentBand, null /* rds */);
178             addOrReplaceInScannedStations(station);
179         }
180 
181         // Initialize another seek to the next valid station.
182         mRadioTuner.scan(RadioTuner.DIRECTION_UP, true);
183     }
184 
185     @Override
onMetadataChanged(RadioMetadata metadata)186     public void onMetadataChanged(RadioMetadata metadata) {
187         if (metadata == null) {
188             return;
189         }
190 
191         String stationInfo = metadata.getString(RadioMetadata.METADATA_KEY_RDS_PS);
192         RadioRds rds = new RadioRds(stationInfo, null /* songArtist */, null /* songTitle */);
193 
194         RadioStation station = new RadioStation(mCurrentChannel, 0 /* subChannelNumber */,
195                 mCurrentBand, rds);
196         addOrReplaceInScannedStations(station);
197     }
198 
199     @Override
onConfigurationChanged(RadioManager.BandConfig config)200     public void onConfigurationChanged(RadioManager.BandConfig config) {
201         if (config == null) {
202             return;
203         }
204 
205         mCurrentBand = config.getType();
206     }
207 }
208