• 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 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