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