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.ContentValues;
20 import android.content.Context;
21 import android.database.Cursor;
22 import android.database.sqlite.SQLiteDatabase;
23 import android.database.sqlite.SQLiteOpenHelper;
24 import android.os.Looper;
25 import android.support.annotation.NonNull;
26 import android.support.annotation.WorkerThread;
27 import android.text.TextUtils;
28 import android.util.Log;
29 import com.android.car.radio.service.RadioRds;
30 import com.android.car.radio.service.RadioStation;
31 
32 import java.util.ArrayList;
33 import java.util.List;
34 
35 /**
36  * A helper class that manages all operations relating to the database. This class should not
37  * be accessed directly. Instead, {@link RadioStorage} interfaces directly with it.
38  */
39 public final class RadioDatabase extends SQLiteOpenHelper {
40     private static final String TAG = "Em.RadioDatabase";
41 
42     private static final String DATABASE_NAME = "RadioDatabase";
43     private static final int DATABASE_VERSION = 1;
44 
45     /**
46      * The table that holds all the user's currently stored presets.
47      */
48     private static final class RadioPresetsTable {
49         public static final String NAME = "presets_table";
50 
51         private static final class Columns {
52             public static final String CHANNEL_NUMBER = "channel_number";
53             public static final String SUB_CHANNEL = "sub_channel";
54             public static final String BAND = "band";
55             public static final String PROGRAM_SERVICE = "program_service";
56         }
57     }
58 
59     /**
60      * Creates the radio presets table. A channel number together with its subchannel number
61      * represents the primary key
62      */
63     private static final String CREATE_PRESETS_TABLE =
64             "CREATE TABLE " + RadioPresetsTable.NAME + " ("
65                     + RadioPresetsTable.Columns.CHANNEL_NUMBER + " INTEGER NOT NULL, "
66                     + RadioPresetsTable.Columns.SUB_CHANNEL + " INTEGER NOT NULL, "
67                     + RadioPresetsTable.Columns.BAND + " INTEGER NOT NULL, "
68                     + RadioPresetsTable.Columns.PROGRAM_SERVICE + " TEXT, "
69                     + "PRIMARY KEY ("
70                     + RadioPresetsTable.Columns.CHANNEL_NUMBER + ", "
71                     + RadioPresetsTable.Columns.SUB_CHANNEL + "));";
72 
73     private static final String DELETE_PRESETS_TABLE =
74             "DROP TABLE IF EXISTS " + RadioPresetsTable.NAME;
75 
76     /**
77      * Query to return the entire {@link RadioPresetsTable}.
78      */
79     private static final String GET_ALL_PRESETS =
80             "SELECT * FROM " + RadioPresetsTable.NAME
81                     + " ORDER BY " + RadioPresetsTable.Columns.CHANNEL_NUMBER
82                     + ", " + RadioPresetsTable.Columns.SUB_CHANNEL;
83 
84     /**
85      * The WHERE clause for a delete preset operation. A preset is identified uniquely by its
86      * channel and subchannel number.
87      */
88     private static final String DELETE_PRESETS_WHERE_CLAUSE =
89             RadioPresetsTable.Columns.CHANNEL_NUMBER + " = ? AND "
90             + RadioPresetsTable.Columns.SUB_CHANNEL + " = ?";
91 
92     /**
93      * The table that holds all radio stations have have been pre-scanned by a secondary tuner.
94      */
95     private static final class PreScannedStationsTable {
96         public static final String NAME = "pre_scanned_table";
97 
98         private static final class Columns {
99             public static final String CHANNEL_NUMBER = "channel_number";
100             public static final String SUB_CHANNEL = "sub_channel";
101             public static final String BAND = "band";
102             public static final String PROGRAM_SERVICE = "program_service";
103         }
104     }
105 
106     /**
107      * Creates the radio pre-scanned table. A channel number together with its subchannel number
108      * represents the primary key
109      */
110     private static final String CREATE_PRE_SCAN_TABLE =
111             "CREATE TABLE " + PreScannedStationsTable.NAME + " ("
112                     + PreScannedStationsTable.Columns.CHANNEL_NUMBER + " INTEGER NOT NULL, "
113                     + PreScannedStationsTable.Columns.SUB_CHANNEL + " INTEGER NOT NULL, "
114                     + PreScannedStationsTable.Columns.BAND + " INTEGER NOT NULL, "
115                     + PreScannedStationsTable.Columns.PROGRAM_SERVICE + " TEXT, "
116                     + "PRIMARY KEY ("
117                     + PreScannedStationsTable.Columns.CHANNEL_NUMBER + ", "
118                     + PreScannedStationsTable.Columns.SUB_CHANNEL + "));";
119 
120     private static final String DELETE_PRE_SCAN_TABLE =
121             "DROP TABLE IF EXISTS " + PreScannedStationsTable.NAME;
122 
123     /**
124      * Query to return all the pre-scanned stations for a particular radio band.
125      */
126     private static final String GET_ALL_PRE_SCAN_FOR_BAND =
127             "SELECT * FROM " + PreScannedStationsTable.NAME
128                     + " WHERE " + PreScannedStationsTable.Columns.BAND  + " = ? "
129                     + " ORDER BY " + PreScannedStationsTable.Columns.CHANNEL_NUMBER
130                     + ", " + PreScannedStationsTable.Columns.SUB_CHANNEL;
131 
132     /**
133      * The WHERE clause for a delete operation that will remove all pre-scanned stations for a
134      * paritcular radio band.
135      */
136     private static final String DELETE_PRE_SCAN_WHERE_CLAUSE =
137             PreScannedStationsTable.Columns.BAND + " = ?";
138 
RadioDatabase(Context context)139     public RadioDatabase(Context context) {
140         super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION);
141     }
142 
143     /**
144      * Returns a list of all user defined radio presets sorted by channel number and then sub
145      * channel number. If there are no presets, then an empty {@link List} is returned.
146      */
147     @WorkerThread
getAllPresets()148     public List<RadioStation> getAllPresets() {
149         assertNotMainThread();
150 
151         if (Log.isLoggable(TAG, Log.DEBUG)) {
152             Log.d(TAG, "getAllPresets()");
153         }
154 
155         SQLiteDatabase db = getReadableDatabase();
156         List<RadioStation> presets = new ArrayList<>();
157         Cursor cursor = null;
158 
159         db.beginTransaction();
160         try {
161             cursor = db.rawQuery(GET_ALL_PRESETS, null /* selectionArgs */);
162             while (cursor.moveToNext()) {
163                 int channel = cursor.getInt(
164                         cursor.getColumnIndexOrThrow(RadioPresetsTable.Columns.CHANNEL_NUMBER));
165                 int subChannel = cursor.getInt(
166                         cursor.getColumnIndexOrThrow(RadioPresetsTable.Columns.SUB_CHANNEL));
167                 int band = cursor.getInt(
168                         cursor.getColumnIndexOrThrow(RadioPresetsTable.Columns.BAND));
169                 String programService = cursor.getString(
170                         cursor.getColumnIndexOrThrow(RadioPresetsTable.Columns.PROGRAM_SERVICE));
171 
172                 RadioRds rds = null;
173                 if (!TextUtils.isEmpty(programService)) {
174                     rds = new RadioRds(programService, null /* songArtist */, null /* songTitle */);
175                 }
176 
177                 presets.add(new RadioStation(channel, subChannel, band, rds));
178             }
179 
180             db.setTransactionSuccessful();
181         } finally {
182             if (cursor != null) {
183                 cursor.close();
184             }
185 
186             db.endTransaction();
187             db.close();
188         }
189 
190         return presets;
191     }
192 
193     /**
194      * Inserts that given {@link RadioStation} as a preset into the database. The given station
195      * will replace any existing station in the database if there is a conflict.
196      *
197      * @return {@code true} if the operation succeeded.
198      */
199     @WorkerThread
insertPreset(RadioStation preset)200     public boolean insertPreset(RadioStation preset) {
201         assertNotMainThread();
202 
203         ContentValues values = new ContentValues();
204         values.put(RadioPresetsTable.Columns.CHANNEL_NUMBER, preset.getChannelNumber());
205         values.put(RadioPresetsTable.Columns.SUB_CHANNEL, preset.getSubChannelNumber());
206         values.put(RadioPresetsTable.Columns.BAND, preset.getRadioBand());
207 
208         if (preset.getRds() != null) {
209             values.put(RadioPresetsTable.Columns.PROGRAM_SERVICE,
210                     preset.getRds().getProgramService());
211         }
212 
213         SQLiteDatabase db = getWritableDatabase();
214         long status = -1;
215 
216         db.beginTransaction();
217         try {
218             status = db.insertWithOnConflict(RadioPresetsTable.NAME, null /* nullColumnHack */,
219                     values, SQLiteDatabase.CONFLICT_REPLACE);
220 
221             db.setTransactionSuccessful();
222         } finally {
223             db.endTransaction();
224             db.close();
225         }
226 
227         return status != -1;
228     }
229 
230     /**
231      * Removes the preset represented by the given {@link RadioStation}.
232      *
233      * @return {@code true} if the operation succeeded.
234      */
235     @WorkerThread
deletePreset(RadioStation preset)236     public boolean deletePreset(RadioStation preset) {
237         assertNotMainThread();
238 
239         SQLiteDatabase db = getWritableDatabase();
240         long rowsDeleted = 0;
241 
242         db.beginTransaction();
243         try {
244             String channelNumber = Integer.toString(preset.getChannelNumber());
245             String subChannelNumber = Integer.toString(preset.getSubChannelNumber());
246 
247             rowsDeleted = db.delete(RadioPresetsTable.NAME, DELETE_PRESETS_WHERE_CLAUSE,
248                     new String[] { channelNumber, subChannelNumber });
249 
250             db.setTransactionSuccessful();
251         } finally {
252             db.endTransaction();
253             db.close();
254         }
255 
256         return rowsDeleted != 0;
257     }
258 
259     /**
260      * Returns all the pre-scanned stations for the given radio band.
261      *
262      * @param radioBand One of the band values in {@link android.hardware.radio.RadioManager}.
263      * @return A list of pre-scanned stations or an empty array if no stations found.
264      */
265     @NonNull
266     @WorkerThread
getAllPreScannedStationsForBand(int radioBand)267     public List<RadioStation> getAllPreScannedStationsForBand(int radioBand) {
268         assertNotMainThread();
269 
270         if (Log.isLoggable(TAG, Log.DEBUG)) {
271             Log.d(TAG, "getAllPreScannedStationsForBand()");
272         }
273 
274         SQLiteDatabase db = getReadableDatabase();
275         List<RadioStation> stations = new ArrayList<>();
276         Cursor cursor = null;
277 
278         db.beginTransaction();
279         try {
280             cursor = db.rawQuery(GET_ALL_PRE_SCAN_FOR_BAND,
281                     new String[] { Integer.toString(radioBand) });
282 
283             while (cursor.moveToNext()) {
284                 int channel = cursor.getInt(cursor.getColumnIndexOrThrow(
285                         PreScannedStationsTable.Columns.CHANNEL_NUMBER));
286                 int subChannel = cursor.getInt(
287                         cursor.getColumnIndexOrThrow(PreScannedStationsTable.Columns.SUB_CHANNEL));
288                 int band = cursor.getInt(
289                         cursor.getColumnIndexOrThrow(PreScannedStationsTable.Columns.BAND));
290                 String programService = cursor.getString(cursor.getColumnIndexOrThrow(
291                         PreScannedStationsTable.Columns.PROGRAM_SERVICE));
292 
293                 RadioRds rds = null;
294                 if (!TextUtils.isEmpty(programService)) {
295                     rds = new RadioRds(programService, null /* songArtist */, null /* songTitle */);
296                 }
297 
298                 stations.add(new RadioStation(channel, subChannel, band, rds));
299             }
300 
301             db.setTransactionSuccessful();
302         } finally {
303             if (cursor != null) {
304                 cursor.close();
305             }
306 
307             db.endTransaction();
308             db.close();
309         }
310 
311         return stations;
312     }
313 
314     /**
315      * Inserts the given list of {@link RadioStation}s as the list of pre-scanned stations for the
316      * given band. This operation will clear all currently stored stations for the given band
317      * and replace them with the given list.
318      *
319      * @param radioBand One of the band values in {@link android.hardware.radio.RadioManager}.
320      * @param stations A list of {@link RadioStation}s representing the pre-scanned stations.
321      * @return {@code true} if the operation was successful.
322      */
323     @WorkerThread
insertPreScannedStations(int radioBand, List<RadioStation> stations)324     public boolean insertPreScannedStations(int radioBand, List<RadioStation> stations) {
325         assertNotMainThread();
326 
327         SQLiteDatabase db = getWritableDatabase();
328         db.beginTransaction();
329 
330         long status = -1;
331 
332         try {
333             // First clear all pre-scanned stations for the given radio band so that they can be
334             // replaced by the list of stations.
335             db.delete(PreScannedStationsTable.NAME, DELETE_PRE_SCAN_WHERE_CLAUSE,
336                     new String[] { Integer.toString(radioBand) });
337 
338             for (RadioStation station : stations) {
339                 ContentValues values = new ContentValues();
340                 values.put(PreScannedStationsTable.Columns.CHANNEL_NUMBER,
341                         station.getChannelNumber());
342                 values.put(PreScannedStationsTable.Columns.SUB_CHANNEL,
343                         station.getSubChannelNumber());
344                 values.put(PreScannedStationsTable.Columns.BAND, station.getRadioBand());
345 
346                 if (station.getRds() != null) {
347                     values.put(PreScannedStationsTable.Columns.PROGRAM_SERVICE,
348                             station.getRds().getProgramService());
349                 }
350 
351                 status = db.insertWithOnConflict(PreScannedStationsTable.NAME,
352                         null /* nullColumnHack */, values, SQLiteDatabase.CONFLICT_REPLACE);
353             }
354 
355             db.setTransactionSuccessful();
356         } finally {
357             db.endTransaction();
358             db.close();
359         }
360 
361         return status != -1;
362     }
363 
364     /**
365      * Checks that the current thread is not the main thread. If it is, then an
366      * {@link IllegalStateException} is thrown. This assert should be called before all database
367      * operations.
368      */
assertNotMainThread()369     private void assertNotMainThread() {
370         if (Looper.myLooper() == Looper.getMainLooper()) {
371             throw new IllegalStateException("Attempting to call database methods on main thread.");
372         }
373     }
374 
375     @Override
onCreate(SQLiteDatabase db)376     public void onCreate(SQLiteDatabase db) {
377         db.execSQL(CREATE_PRESETS_TABLE);
378         db.execSQL(CREATE_PRE_SCAN_TABLE);
379     }
380 
381     @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)382     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
383         db.beginTransaction();
384 
385         try {
386             // Currently no upgrade steps as this is the first version of the database. Simply drop
387             // all tables and re-create.
388             db.execSQL(DELETE_PRESETS_TABLE);
389             db.execSQL(DELETE_PRE_SCAN_TABLE);
390             onCreate(db);
391 
392             db.setTransactionSuccessful();
393         } finally {
394             db.endTransaction();
395         }
396     }
397 
398     @Override
onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)399     public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
400         onUpgrade(db, oldVersion, newVersion);
401     }
402 }
403