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 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 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 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 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 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 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 */ 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 376 public void onCreate(SQLiteDatabase db) { 377 db.execSQL(CREATE_PRESETS_TABLE); 378 db.execSQL(CREATE_PRE_SCAN_TABLE); 379 } 380 381 @Override 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 399 public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { 400 onUpgrade(db, oldVersion, newVersion); 401 } 402 } 403