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
17package com.android.server.voiceinteraction;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.database.Cursor;
22import android.database.sqlite.SQLiteDatabase;
23import android.database.sqlite.SQLiteOpenHelper;
24import android.hardware.soundtrigger.SoundTrigger;
25import android.hardware.soundtrigger.SoundTrigger.Keyphrase;
26import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
27import android.text.TextUtils;
28import android.util.Slog;
29
30import java.util.Locale;
31import java.util.UUID;
32
33/**
34 * Helper to manage the database of the sound models that have been registered on the device.
35 *
36 * @hide
37 */
38public class DatabaseHelper extends SQLiteOpenHelper {
39    static final String TAG = "SoundModelDBHelper";
40    static final boolean DBG = false;
41
42    private static final String NAME = "sound_model.db";
43    private static final int VERSION = 4;
44
45    public static interface SoundModelContract {
46        public static final String TABLE = "sound_model";
47        public static final String KEY_MODEL_UUID = "model_uuid";
48        public static final String KEY_KEYPHRASE_ID = "keyphrase_id";
49        public static final String KEY_TYPE = "type";
50        public static final String KEY_DATA = "data";
51        public static final String KEY_RECOGNITION_MODES = "recognition_modes";
52        public static final String KEY_LOCALE = "locale";
53        public static final String KEY_HINT_TEXT = "hint_text";
54        public static final String KEY_USERS = "users";
55    }
56
57    // Table Create Statement
58    private static final String CREATE_TABLE_SOUND_MODEL = "CREATE TABLE "
59            + SoundModelContract.TABLE + "("
60            + SoundModelContract.KEY_MODEL_UUID + " TEXT PRIMARY KEY,"
61            + SoundModelContract.KEY_KEYPHRASE_ID + " INTEGER,"
62            + SoundModelContract.KEY_TYPE + " INTEGER,"
63            + SoundModelContract.KEY_DATA + " BLOB,"
64            + SoundModelContract.KEY_RECOGNITION_MODES + " INTEGER,"
65            + SoundModelContract.KEY_LOCALE + " TEXT,"
66            + SoundModelContract.KEY_HINT_TEXT + " TEXT,"
67            + SoundModelContract.KEY_USERS + " TEXT" + ")";
68
69    public DatabaseHelper(Context context) {
70        super(context, NAME, null, VERSION);
71    }
72
73    @Override
74    public void onCreate(SQLiteDatabase db) {
75        // creating required tables
76        db.execSQL(CREATE_TABLE_SOUND_MODEL);
77    }
78
79    @Override
80    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
81        // TODO: For now, drop older tables and recreate new ones.
82        db.execSQL("DROP TABLE IF EXISTS " + SoundModelContract.TABLE);
83        onCreate(db);
84    }
85
86    /**
87     * Updates the given keyphrase model, adds it, if it doesn't already exist.
88     *
89     * TODO: We only support one keyphrase currently.
90     */
91    public boolean updateKeyphraseSoundModel(KeyphraseSoundModel soundModel) {
92        synchronized(this) {
93            SQLiteDatabase db = getWritableDatabase();
94            ContentValues values = new ContentValues();
95            values.put(SoundModelContract.KEY_MODEL_UUID, soundModel.uuid.toString());
96            values.put(SoundModelContract.KEY_TYPE, SoundTrigger.SoundModel.TYPE_KEYPHRASE);
97            values.put(SoundModelContract.KEY_DATA, soundModel.data);
98
99            if (soundModel.keyphrases != null && soundModel.keyphrases.length == 1) {
100                values.put(SoundModelContract.KEY_KEYPHRASE_ID, soundModel.keyphrases[0].id);
101                values.put(SoundModelContract.KEY_RECOGNITION_MODES,
102                        soundModel.keyphrases[0].recognitionModes);
103                values.put(SoundModelContract.KEY_USERS,
104                        getCommaSeparatedString(soundModel.keyphrases[0].users));
105                values.put(SoundModelContract.KEY_LOCALE, soundModel.keyphrases[0].locale);
106                values.put(SoundModelContract.KEY_HINT_TEXT, soundModel.keyphrases[0].text);
107                try {
108                    return db.insertWithOnConflict(SoundModelContract.TABLE, null, values,
109                            SQLiteDatabase.CONFLICT_REPLACE) != -1;
110                } finally {
111                    db.close();
112                }
113            }
114            return false;
115        }
116    }
117
118    /**
119     * Deletes the sound model and associated keyphrases.
120     */
121    public boolean deleteKeyphraseSoundModel(int keyphraseId, int userHandle, String bcp47Locale) {
122        // Sanitize the locale to guard against SQL injection.
123        bcp47Locale = Locale.forLanguageTag(bcp47Locale).toLanguageTag();
124        synchronized(this) {
125            KeyphraseSoundModel soundModel = getKeyphraseSoundModel(keyphraseId, userHandle,
126                    bcp47Locale);
127            if (soundModel == null) {
128                return false;
129            }
130
131            // Delete all sound models for the given keyphrase and specified user.
132            SQLiteDatabase db = getWritableDatabase();
133            String soundModelClause = SoundModelContract.KEY_MODEL_UUID
134                    + "='" + soundModel.uuid.toString() + "'";
135            try {
136                return db.delete(SoundModelContract.TABLE, soundModelClause, null) != 0;
137            } finally {
138                db.close();
139            }
140        }
141    }
142
143    /**
144     * Returns a matching {@link KeyphraseSoundModel} for the keyphrase ID.
145     * Returns null if a match isn't found.
146     *
147     * TODO: We only support one keyphrase currently.
148     */
149    public KeyphraseSoundModel getKeyphraseSoundModel(int keyphraseId, int userHandle,
150            String bcp47Locale) {
151        // Sanitize the locale to guard against SQL injection.
152        bcp47Locale = Locale.forLanguageTag(bcp47Locale).toLanguageTag();
153        synchronized(this) {
154            // Find the corresponding sound model ID for the keyphrase.
155            String selectQuery = "SELECT  * FROM " + SoundModelContract.TABLE
156                    + " WHERE " + SoundModelContract.KEY_KEYPHRASE_ID + "= '" + keyphraseId
157                    + "' AND " + SoundModelContract.KEY_LOCALE + "='" + bcp47Locale + "'";
158            SQLiteDatabase db = getReadableDatabase();
159            Cursor c = db.rawQuery(selectQuery, null);
160
161            try {
162                if (c.moveToFirst()) {
163                    do {
164                        int type = c.getInt(c.getColumnIndex(SoundModelContract.KEY_TYPE));
165                        if (type != SoundTrigger.SoundModel.TYPE_KEYPHRASE) {
166                            if (DBG) {
167                                Slog.w(TAG, "Ignoring SoundModel since it's type is incorrect");
168                            }
169                            continue;
170                        }
171
172                        String modelUuid = c.getString(
173                                c.getColumnIndex(SoundModelContract.KEY_MODEL_UUID));
174                        if (modelUuid == null) {
175                            Slog.w(TAG, "Ignoring SoundModel since it doesn't specify an ID");
176                            continue;
177                        }
178
179                        byte[] data = c.getBlob(c.getColumnIndex(SoundModelContract.KEY_DATA));
180                        int recognitionModes = c.getInt(
181                                c.getColumnIndex(SoundModelContract.KEY_RECOGNITION_MODES));
182                        int[] users = getArrayForCommaSeparatedString(
183                                c.getString(c.getColumnIndex(SoundModelContract.KEY_USERS)));
184                        String modelLocale = c.getString(
185                                c.getColumnIndex(SoundModelContract.KEY_LOCALE));
186                        String text = c.getString(
187                                c.getColumnIndex(SoundModelContract.KEY_HINT_TEXT));
188
189                        // Only add keyphrases meant for the current user.
190                        if (users == null) {
191                            // No users present in the keyphrase.
192                            Slog.w(TAG, "Ignoring SoundModel since it doesn't specify users");
193                            continue;
194                        }
195
196                        boolean isAvailableForCurrentUser = false;
197                        for (int user : users) {
198                            if (userHandle == user) {
199                                isAvailableForCurrentUser = true;
200                                break;
201                            }
202                        }
203                        if (!isAvailableForCurrentUser) {
204                            if (DBG) {
205                                Slog.w(TAG, "Ignoring SoundModel since user handles don't match");
206                            }
207                            continue;
208                        } else {
209                            if (DBG) Slog.d(TAG, "Found a SoundModel for user: " + userHandle);
210                        }
211
212                        Keyphrase[] keyphrases = new Keyphrase[1];
213                        keyphrases[0] = new Keyphrase(
214                                keyphraseId, recognitionModes, modelLocale, text, users);
215                        KeyphraseSoundModel model = new KeyphraseSoundModel(
216                                UUID.fromString(modelUuid),
217                                null /* FIXME use vendor UUID */, data, keyphrases);
218                        if (DBG) {
219                            Slog.d(TAG, "Found SoundModel for the given keyphrase/locale/user: "
220                                    + model);
221                        }
222                        return model;
223                    } while (c.moveToNext());
224                }
225                Slog.w(TAG, "No SoundModel available for the given keyphrase");
226            } finally {
227                c.close();
228                db.close();
229            }
230            return null;
231        }
232    }
233
234    private static String getCommaSeparatedString(int[] users) {
235        if (users == null) {
236            return "";
237        }
238        StringBuilder sb = new StringBuilder();
239        for (int i = 0; i < users.length; i++) {
240            if (i != 0) {
241                sb.append(',');
242            }
243            sb.append(users[i]);
244        }
245        return sb.toString();
246    }
247
248    private static int[] getArrayForCommaSeparatedString(String text) {
249        if (TextUtils.isEmpty(text)) {
250            return null;
251        }
252        String[] usersStr = text.split(",");
253        int[] users = new int[usersStr.length];
254        for (int i = 0; i < usersStr.length; i++) {
255            users[i] = Integer.parseInt(usersStr[i]);
256        }
257        return users;
258    }
259}
260