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 implements IEnrolledModelDb { 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 @Override updateKeyphraseSoundModel(KeyphraseSoundModel soundModel)157 public boolean updateKeyphraseSoundModel(KeyphraseSoundModel soundModel) { 158 synchronized(this) { 159 SQLiteDatabase db = getWritableDatabase(); 160 ContentValues values = new ContentValues(); 161 values.put(SoundModelContract.KEY_MODEL_UUID, soundModel.getUuid().toString()); 162 if (soundModel.getVendorUuid() != null) { 163 values.put(SoundModelContract.KEY_VENDOR_UUID, 164 soundModel.getVendorUuid().toString()); 165 } 166 values.put(SoundModelContract.KEY_TYPE, SoundTrigger.SoundModel.TYPE_KEYPHRASE); 167 values.put(SoundModelContract.KEY_DATA, soundModel.getData()); 168 values.put(SoundModelContract.KEY_MODEL_VERSION, soundModel.getVersion()); 169 170 if (soundModel.getKeyphrases() != null && soundModel.getKeyphrases().length == 1) { 171 values.put(SoundModelContract.KEY_KEYPHRASE_ID, 172 soundModel.getKeyphrases()[0].getId()); 173 values.put(SoundModelContract.KEY_RECOGNITION_MODES, 174 soundModel.getKeyphrases()[0].getRecognitionModes()); 175 values.put(SoundModelContract.KEY_USERS, 176 getCommaSeparatedString(soundModel.getKeyphrases()[0].getUsers())); 177 values.put(SoundModelContract.KEY_LOCALE, 178 soundModel.getKeyphrases()[0].getLocale().toLanguageTag()); 179 values.put(SoundModelContract.KEY_HINT_TEXT, 180 soundModel.getKeyphrases()[0].getText()); 181 try { 182 return db.insertWithOnConflict(SoundModelContract.TABLE, null, values, 183 SQLiteDatabase.CONFLICT_REPLACE) != -1; 184 } finally { 185 db.close(); 186 } 187 } 188 return false; 189 } 190 } 191 192 @Override deleteKeyphraseSoundModel(int keyphraseId, int userHandle, String bcp47Locale)193 public boolean deleteKeyphraseSoundModel(int keyphraseId, int userHandle, String bcp47Locale) { 194 // Normalize the locale to guard against SQL injection. 195 bcp47Locale = Locale.forLanguageTag(bcp47Locale).toLanguageTag(); 196 synchronized(this) { 197 KeyphraseSoundModel soundModel = getKeyphraseSoundModel(keyphraseId, userHandle, 198 bcp47Locale); 199 if (soundModel == null) { 200 return false; 201 } 202 203 // Delete all sound models for the given keyphrase and specified user. 204 SQLiteDatabase db = getWritableDatabase(); 205 String soundModelClause = SoundModelContract.KEY_MODEL_UUID 206 + "='" + soundModel.getUuid().toString() + "'"; 207 try { 208 return db.delete(SoundModelContract.TABLE, soundModelClause, null) != 0; 209 } finally { 210 db.close(); 211 } 212 } 213 } 214 215 @Override getKeyphraseSoundModel(int keyphraseId, int userHandle, String bcp47Locale)216 public KeyphraseSoundModel getKeyphraseSoundModel(int keyphraseId, int userHandle, 217 String bcp47Locale) { 218 // Sanitize the locale to guard against SQL injection. 219 bcp47Locale = Locale.forLanguageTag(bcp47Locale).toLanguageTag(); 220 synchronized(this) { 221 // Find the corresponding sound model ID for the keyphrase. 222 String selectQuery = "SELECT * FROM " + SoundModelContract.TABLE 223 + " WHERE " + SoundModelContract.KEY_KEYPHRASE_ID + "= '" + keyphraseId 224 + "' AND " + SoundModelContract.KEY_LOCALE + "='" + bcp47Locale + "'"; 225 return getValidKeyphraseSoundModelForUser(selectQuery, userHandle); 226 } 227 } 228 229 @Override getKeyphraseSoundModel(String keyphrase, int userHandle, String bcp47Locale)230 public KeyphraseSoundModel getKeyphraseSoundModel(String keyphrase, int userHandle, 231 String bcp47Locale) { 232 // Sanitize the locale to guard against SQL injection. 233 bcp47Locale = Locale.forLanguageTag(bcp47Locale).toLanguageTag(); 234 synchronized (this) { 235 // Find the corresponding sound model ID for the keyphrase. 236 String selectQuery = "SELECT * FROM " + SoundModelContract.TABLE 237 + " WHERE " + SoundModelContract.KEY_HINT_TEXT + "= '" + keyphrase 238 + "' AND " + SoundModelContract.KEY_LOCALE + "='" + bcp47Locale + "'"; 239 return getValidKeyphraseSoundModelForUser(selectQuery, userHandle); 240 } 241 } 242 getValidKeyphraseSoundModelForUser(String selectQuery, int userHandle)243 private KeyphraseSoundModel getValidKeyphraseSoundModelForUser(String selectQuery, 244 int userHandle) { 245 SQLiteDatabase db = getReadableDatabase(); 246 Cursor c = db.rawQuery(selectQuery, null); 247 if (DBG) { 248 Slog.w(TAG, "querying database: " + selectQuery); 249 } 250 251 try { 252 if (c.moveToFirst()) { 253 do { 254 int type = c.getInt(c.getColumnIndex(SoundModelContract.KEY_TYPE)); 255 if (type != SoundTrigger.SoundModel.TYPE_KEYPHRASE) { 256 if (DBG) { 257 Slog.w(TAG, "Ignoring SoundModel since its type is incorrect"); 258 } 259 continue; 260 } 261 262 String modelUuid = c.getString( 263 c.getColumnIndex(SoundModelContract.KEY_MODEL_UUID)); 264 if (modelUuid == null) { 265 Slog.w(TAG, "Ignoring SoundModel since it doesn't specify an ID"); 266 continue; 267 } 268 269 String vendorUuidString = null; 270 int vendorUuidColumn = c.getColumnIndex(SoundModelContract.KEY_VENDOR_UUID); 271 if (vendorUuidColumn != -1) { 272 vendorUuidString = c.getString(vendorUuidColumn); 273 } 274 int keyphraseId = c.getInt( 275 c.getColumnIndex(SoundModelContract.KEY_KEYPHRASE_ID)); 276 byte[] data = c.getBlob(c.getColumnIndex(SoundModelContract.KEY_DATA)); 277 int recognitionModes = c.getInt( 278 c.getColumnIndex(SoundModelContract.KEY_RECOGNITION_MODES)); 279 int[] users = getArrayForCommaSeparatedString( 280 c.getString(c.getColumnIndex(SoundModelContract.KEY_USERS))); 281 Locale modelLocale = Locale.forLanguageTag(c.getString( 282 c.getColumnIndex(SoundModelContract.KEY_LOCALE))); 283 String text = c.getString( 284 c.getColumnIndex(SoundModelContract.KEY_HINT_TEXT)); 285 int version = c.getInt( 286 c.getColumnIndex(SoundModelContract.KEY_MODEL_VERSION)); 287 288 // Only add keyphrases meant for the current user. 289 if (users == null) { 290 // No users present in the keyphrase. 291 Slog.w(TAG, "Ignoring SoundModel since it doesn't specify users"); 292 continue; 293 } 294 295 boolean isAvailableForCurrentUser = false; 296 for (int user : users) { 297 if (userHandle == user) { 298 isAvailableForCurrentUser = true; 299 break; 300 } 301 } 302 if (!isAvailableForCurrentUser) { 303 if (DBG) { 304 Slog.w(TAG, "Ignoring SoundModel since user handles don't match"); 305 } 306 continue; 307 } else { 308 if (DBG) Slog.d(TAG, "Found a SoundModel for user: " + userHandle); 309 } 310 311 Keyphrase[] keyphrases = new Keyphrase[1]; 312 keyphrases[0] = new Keyphrase( 313 keyphraseId, recognitionModes, modelLocale, text, users); 314 UUID vendorUuid = null; 315 if (vendorUuidString != null) { 316 vendorUuid = UUID.fromString(vendorUuidString); 317 } 318 KeyphraseSoundModel model = new KeyphraseSoundModel( 319 UUID.fromString(modelUuid), vendorUuid, data, keyphrases, version); 320 if (DBG) { 321 Slog.d(TAG, "Found SoundModel for the given keyphrase/locale/user: " 322 + model); 323 } 324 return model; 325 } while (c.moveToNext()); 326 } 327 328 if (DBG) { 329 Slog.w(TAG, "No SoundModel available for the given keyphrase"); 330 } 331 } finally { 332 c.close(); 333 db.close(); 334 } 335 336 return null; 337 } 338 getCommaSeparatedString(int[] users)339 private static String getCommaSeparatedString(int[] users) { 340 if (users == null) { 341 return ""; 342 } 343 StringBuilder sb = new StringBuilder(); 344 for (int i = 0; i < users.length; i++) { 345 if (i != 0) { 346 sb.append(','); 347 } 348 sb.append(users[i]); 349 } 350 return sb.toString(); 351 } 352 getArrayForCommaSeparatedString(String text)353 private static int[] getArrayForCommaSeparatedString(String text) { 354 if (TextUtils.isEmpty(text)) { 355 return null; 356 } 357 String[] usersStr = text.split(","); 358 int[] users = new int[usersStr.length]; 359 for (int i = 0; i < usersStr.length; i++) { 360 users[i] = Integer.parseInt(usersStr[i]); 361 } 362 return users; 363 } 364 365 /** 366 * SoundModelRecord is no longer used, and it should only be used on database migration. 367 * This class does not need to be modified when modifying the database scheme. 368 */ 369 private static class SoundModelRecord { 370 public final String modelUuid; 371 public final String vendorUuid; 372 public final int keyphraseId; 373 public final int type; 374 public final byte[] data; 375 public final int recognitionModes; 376 public final String locale; 377 public final String hintText; 378 public final String users; 379 SoundModelRecord(int version, Cursor c)380 public SoundModelRecord(int version, Cursor c) { 381 modelUuid = c.getString(c.getColumnIndex(SoundModelContract.KEY_MODEL_UUID)); 382 if (version >= 5) { 383 vendorUuid = c.getString(c.getColumnIndex(SoundModelContract.KEY_VENDOR_UUID)); 384 } else { 385 vendorUuid = null; 386 } 387 keyphraseId = c.getInt(c.getColumnIndex(SoundModelContract.KEY_KEYPHRASE_ID)); 388 type = c.getInt(c.getColumnIndex(SoundModelContract.KEY_TYPE)); 389 data = c.getBlob(c.getColumnIndex(SoundModelContract.KEY_DATA)); 390 recognitionModes = c.getInt(c.getColumnIndex(SoundModelContract.KEY_RECOGNITION_MODES)); 391 locale = c.getString(c.getColumnIndex(SoundModelContract.KEY_LOCALE)); 392 hintText = c.getString(c.getColumnIndex(SoundModelContract.KEY_HINT_TEXT)); 393 users = c.getString(c.getColumnIndex(SoundModelContract.KEY_USERS)); 394 } 395 V6PrimaryKeyMatches(SoundModelRecord record)396 private boolean V6PrimaryKeyMatches(SoundModelRecord record) { 397 return keyphraseId == record.keyphraseId && stringComparisonHelper(locale, record.locale) 398 && stringComparisonHelper(users, record.users); 399 } 400 401 // Returns true if this record is a) the only record with the same V6 primary key, or b) the 402 // first record in the list of all records that have the same primary key and equal data. 403 // It will return false if a) there are any records that have the same primary key and 404 // different data, or b) there is a previous record in the list that has the same primary 405 // key and data. 406 // Note that 'this' object must be inside the list. ifViolatesV6PrimaryKeyIsFirstOfAnyDuplicates( List<SoundModelRecord> records)407 public boolean ifViolatesV6PrimaryKeyIsFirstOfAnyDuplicates( 408 List<SoundModelRecord> records) { 409 // First pass - check to see if all the records that have the same primary key have 410 // duplicated data. 411 for (SoundModelRecord record : records) { 412 if (this == record) { 413 continue; 414 } 415 // If we have different/missing data with the same primary key, then we should drop 416 // everything. 417 if (this.V6PrimaryKeyMatches(record) && !Arrays.equals(data, record.data)) { 418 return false; 419 } 420 } 421 422 // We only want to return true for the first duplicated model. 423 for (SoundModelRecord record : records) { 424 if (this.V6PrimaryKeyMatches(record)) { 425 return this == record; 426 } 427 } 428 return true; 429 } 430 writeToDatabase(int version, SQLiteDatabase db)431 public long writeToDatabase(int version, SQLiteDatabase db) { 432 ContentValues values = new ContentValues(); 433 values.put(SoundModelContract.KEY_MODEL_UUID, modelUuid); 434 if (version >= 5) { 435 values.put(SoundModelContract.KEY_VENDOR_UUID, vendorUuid); 436 } 437 values.put(SoundModelContract.KEY_KEYPHRASE_ID, keyphraseId); 438 values.put(SoundModelContract.KEY_TYPE, type); 439 values.put(SoundModelContract.KEY_DATA, data); 440 values.put(SoundModelContract.KEY_RECOGNITION_MODES, recognitionModes); 441 values.put(SoundModelContract.KEY_LOCALE, locale); 442 values.put(SoundModelContract.KEY_HINT_TEXT, hintText); 443 values.put(SoundModelContract.KEY_USERS, users); 444 445 return db.insertWithOnConflict( 446 SoundModelContract.TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE); 447 } 448 449 // Helper for checking string equality - including the case when they are null. stringComparisonHelper(String a, String b)450 static private boolean stringComparisonHelper(String a, String b) { 451 if (a != null) { 452 return a.equals(b); 453 } 454 return a == b; 455 } 456 } 457 458 /** 459 * Dumps contents of database for dumpsys 460 */ dump(PrintWriter pw)461 public void dump(PrintWriter pw) { 462 synchronized (this) { 463 String selectQuery = "SELECT * FROM " + SoundModelContract.TABLE; 464 SQLiteDatabase db = getReadableDatabase(); 465 Cursor c = db.rawQuery(selectQuery, null); 466 try { 467 pw.println(" Enrolled KeyphraseSoundModels:"); 468 if (c.moveToFirst()) { 469 String[] columnNames = c.getColumnNames(); 470 do { 471 for (String name : columnNames) { 472 int colNameIndex = c.getColumnIndex(name); 473 int type = c.getType(colNameIndex); 474 switch (type) { 475 case Cursor.FIELD_TYPE_STRING: 476 pw.printf(" %s: %s\n", name, 477 c.getString(colNameIndex)); 478 break; 479 case Cursor.FIELD_TYPE_BLOB: 480 pw.printf(" %s: data blob\n", name); 481 break; 482 case Cursor.FIELD_TYPE_INTEGER: 483 pw.printf(" %s: %d\n", name, 484 c.getInt(colNameIndex)); 485 break; 486 case Cursor.FIELD_TYPE_FLOAT: 487 pw.printf(" %s: %f\n", name, 488 c.getFloat(colNameIndex)); 489 break; 490 case Cursor.FIELD_TYPE_NULL: 491 pw.printf(" %s: null\n", name); 492 break; 493 } 494 } 495 pw.println(); 496 } while (c.moveToNext()); 497 } 498 } finally { 499 c.close(); 500 db.close(); 501 } 502 } 503 } 504 } 505