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.content.SharedPreferences;
21 import android.hardware.radio.RadioManager;
22 import android.os.AsyncTask;
23 import android.os.SystemProperties;
24 import android.support.annotation.NonNull;
25 import android.support.annotation.WorkerThread;
26 import android.util.Log;
27 import com.android.car.radio.service.RadioStation;
28 import com.android.car.radio.demo.DemoRadioStations;
29 import com.android.car.radio.demo.RadioDemo;
30 
31 import java.util.ArrayList;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Set;
35 
36 /**
37  * Class that manages persistent storage of various radio options.
38  */
39 public class RadioStorage {
40     private static final String TAG = "Em.RadioStorage";
41     private static final String PREF_NAME = "com.android.car.radio.RadioStorage";
42 
43     // Keys used for storage in the SharedPreferences.
44     private static final String PREF_KEY_RADIO_BAND = "radio_band";
45     private static final String PREF_KEY_RADIO_CHANNEL_AM = "radio_channel_am";
46     private static final String PREF_KEY_RADIO_CHANNEL_FM = "radio_channel_fm";
47 
48     public static final int INVALID_RADIO_CHANNEL = -1;
49     public static final int INVALID_RADIO_BAND = -1;
50 
51     private static SharedPreferences sSharedPref;
52     private static RadioStorage sInstance;
53     private static RadioDatabase sRadioDatabase;
54 
55     /**
56      * Listener that will be called when something in the radio storage changes.
57      */
58     public interface PresetsChangeListener {
59         /**
60          * Called when {@link #refreshPresets()} has completed.
61          */
onPresetsRefreshed()62         void onPresetsRefreshed();
63     }
64 
65     /**
66      * Listener that will be called when something in the pre-scanned channels has changed.
67      */
68     public interface PreScannedChannelChangeListener {
69         /**
70          * Notifies that the pre-scanned channels for the given radio band has changed.
71          *
72          * @param radioBand One of the band values in {@link RadioManager}.
73          */
onPreScannedChannelChange(int radioBand)74         void onPreScannedChannelChange(int radioBand);
75     }
76 
77     private Set<PresetsChangeListener> mPresetListeners = new HashSet<>();
78 
79     /**
80      * Set of listeners that will be notified whenever pre-scanned channels have changed.
81      *
82      * <p>Note that this set is not initialized because pre-scanned channels are only needed if
83      * dual-tuners exist in the current radio. Thus, this set is created conditionally.
84      */
85     private Set<PreScannedChannelChangeListener> mPreScannedListeners;
86 
87     private List<RadioStation> mPresets = new ArrayList<>();
88 
RadioStorage(Context context)89     private RadioStorage(Context context) {
90         if (sSharedPref == null) {
91             sSharedPref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
92         }
93 
94         if (sRadioDatabase == null) {
95             sRadioDatabase = new RadioDatabase(context);
96         }
97     }
98 
getInstance(Context context)99     public static RadioStorage getInstance(Context context) {
100         if (sInstance == null) {
101             sInstance = new RadioStorage(context.getApplicationContext());
102 
103             // When the RadioStorage is first created, load the list of radio presets.
104             sInstance.refreshPresets();
105         }
106 
107         return sInstance;
108     }
109 
110     /**
111      * Registers the given {@link PresetsChangeListener} to be notified when any radio preset state
112      * has changed.
113      */
addPresetsChangeListener(PresetsChangeListener listener)114     public void addPresetsChangeListener(PresetsChangeListener listener) {
115         mPresetListeners.add(listener);
116     }
117 
118     /**
119      * Unregisters the given {@link PresetsChangeListener}.
120      */
removePresetsChangeListener(PresetsChangeListener listener)121     public void removePresetsChangeListener(PresetsChangeListener listener) {
122         mPresetListeners.remove(listener);
123     }
124 
125     /**
126      * Registers the given {@link PreScannedChannelChangeListener} to be notified of changes to
127      * pre-scanned channels.
128      */
addPreScannedChannelChangeListener(PreScannedChannelChangeListener listener)129     public void addPreScannedChannelChangeListener(PreScannedChannelChangeListener listener) {
130         if (mPreScannedListeners == null) {
131             mPreScannedListeners = new HashSet<>();
132         }
133 
134         mPreScannedListeners.add(listener);
135     }
136 
137     /**
138      * Unregisters the given {@link PreScannedChannelChangeListener}.
139      */
removePreScannedChannelChangeListener(PreScannedChannelChangeListener listener)140     public void removePreScannedChannelChangeListener(PreScannedChannelChangeListener listener) {
141         if (mPreScannedListeners == null) {
142             return;
143         }
144 
145         mPreScannedListeners.remove(listener);
146     }
147 
148     /**
149      * Requests a load of all currently stored presets. This operation runs asynchronously. When
150      * the presets have been loaded, any registered {@link PresetsChangeListener}s are
151      * notified via the {@link PresetsChangeListener#onPresetsRefreshed()} method.
152      */
refreshPresets()153     private void refreshPresets() {
154         new GetAllPresetsAsyncTask().execute();
155     }
156 
157     /**
158      * Returns all currently loaded presets. If there are no stored presets, this method will
159      * return an empty {@link List}.
160      *
161      * <p>Register as a {@link PresetsChangeListener} to be notified of any changes in the
162      * preset list.
163      */
getPresets()164     public List<RadioStation> getPresets() {
165         return mPresets;
166     }
167 
168     /**
169      * Convenience method for checking if a specific channel is a preset. This method will assume
170      * the subchannel is 0.
171      *
172      * @see {@link #isPreset(RadioStation)}
173      * @return {@code true} if the channel is a user saved preset.
174      */
isPreset(int channel, int radioBand)175     public boolean isPreset(int channel, int radioBand) {
176         return isPreset(new RadioStation(channel, 0 /* subchannel */, radioBand, null /* rds */));
177     }
178 
179     /**
180      * Returns {@code true} if the given {@link RadioStation} is a user saved preset.
181      */
isPreset(RadioStation station)182     public boolean isPreset(RadioStation station) {
183         if (station == null) {
184             return false;
185         }
186 
187         // Just iterate through the list and match the station. If we anticipate this list growing
188         // large, might have to change it to some sort of Set.
189         for (RadioStation preset : mPresets) {
190             if (preset.equals(station)) {
191                 return true;
192             }
193         }
194 
195         return false;
196     }
197 
198     /**
199      * Stores that given {@link RadioStation} as a preset. This operation will override any
200      * previously stored preset that matches the given preset.
201      *
202      * <p>Upon a successful store, the presets list will be refreshed via a call to
203      * {@link #refreshPresets()}.
204      *
205      * @see {@link #refreshPresets()}
206      */
storePreset(RadioStation preset)207     public void storePreset(RadioStation preset) {
208         if (preset == null) {
209             return;
210         }
211 
212         new StorePresetAsyncTask().execute(preset);
213     }
214 
215     /**
216      * Removes the given {@link RadioStation} as a preset.
217      *
218      * <p>Upon a successful removal, the presets list will be refreshed via a call to
219      * {@link #refreshPresets()}.
220      *
221      * @see {@link #refreshPresets()}
222      */
removePreset(RadioStation preset)223     public void removePreset(RadioStation preset) {
224         if (preset == null) {
225             return;
226         }
227 
228         new RemovePresetAsyncTask().execute(preset);
229     }
230 
231     /**
232      * Returns the stored radio band that was set in {@link #storeRadioBand(int)}. If a radio band
233      * has not previously been stored, then {@link RadioManager#BAND_FM} is returned.
234      *
235      * @return One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM},
236      * {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}.
237      */
getStoredRadioBand()238     public int getStoredRadioBand() {
239         // No need to verify that the returned value is one of AM_BAND or FM_BAND because this is
240         // done in storeRadioBand(int).
241         return sSharedPref.getInt(PREF_KEY_RADIO_BAND, RadioManager.BAND_FM);
242     }
243 
244     /**
245      * Stores a radio band for later retrieval via {@link #getStoredRadioBand()}.
246      */
storeRadioBand(int radioBand)247     public void storeRadioBand(int radioBand) {
248         // Ensure that an incorrect radio band is not stored. Currently only FM and AM supported.
249         if (radioBand != RadioManager.BAND_FM && radioBand != RadioManager.BAND_AM) {
250             return;
251         }
252 
253         sSharedPref.edit().putInt(PREF_KEY_RADIO_BAND, radioBand).apply();
254     }
255 
256     /**
257      * Returns the stored radio channel that was set in {@link #storeRadioChannel(int, int)}. If a
258      * radio channel for the given band has not been previously stored, then
259      * {@link #INVALID_RADIO_CHANNEL} is returned.
260      *
261      * @param band One of the BAND_* values from {@link RadioManager}. For example,
262      *             {@link RadioManager#BAND_AM}.
263      */
getStoredRadioChannel(int band)264     public int getStoredRadioChannel(int band) {
265         switch (band) {
266             case RadioManager.BAND_AM:
267                 return sSharedPref.getInt(PREF_KEY_RADIO_CHANNEL_AM, INVALID_RADIO_CHANNEL);
268 
269             case RadioManager.BAND_FM:
270                 return sSharedPref.getInt(PREF_KEY_RADIO_CHANNEL_FM, INVALID_RADIO_CHANNEL);
271 
272             default:
273                 return INVALID_RADIO_CHANNEL;
274         }
275     }
276 
277     /**
278      * Stores a radio channel (i.e. the radio frequency) for a particular band so it can be later
279      * retrieved via {@link #getStoredRadioChannel(int band)}.
280      */
storeRadioChannel(int band, int channel)281     public void storeRadioChannel(int band, int channel) {
282         if (Log.isLoggable(TAG, Log.DEBUG)) {
283             Log.d(TAG, String.format("storeRadioChannel(); band: %s, channel %s", band, channel));
284         }
285 
286         if (channel <= 0) {
287             return;
288         }
289 
290         switch (band) {
291             case RadioManager.BAND_AM:
292                 sSharedPref.edit().putInt(PREF_KEY_RADIO_CHANNEL_AM, channel).apply();
293                 break;
294 
295             case RadioManager.BAND_FM:
296                 sSharedPref.edit().putInt(PREF_KEY_RADIO_CHANNEL_FM, channel).apply();
297                 break;
298 
299             default:
300                 Log.w(TAG, "Attempting to store channel for invalid band: " + band);
301         }
302     }
303 
304     /**
305      * Stores the list of {@link RadioStation}s as the pre-scanned stations for the given radio
306      * band.
307      *
308      * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM},
309      * {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}.
310      */
storePreScannedStations(int radioBand, List<RadioStation> stations)311     public void storePreScannedStations(int radioBand, List<RadioStation> stations) {
312         if (stations == null) {
313             return;
314         }
315 
316         new StorePreScannedAsyncTask(radioBand).execute(stations);
317     }
318 
319     /**
320      * Returns the list of pre-scanned radio channels for the given band.
321      */
322     @NonNull
323     @WorkerThread
getPreScannedStationsForBand(int radioBand)324     public List<RadioStation> getPreScannedStationsForBand(int radioBand) {
325         if (SystemProperties.getBoolean(RadioDemo.DEMO_MODE_PROPERTY, false)) {
326             switch (radioBand) {
327                 case RadioManager.BAND_AM:
328                     return DemoRadioStations.getAmStations();
329 
330                 case RadioManager.BAND_FM:
331                 default:
332                     return DemoRadioStations.getFmStations();
333 
334             }
335         }
336 
337         return sRadioDatabase.getAllPreScannedStationsForBand(radioBand);
338     }
339 
340     /**
341      * Calls {@link PresetsChangeListener#onPresetsRefreshed()} for all registered
342      * {@link PresetsChangeListener}s.
343      */
notifyPresetsListeners()344     private void notifyPresetsListeners() {
345         for (PresetsChangeListener listener : mPresetListeners) {
346             listener.onPresetsRefreshed();
347         }
348     }
349 
350     /**
351      * Calls {@link PreScannedChannelChangeListener#onPreScannedChannelChange(int)} for all
352      * registered {@link PreScannedChannelChangeListener}s.
353      */
notifyPreScannedListeners(int radioBand)354     private void notifyPreScannedListeners(int radioBand) {
355         if (mPreScannedListeners == null) {
356             return;
357         }
358 
359         for (PreScannedChannelChangeListener listener : mPreScannedListeners) {
360             listener.onPreScannedChannelChange(radioBand);
361         }
362     }
363 
364     /**
365      * {@link AsyncTask} that will fetch all stored radio presets.
366      */
367     private class GetAllPresetsAsyncTask extends AsyncTask<Void, Void, Void> {
368         private static final String TAG = "Em.GetAllPresetsAT";
369 
370         @Override
doInBackground(Void... voids)371         protected Void doInBackground(Void... voids) {
372             mPresets = sRadioDatabase.getAllPresets();
373 
374             if (Log.isLoggable(TAG, Log.DEBUG)) {
375                 Log.d(TAG, "Loaded presets: " + mPresets);
376             }
377 
378             return null;
379         }
380 
381         @Override
onPostExecute(Void result)382         public void onPostExecute(Void result) {
383             notifyPresetsListeners();
384         }
385     }
386 
387     /**
388      * {@link AsyncTask} that will store a single {@link RadioStation} that is passed to its
389      * {@link AsyncTask#execute(Object[])}.
390      */
391     private class StorePresetAsyncTask extends AsyncTask<RadioStation, Void, Boolean> {
392         private static final String TAG = "Em.StorePresetAT";
393 
394         @Override
doInBackground(RadioStation... radioStations)395         protected Boolean doInBackground(RadioStation... radioStations) {
396             RadioStation presetToStore = radioStations[0];
397             boolean result = sRadioDatabase.insertPreset(presetToStore);
398 
399             if (Log.isLoggable(TAG, Log.DEBUG)) {
400                 Log.d(TAG, "Store preset success: " + result);
401             }
402 
403             if (result) {
404                 // Refresh the presets list.
405                 mPresets = sRadioDatabase.getAllPresets();
406             }
407 
408             return result;
409         }
410 
411         @Override
onPostExecute(Boolean result)412         public void onPostExecute(Boolean result) {
413             if (result) {
414                 notifyPresetsListeners();
415             }
416         }
417     }
418 
419     /**
420      * {@link AsyncTask} that will remove a single {@link RadioStation} that is passed to its
421      * {@link AsyncTask#execute(Object[])}.
422      */
423     private class RemovePresetAsyncTask extends AsyncTask<RadioStation, Void, Boolean> {
424         private static final String TAG = "Em.RemovePresetAT";
425 
426         @Override
doInBackground(RadioStation... radioStations)427         protected Boolean doInBackground(RadioStation... radioStations) {
428             RadioStation presetToStore = radioStations[0];
429             boolean result = sRadioDatabase.deletePreset(presetToStore);
430 
431             if (Log.isLoggable(TAG, Log.DEBUG)) {
432                 Log.d(TAG, "Remove preset success: " + result);
433             }
434 
435             if (result) {
436                 // Refresh the presets list.
437                 mPresets = sRadioDatabase.getAllPresets();
438             }
439 
440             return result;
441         }
442 
443         @Override
onPostExecute(Boolean result)444         public void onPostExecute(Boolean result) {
445             if (result) {
446                 notifyPresetsListeners();
447             }
448         }
449     }
450 
451     /**
452      * {@link AsyncTask} that will store a list of pre-scanned {@link RadioStation}s that is passed
453      * to its {@link AsyncTask#execute(Object[])}.
454      */
455     private class StorePreScannedAsyncTask extends AsyncTask<List<RadioStation>, Void, Boolean> {
456         private static final String TAG = "Em.StorePreScannedAT";
457         private final int mRadioBand;
458 
StorePreScannedAsyncTask(int radioBand)459         public StorePreScannedAsyncTask(int radioBand) {
460             mRadioBand = radioBand;
461         }
462 
463         @Override
doInBackground(List<RadioStation> .... stationsList)464         protected Boolean doInBackground(List<RadioStation> ... stationsList) {
465             List<RadioStation> stations = stationsList[0];
466 
467             boolean result = sRadioDatabase.insertPreScannedStations(mRadioBand, stations);
468 
469             if (Log.isLoggable(TAG, Log.DEBUG)) {
470                 Log.d(TAG, "Store pre-scanned stations success: " + result);
471             }
472 
473             return result;
474         }
475 
476         @Override
onPostExecute(Boolean result)477         public void onPostExecute(Boolean result) {
478             if (result) {
479                 notifyPreScannedListeners(mRadioBand);
480             }
481         }
482     }
483 }
484