Home | History | Annotate | Download | only in radio
      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