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