Home | History | Annotate | Download | only in voiceinteraction
      1 /**
      2  * Copyright (C) 2014 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.server.voiceinteraction;
     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.hardware.soundtrigger.SoundTrigger;
     25 import android.hardware.soundtrigger.SoundTrigger.Keyphrase;
     26 import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
     27 import android.text.TextUtils;
     28 import android.util.Slog;
     29 
     30 import java.util.ArrayList;
     31 import java.util.Arrays;
     32 import java.util.List;
     33 import java.util.Locale;
     34 import java.util.UUID;
     35 
     36 /**
     37  * Helper to manage the database of the sound models that have been registered on the device.
     38  *
     39  * @hide
     40  */
     41 public class DatabaseHelper extends SQLiteOpenHelper {
     42     static final String TAG = "SoundModelDBHelper";
     43     static final boolean DBG = false;
     44 
     45     private static final String NAME = "sound_model.db";
     46     private static final int VERSION = 6;
     47 
     48     public static interface SoundModelContract {
     49         public static final String TABLE = "sound_model";
     50         public static final String KEY_MODEL_UUID = "model_uuid";
     51         public static final String KEY_VENDOR_UUID = "vendor_uuid";
     52         public static final String KEY_KEYPHRASE_ID = "keyphrase_id";
     53         public static final String KEY_TYPE = "type";
     54         public static final String KEY_DATA = "data";
     55         public static final String KEY_RECOGNITION_MODES = "recognition_modes";
     56         public static final String KEY_LOCALE = "locale";
     57         public static final String KEY_HINT_TEXT = "hint_text";
     58         public static final String KEY_USERS = "users";
     59     }
     60 
     61     // Table Create Statement
     62     private static final String CREATE_TABLE_SOUND_MODEL = "CREATE TABLE "
     63             + SoundModelContract.TABLE + "("
     64             + SoundModelContract.KEY_MODEL_UUID + " TEXT,"
     65             + SoundModelContract.KEY_VENDOR_UUID + " TEXT,"
     66             + SoundModelContract.KEY_KEYPHRASE_ID + " INTEGER,"
     67             + SoundModelContract.KEY_TYPE + " INTEGER,"
     68             + SoundModelContract.KEY_DATA + " BLOB,"
     69             + SoundModelContract.KEY_RECOGNITION_MODES + " INTEGER,"
     70             + SoundModelContract.KEY_LOCALE + " TEXT,"
     71             + SoundModelContract.KEY_HINT_TEXT + " TEXT,"
     72             + SoundModelContract.KEY_USERS + " TEXT,"
     73             + "PRIMARY KEY (" + SoundModelContract.KEY_KEYPHRASE_ID + ","
     74                               + SoundModelContract.KEY_LOCALE + ","
     75                               + SoundModelContract.KEY_USERS + ")"
     76             + ")";
     77 
     78     public DatabaseHelper(Context context) {
     79         super(context, NAME, null, VERSION);
     80     }
     81 
     82     @Override
     83     public void onCreate(SQLiteDatabase db) {
     84         // creating required tables
     85         db.execSQL(CREATE_TABLE_SOUND_MODEL);
     86     }
     87 
     88     @Override
     89     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
     90         if (oldVersion < 4) {
     91             // For old versions just drop the tables and recreate new ones.
     92             db.execSQL("DROP TABLE IF EXISTS " + SoundModelContract.TABLE);
     93             onCreate(db);
     94         } else {
     95             // In the jump to version 5, we added support for the vendor UUID.
     96             if (oldVersion == 4) {
     97                 Slog.d(TAG, "Adding vendor UUID column");
     98                 db.execSQL("ALTER TABLE " + SoundModelContract.TABLE + " ADD COLUMN "
     99                         + SoundModelContract.KEY_VENDOR_UUID + " TEXT");
    100                 oldVersion++;
    101             }
    102         }
    103         if (oldVersion == 5) {
    104             // We need to enforce the new primary key constraint that the
    105             // keyphrase id, locale, and users are unique. We have to first pull
    106             // everything out of the database, remove duplicates, create the new
    107             // table, then push everything back in.
    108             String selectQuery = "SELECT * FROM " + SoundModelContract.TABLE;
    109             Cursor c = db.rawQuery(selectQuery, null);
    110             List<SoundModelRecord> old_records = new ArrayList<SoundModelRecord>();
    111             try {
    112                 if (c.moveToFirst()) {
    113                     do {
    114                         try {
    115                             old_records.add(new SoundModelRecord(5, c));
    116                         } catch (Exception e) {
    117                             Slog.e(TAG, "Failed to extract V5 record", e);
    118                         }
    119                     } while (c.moveToNext());
    120                 }
    121             } finally {
    122                 c.close();
    123             }
    124             db.execSQL("DROP TABLE IF EXISTS " + SoundModelContract.TABLE);
    125             onCreate(db);
    126             for (SoundModelRecord record : old_records) {
    127                 if (record.ifViolatesV6PrimaryKeyIsFirstOfAnyDuplicates(old_records)) {
    128                     try {
    129                         long return_value = record.writeToDatabase(6, db);
    130                         if (return_value == -1) {
    131                             Slog.e(TAG, "Database write failed " + record.modelUuid + ": "
    132                                     + return_value);
    133                         }
    134                     } catch (Exception e) {
    135                         Slog.e(TAG, "Failed to update V6 record " + record.modelUuid, e);
    136                     }
    137                 }
    138             }
    139             oldVersion++;
    140         }
    141     }
    142 
    143     /**
    144      * Updates the given keyphrase model, adds it, if it doesn't already exist.
    145      *
    146      * TODO: We only support one keyphrase currently.
    147      */
    148     public boolean updateKeyphraseSoundModel(KeyphraseSoundModel soundModel) {
    149         synchronized(this) {
    150             SQLiteDatabase db = getWritableDatabase();
    151             ContentValues values = new ContentValues();
    152             values.put(SoundModelContract.KEY_MODEL_UUID, soundModel.uuid.toString());
    153             if (soundModel.vendorUuid != null) {
    154                 values.put(SoundModelContract.KEY_VENDOR_UUID, soundModel.vendorUuid.toString());
    155             }
    156             values.put(SoundModelContract.KEY_TYPE, SoundTrigger.SoundModel.TYPE_KEYPHRASE);
    157             values.put(SoundModelContract.KEY_DATA, soundModel.data);
    158 
    159             if (soundModel.keyphrases != null && soundModel.keyphrases.length == 1) {
    160                 values.put(SoundModelContract.KEY_KEYPHRASE_ID, soundModel.keyphrases[0].id);
    161                 values.put(SoundModelContract.KEY_RECOGNITION_MODES,
    162                         soundModel.keyphrases[0].recognitionModes);
    163                 values.put(SoundModelContract.KEY_USERS,
    164                         getCommaSeparatedString(soundModel.keyphrases[0].users));
    165                 values.put(SoundModelContract.KEY_LOCALE, soundModel.keyphrases[0].locale);
    166                 values.put(SoundModelContract.KEY_HINT_TEXT, soundModel.keyphrases[0].text);
    167                 try {
    168                     return db.insertWithOnConflict(SoundModelContract.TABLE, null, values,
    169                             SQLiteDatabase.CONFLICT_REPLACE) != -1;
    170                 } finally {
    171                     db.close();
    172                 }
    173             }
    174             return false;
    175         }
    176     }
    177 
    178     /**
    179      * Deletes the sound model and associated keyphrases.
    180      */
    181     public boolean deleteKeyphraseSoundModel(int keyphraseId, int userHandle, String bcp47Locale) {
    182         // Sanitize the locale to guard against SQL injection.
    183         bcp47Locale = Locale.forLanguageTag(bcp47Locale).toLanguageTag();
    184         synchronized(this) {
    185             KeyphraseSoundModel soundModel = getKeyphraseSoundModel(keyphraseId, userHandle,
    186                     bcp47Locale);
    187             if (soundModel == null) {
    188                 return false;
    189             }
    190 
    191             // Delete all sound models for the given keyphrase and specified user.
    192             SQLiteDatabase db = getWritableDatabase();
    193             String soundModelClause = SoundModelContract.KEY_MODEL_UUID
    194                     + "='" + soundModel.uuid.toString() + "'";
    195             try {
    196                 return db.delete(SoundModelContract.TABLE, soundModelClause, null) != 0;
    197             } finally {
    198                 db.close();
    199             }
    200         }
    201     }
    202 
    203     /**
    204      * Returns a matching {@link KeyphraseSoundModel} for the keyphrase ID.
    205      * Returns null if a match isn't found.
    206      *
    207      * TODO: We only support one keyphrase currently.
    208      */
    209     public KeyphraseSoundModel getKeyphraseSoundModel(int keyphraseId, int userHandle,
    210             String bcp47Locale) {
    211         // Sanitize the locale to guard against SQL injection.
    212         bcp47Locale = Locale.forLanguageTag(bcp47Locale).toLanguageTag();
    213         synchronized(this) {
    214             // Find the corresponding sound model ID for the keyphrase.
    215             String selectQuery = "SELECT  * FROM " + SoundModelContract.TABLE
    216                     + " WHERE " + SoundModelContract.KEY_KEYPHRASE_ID + "= '" + keyphraseId
    217                     + "' AND " + SoundModelContract.KEY_LOCALE + "='" + bcp47Locale + "'";
    218             SQLiteDatabase db = getReadableDatabase();
    219             Cursor c = db.rawQuery(selectQuery, null);
    220 
    221             try {
    222                 if (c.moveToFirst()) {
    223                     do {
    224                         int type = c.getInt(c.getColumnIndex(SoundModelContract.KEY_TYPE));
    225                         if (type != SoundTrigger.SoundModel.TYPE_KEYPHRASE) {
    226                             if (DBG) {
    227                                 Slog.w(TAG, "Ignoring SoundModel since it's type is incorrect");
    228                             }
    229                             continue;
    230                         }
    231 
    232                         String modelUuid = c.getString(
    233                                 c.getColumnIndex(SoundModelContract.KEY_MODEL_UUID));
    234                         if (modelUuid == null) {
    235                             Slog.w(TAG, "Ignoring SoundModel since it doesn't specify an ID");
    236                             continue;
    237                         }
    238 
    239                         String vendorUuidString = null;
    240                         int vendorUuidColumn = c.getColumnIndex(SoundModelContract.KEY_VENDOR_UUID);
    241                         if (vendorUuidColumn != -1) {
    242                             vendorUuidString = c.getString(vendorUuidColumn);
    243                         }
    244                         byte[] data = c.getBlob(c.getColumnIndex(SoundModelContract.KEY_DATA));
    245                         int recognitionModes = c.getInt(
    246                                 c.getColumnIndex(SoundModelContract.KEY_RECOGNITION_MODES));
    247                         int[] users = getArrayForCommaSeparatedString(
    248                                 c.getString(c.getColumnIndex(SoundModelContract.KEY_USERS)));
    249                         String modelLocale = c.getString(
    250                                 c.getColumnIndex(SoundModelContract.KEY_LOCALE));
    251                         String text = c.getString(
    252                                 c.getColumnIndex(SoundModelContract.KEY_HINT_TEXT));
    253 
    254                         // Only add keyphrases meant for the current user.
    255                         if (users == null) {
    256                             // No users present in the keyphrase.
    257                             Slog.w(TAG, "Ignoring SoundModel since it doesn't specify users");
    258                             continue;
    259                         }
    260 
    261                         boolean isAvailableForCurrentUser = false;
    262                         for (int user : users) {
    263                             if (userHandle == user) {
    264                                 isAvailableForCurrentUser = true;
    265                                 break;
    266                             }
    267                         }
    268                         if (!isAvailableForCurrentUser) {
    269                             if (DBG) {
    270                                 Slog.w(TAG, "Ignoring SoundModel since user handles don't match");
    271                             }
    272                             continue;
    273                         } else {
    274                             if (DBG) Slog.d(TAG, "Found a SoundModel for user: " + userHandle);
    275                         }
    276 
    277                         Keyphrase[] keyphrases = new Keyphrase[1];
    278                         keyphrases[0] = new Keyphrase(
    279                                 keyphraseId, recognitionModes, modelLocale, text, users);
    280                         UUID vendorUuid = null;
    281                         if (vendorUuidString != null) {
    282                             vendorUuid = UUID.fromString(vendorUuidString);
    283                         }
    284                         KeyphraseSoundModel model = new KeyphraseSoundModel(
    285                                 UUID.fromString(modelUuid), vendorUuid, data, keyphrases);
    286                         if (DBG) {
    287                             Slog.d(TAG, "Found SoundModel for the given keyphrase/locale/user: "
    288                                     + model);
    289                         }
    290                         return model;
    291                     } while (c.moveToNext());
    292                 }
    293                 Slog.w(TAG, "No SoundModel available for the given keyphrase");
    294             } finally {
    295                 c.close();
    296                 db.close();
    297             }
    298             return null;
    299         }
    300     }
    301 
    302     private static String getCommaSeparatedString(int[] users) {
    303         if (users == null) {
    304             return "";
    305         }
    306         StringBuilder sb = new StringBuilder();
    307         for (int i = 0; i < users.length; i++) {
    308             if (i != 0) {
    309                 sb.append(',');
    310             }
    311             sb.append(users[i]);
    312         }
    313         return sb.toString();
    314     }
    315 
    316     private static int[] getArrayForCommaSeparatedString(String text) {
    317         if (TextUtils.isEmpty(text)) {
    318             return null;
    319         }
    320         String[] usersStr = text.split(",");
    321         int[] users = new int[usersStr.length];
    322         for (int i = 0; i < usersStr.length; i++) {
    323             users[i] = Integer.parseInt(usersStr[i]);
    324         }
    325         return users;
    326     }
    327 
    328     private static class SoundModelRecord {
    329         public final String modelUuid;
    330         public final String vendorUuid;
    331         public final int keyphraseId;
    332         public final int type;
    333         public final byte[] data;
    334         public final int recognitionModes;
    335         public final String locale;
    336         public final String hintText;
    337         public final String users;
    338 
    339         public SoundModelRecord(int version, Cursor c) {
    340             modelUuid = c.getString(c.getColumnIndex(SoundModelContract.KEY_MODEL_UUID));
    341             if (version >= 5) {
    342                 vendorUuid = c.getString(c.getColumnIndex(SoundModelContract.KEY_VENDOR_UUID));
    343             } else {
    344                 vendorUuid = null;
    345             }
    346             keyphraseId = c.getInt(c.getColumnIndex(SoundModelContract.KEY_KEYPHRASE_ID));
    347             type = c.getInt(c.getColumnIndex(SoundModelContract.KEY_TYPE));
    348             data = c.getBlob(c.getColumnIndex(SoundModelContract.KEY_DATA));
    349             recognitionModes = c.getInt(c.getColumnIndex(SoundModelContract.KEY_RECOGNITION_MODES));
    350             locale = c.getString(c.getColumnIndex(SoundModelContract.KEY_LOCALE));
    351             hintText = c.getString(c.getColumnIndex(SoundModelContract.KEY_HINT_TEXT));
    352             users = c.getString(c.getColumnIndex(SoundModelContract.KEY_USERS));
    353         }
    354 
    355         private boolean V6PrimaryKeyMatches(SoundModelRecord record) {
    356           return keyphraseId == record.keyphraseId && stringComparisonHelper(locale, record.locale)
    357               && stringComparisonHelper(users, record.users);
    358         }
    359 
    360         // Returns true if this record is a) the only record with the same V6 primary key, or b) the
    361         // first record in the list of all records that have the same primary key and equal data.
    362         // It will return false if a) there are any records that have the same primary key and
    363         // different data, or b) there is a previous record in the list that has the same primary
    364         // key and data.
    365         // Note that 'this' object must be inside the list.
    366         public boolean ifViolatesV6PrimaryKeyIsFirstOfAnyDuplicates(
    367                 List<SoundModelRecord> records) {
    368             // First pass - check to see if all the records that have the same primary key have
    369             // duplicated data.
    370             for (SoundModelRecord record : records) {
    371                 if (this == record) {
    372                     continue;
    373                 }
    374                 // If we have different/missing data with the same primary key, then we should drop
    375                 // everything.
    376                 if (this.V6PrimaryKeyMatches(record) && !Arrays.equals(data, record.data)) {
    377                     return false;
    378                 }
    379             }
    380 
    381             // We only want to return true for the first duplicated model.
    382             for (SoundModelRecord record : records) {
    383                 if (this.V6PrimaryKeyMatches(record)) {
    384                     return this == record;
    385                 }
    386             }
    387             return true;
    388         }
    389 
    390         public long writeToDatabase(int version, SQLiteDatabase db) {
    391             ContentValues values = new ContentValues();
    392             values.put(SoundModelContract.KEY_MODEL_UUID, modelUuid);
    393             if (version >= 5) {
    394                 values.put(SoundModelContract.KEY_VENDOR_UUID, vendorUuid);
    395             }
    396             values.put(SoundModelContract.KEY_KEYPHRASE_ID, keyphraseId);
    397             values.put(SoundModelContract.KEY_TYPE, type);
    398             values.put(SoundModelContract.KEY_DATA, data);
    399             values.put(SoundModelContract.KEY_RECOGNITION_MODES, recognitionModes);
    400             values.put(SoundModelContract.KEY_LOCALE, locale);
    401             values.put(SoundModelContract.KEY_HINT_TEXT, hintText);
    402             values.put(SoundModelContract.KEY_USERS, users);
    403 
    404             return db.insertWithOnConflict(
    405                        SoundModelContract.TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
    406         }
    407 
    408         // Helper for checking string equality - including the case when they are null.
    409         static private boolean stringComparisonHelper(String a, String b) {
    410           if (a != null) {
    411             return a.equals(b);
    412           }
    413           return a == b;
    414         }
    415     }
    416 }
    417