• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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