private final Handler mHandler;
/**
- * The sound model for the keyphrase, derived from the model management service
- * (IVoiceInteractionManagerService). May be null if the keyphrase isn't enrolled yet.
+ * Indicates if there is a sound model enrolled for the keyphrase,
+ * derived from the model management service (IVoiceInteractionManagerService).
*/
- private KeyphraseSoundModel mEnrolledSoundModel;
+ private boolean mIsEnrolledForDetection;
private int mAvailability = STATE_NOT_READY;
/**
int code = STATUS_ERROR;
try {
code = mModelManagementService.startRecognition(mVoiceInteractionService,
- mKeyphraseMetadata.id, mEnrolledSoundModel, mInternalCallback,
+ mKeyphraseMetadata.id, mInternalCallback,
new RecognitionConfig(
captureTriggerAudio, recognitionExtra, null /* additional data */));
} catch (RemoteException e) {
@Override
public Void doInBackground(Void... params) {
int availability = internalGetInitialAvailability();
- KeyphraseSoundModel soundModel = null;
+ boolean enrolled = false;
// Fetch the sound model if the availability is one of the supported ones.
if (availability == STATE_NOT_READY
|| availability == STATE_KEYPHRASE_UNENROLLED
|| availability == STATE_KEYPHRASE_ENROLLED) {
- soundModel =
- internalGetKeyphraseSoundModel(mKeyphraseMetadata.id);
- if (soundModel == null) {
+ enrolled = internalGetIsEnrolled(mKeyphraseMetadata.id);
+ if (!enrolled) {
availability = STATE_KEYPHRASE_UNENROLLED;
} else {
availability = STATE_KEYPHRASE_ENROLLED;
Slog.d(TAG, "Hotword availability changed from " + mAvailability
+ " -> " + availability);
}
+ mIsEnrolledForDetection = enrolled;
mAvailability = availability;
- mEnrolledSoundModel = soundModel;
notifyStateChangedLocked();
}
return null;
/**
* @return The corresponding {@link KeyphraseSoundModel} or null if none is found.
*/
- private KeyphraseSoundModel internalGetKeyphraseSoundModel(int keyphraseId) {
- List<KeyphraseSoundModel> soundModels;
+ private boolean internalGetIsEnrolled(int keyphraseId) {
try {
- soundModels = mModelManagementService
- .listRegisteredKeyphraseSoundModels(mVoiceInteractionService);
- if (soundModels == null || soundModels.isEmpty()) {
- Slog.i(TAG, "No available sound models for keyphrase ID: " + keyphraseId);
- return null;
- }
- for (int i = 0; i < soundModels.size(); i++) {
- KeyphraseSoundModel soundModel = soundModels.get(i);
- if (soundModel.keyphrases == null || soundModel.keyphrases.length == 0) {
- continue;
- }
- for (int j = 0; i < soundModel.keyphrases.length; j++) {
- Keyphrase keyphrase = soundModel.keyphrases[j];
- if (keyphrase.id == keyphraseId) {
- return soundModel;
- }
- }
- }
+ return mModelManagementService.isEnrolledForKeyphrase(
+ mVoiceInteractionService, keyphraseId);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in listRegisteredKeyphraseSoundModels!");
}
- return null;
+ return false;
}
}
}
import android.text.TextUtils;
import android.util.Slog;
-import java.util.ArrayList;
-import java.util.List;
import java.util.UUID;
/**
*/
public class DatabaseHelper extends SQLiteOpenHelper {
static final String TAG = "SoundModelDBHelper";
- static final boolean DBG = false;
+ // TODO: Set to false.
+ static final boolean DBG = true;
private static final String NAME = "sound_model.db";
- private static final int VERSION = 2;
-
- public static interface KeyphraseContract {
- public static final String TABLE = "keyphrase";
- public static final String KEY_ID = "_id";
- public static final String KEY_RECOGNITION_MODES = "modes";
- public static final String KEY_LOCALE = "locale";
- public static final String KEY_HINT_TEXT = "hint_text";
- public static final String KEY_USERS = "users";
- public static final String KEY_SOUND_MODEL_ID = "sound_model_id";
- }
+ private static final int VERSION = 3;
public static interface SoundModelContract {
public static final String TABLE = "sound_model";
- public static final String KEY_ID = "_id";
+ public static final String KEY_KEYPHRASE_ID = "keyphrase_id";
+ public static final String KEY_MODEL_UUID = "model_uuid";
public static final String KEY_TYPE = "type";
public static final String KEY_DATA = "data";
+ public static final String KEY_RECOGNITION_MODES = "recognition_modes";
+ public static final String KEY_LOCALE = "locale";
+ public static final String KEY_HINT_TEXT = "hint_text";
+ public static final String KEY_USERS = "users";
}
- // Table Create Statements
- private static final String CREATE_TABLE_KEYPRHASES = "CREATE TABLE "
- + KeyphraseContract.TABLE + "("
- + KeyphraseContract.KEY_ID + " INTEGER PRIMARY KEY,"
- + KeyphraseContract.KEY_RECOGNITION_MODES + " INTEGER,"
- + KeyphraseContract.KEY_USERS + " TEXT,"
- + KeyphraseContract.KEY_SOUND_MODEL_ID + " TEXT,"
- + KeyphraseContract.KEY_LOCALE + " TEXT,"
- + KeyphraseContract.KEY_HINT_TEXT + " TEXT" + ")";
-
+ // Table Create Statement
private static final String CREATE_TABLE_SOUND_MODEL = "CREATE TABLE "
+ SoundModelContract.TABLE + "("
- + SoundModelContract.KEY_ID + " TEXT PRIMARY KEY,"
+ + SoundModelContract.KEY_KEYPHRASE_ID + " INTEGER PRIMARY KEY,"
+ + SoundModelContract.KEY_MODEL_UUID + " TEXT,"
+ SoundModelContract.KEY_TYPE + " INTEGER,"
- + SoundModelContract.KEY_DATA + " BLOB" + ")";
+ + SoundModelContract.KEY_DATA + " BLOB,"
+ + SoundModelContract.KEY_RECOGNITION_MODES + " INTEGER,"
+ + SoundModelContract.KEY_LOCALE + " TEXT,"
+ + SoundModelContract.KEY_HINT_TEXT + " TEXT,"
+ + SoundModelContract.KEY_USERS + " TEXT" + ")";
private final UserManager mUserManager;
@Override
public void onCreate(SQLiteDatabase db) {
// creating required tables
- db.execSQL(CREATE_TABLE_KEYPRHASES);
db.execSQL(CREATE_TABLE_SOUND_MODEL);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO: For now, drop older tables and recreate new ones.
- db.execSQL("DROP TABLE IF EXISTS " + KeyphraseContract.TABLE);
db.execSQL("DROP TABLE IF EXISTS " + SoundModelContract.TABLE);
onCreate(db);
}
- public boolean addOrUpdateKeyphraseSoundModel(KeyphraseSoundModel soundModel) {
+ /**
+ * Updates the given keyphrase model, adds it, if it doesn't already exist.
+ *
+ * TODO: We only support one keyphrase currently.
+ */
+ public boolean updateKeyphraseSoundModel(KeyphraseSoundModel soundModel) {
synchronized(this) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
- // Generate a random ID for the model.
- values.put(SoundModelContract.KEY_ID, soundModel.uuid.toString());
- values.put(SoundModelContract.KEY_DATA, soundModel.data);
+ values.put(SoundModelContract.KEY_MODEL_UUID, soundModel.uuid.toString());
values.put(SoundModelContract.KEY_TYPE, SoundTrigger.SoundModel.TYPE_KEYPHRASE);
-
- boolean status = true;
- if (db.insertWithOnConflict(SoundModelContract.TABLE, null, values,
- SQLiteDatabase.CONFLICT_REPLACE) != -1) {
- for (Keyphrase keyphrase : soundModel.keyphrases) {
- status &= addOrUpdateKeyphraseLocked(db, soundModel.uuid, keyphrase);
+ values.put(SoundModelContract.KEY_DATA, soundModel.data);
+
+ if (soundModel.keyphrases != null && soundModel.keyphrases.length == 1) {
+ values.put(SoundModelContract.KEY_KEYPHRASE_ID, soundModel.keyphrases[0].id);
+ values.put(SoundModelContract.KEY_RECOGNITION_MODES,
+ soundModel.keyphrases[0].recognitionModes);
+ values.put(SoundModelContract.KEY_USERS,
+ getCommaSeparatedString(soundModel.keyphrases[0].users));
+ values.put(SoundModelContract.KEY_LOCALE, soundModel.keyphrases[0].locale);
+ values.put(SoundModelContract.KEY_HINT_TEXT, soundModel.keyphrases[0].text);
+ try {
+ return db.insertWithOnConflict(SoundModelContract.TABLE, null, values,
+ SQLiteDatabase.CONFLICT_REPLACE) != -1;
+ } finally {
+ db.close();
}
- db.close();
- return status;
- } else {
- Slog.w(TAG, "Failed to persist sound model to database");
- db.close();
- return false;
}
- }
- }
-
- private boolean addOrUpdateKeyphraseLocked(
- SQLiteDatabase db, UUID modelId, Keyphrase keyphrase) {
- ContentValues values = new ContentValues();
- values.put(KeyphraseContract.KEY_ID, keyphrase.id);
- values.put(KeyphraseContract.KEY_RECOGNITION_MODES, keyphrase.recognitionModes);
- values.put(KeyphraseContract.KEY_SOUND_MODEL_ID, modelId.toString());
- values.put(KeyphraseContract.KEY_HINT_TEXT, keyphrase.text);
- values.put(KeyphraseContract.KEY_LOCALE, keyphrase.locale);
- values.put(KeyphraseContract.KEY_USERS, getCommaSeparatedString(keyphrase.users));
- if (db.insertWithOnConflict(
- KeyphraseContract.TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE) != -1) {
- return true;
- } else {
- Slog.w(TAG, "Failed to persist keyphrase to database");
return false;
}
}
/**
* Deletes the sound model and associated keyphrases.
*/
- public boolean deleteKeyphraseSoundModel(UUID uuid) {
+ public boolean deleteKeyphraseSoundModel(int keyphraseId) {
synchronized(this) {
SQLiteDatabase db = getWritableDatabase();
- String modelId = uuid.toString();
- String soundModelClause = SoundModelContract.KEY_ID + "=" + modelId;
- boolean status = true;
- if (db.delete(SoundModelContract.TABLE, soundModelClause, null) == 0) {
- Slog.w(TAG, "No sound models deleted from the database");
- status = false;
- }
- String keyphraseClause = KeyphraseContract.KEY_SOUND_MODEL_ID + "=" + modelId;
- if (db.delete(KeyphraseContract.TABLE, keyphraseClause, null) == 0) {
- Slog.w(TAG, "No keyphrases deleted from the database");
- status = false;
+ String soundModelClause = SoundModelContract.KEY_KEYPHRASE_ID + "=" + keyphraseId;
+
+ try {
+ return db.delete(SoundModelContract.TABLE, soundModelClause, null) != 0;
+ } finally {
+ db.close();
}
- db.close();
- return status;
}
}
/**
- * Lists all the keyphrase sound models currently registered with the system.
+ * Returns a matching {@link KeyphraseSoundModel} for the keyphrase ID.
+ * Returns null if a match isn't found.
+ *
+ * TODO: We only support one keyphrase currently.
*/
- public List<KeyphraseSoundModel> getKephraseSoundModels() {
+ public KeyphraseSoundModel getKeyphraseSoundModel(int keyphraseId) {
synchronized(this) {
- List<KeyphraseSoundModel> models = new ArrayList<>();
- String selectQuery = "SELECT * FROM " + SoundModelContract.TABLE;
+ // Find the corresponding sound model ID for the keyphrase.
+ String selectQuery = "SELECT * FROM " + SoundModelContract.TABLE
+ + " WHERE " + SoundModelContract.KEY_KEYPHRASE_ID + " = '" + keyphraseId + "'";
SQLiteDatabase db = getReadableDatabase();
Cursor c = db.rawQuery(selectQuery, null);
-
- // looping through all rows and adding to list
- if (c.moveToFirst()) {
- do {
+
+ try {
+ if (c.moveToFirst()) {
int type = c.getInt(c.getColumnIndex(SoundModelContract.KEY_TYPE));
if (type != SoundTrigger.SoundModel.TYPE_KEYPHRASE) {
- // Ignore non-keyphrase sound models.
- continue;
+ Slog.w(TAG, "No KeyphraseSoundModel available for the given keyphrase");
+ return null;
}
- String id = c.getString(c.getColumnIndex(SoundModelContract.KEY_ID));
- byte[] data = c.getBlob(c.getColumnIndex(SoundModelContract.KEY_DATA));
- // Get all the keyphrases for this this sound model.
- // Validate the sound model.
- if (id == null) {
+
+ String modelUuid = c.getString(
+ c.getColumnIndex(SoundModelContract.KEY_MODEL_UUID));
+ if (modelUuid == null) {
Slog.w(TAG, "Ignoring sound model since it doesn't specify an ID");
- continue;
- }
- KeyphraseSoundModel model = new KeyphraseSoundModel(
- UUID.fromString(id), data, getKeyphrasesForSoundModelLocked(db, id));
- if (DBG) {
- Slog.d(TAG, "Adding model: " + model);
+ return null;
}
- models.add(model);
- } while (c.moveToNext());
- }
- c.close();
- db.close();
- return models;
- }
- }
-
- private Keyphrase[] getKeyphrasesForSoundModelLocked(SQLiteDatabase db, String modelId) {
- List<Keyphrase> keyphrases = new ArrayList<>();
- String selectQuery = "SELECT * FROM " + KeyphraseContract.TABLE
- + " WHERE " + KeyphraseContract.KEY_SOUND_MODEL_ID + " = '" + modelId + "'";
- Cursor c = db.rawQuery(selectQuery, null);
- // looping through all rows and adding to list
- if (c.moveToFirst()) {
- do {
- int id = c.getInt(c.getColumnIndex(KeyphraseContract.KEY_ID));
- int modes = c.getInt(c.getColumnIndex(KeyphraseContract.KEY_RECOGNITION_MODES));
- int[] users = getArrayForCommaSeparatedString(
- c.getString(c.getColumnIndex(KeyphraseContract.KEY_USERS)));
- String locale = c.getString(c.getColumnIndex(KeyphraseContract.KEY_LOCALE));
- String hintText = c.getString(c.getColumnIndex(KeyphraseContract.KEY_HINT_TEXT));
-
- // Only add keyphrases meant for the current user.
- if (users == null) {
- // No users present in the keyphrase.
- Slog.w(TAG, "Ignoring keyphrase since it doesn't specify users");
- continue;
- }
- boolean isAvailableForCurrentUser = false;
- int currentUser = mUserManager.getUserHandle();
- for (int user : users) {
- if (currentUser == user) {
- isAvailableForCurrentUser = true;
- break;
+ byte[] data = c.getBlob(c.getColumnIndex(SoundModelContract.KEY_DATA));
+ int recognitionModes = c.getInt(
+ c.getColumnIndex(SoundModelContract.KEY_RECOGNITION_MODES));
+ int[] users = getArrayForCommaSeparatedString(
+ c.getString(c.getColumnIndex(SoundModelContract.KEY_USERS)));
+ String locale = c.getString(c.getColumnIndex(SoundModelContract.KEY_LOCALE));
+ String text = c.getString(
+ c.getColumnIndex(SoundModelContract.KEY_HINT_TEXT));
+
+ // Only add keyphrases meant for the current user.
+ if (users == null) {
+ // No users present in the keyphrase.
+ Slog.w(TAG, "Ignoring keyphrase since it doesn't specify users");
+ return null;
+ }
+ boolean isAvailableForCurrentUser = false;
+ int currentUser = mUserManager.getUserHandle();
+ for (int user : users) {
+ if (currentUser == user) {
+ isAvailableForCurrentUser = true;
+ break;
+ }
+ }
+ if (!isAvailableForCurrentUser) {
+ Slog.w(TAG, "Ignoring keyphrase since it's not for the current user");
+ return null;
}
- }
- if (!isAvailableForCurrentUser) {
- Slog.w(TAG, "Ignoring keyphrase since it's not for the current user");
- continue;
- }
- keyphrases.add(new Keyphrase(id, modes, locale, hintText, users));
- } while (c.moveToNext());
+ Keyphrase[] keyphrases = new Keyphrase[1];
+ keyphrases[0] = new Keyphrase(
+ keyphraseId, recognitionModes, locale, text, users);
+ return new KeyphraseSoundModel(UUID.fromString(modelUuid), data, keyphrases);
+ }
+ Slog.w(TAG, "No SoundModel available for the given keyphrase");
+ } finally {
+ c.close();
+ db.close();
+ }
+ return null;
}
- Keyphrase[] keyphraseArr = new Keyphrase[keyphrases.size()];
- keyphrases.toArray(keyphraseArr);
- c.close();
- return keyphraseArr;
}
-
private static String getCommaSeparatedString(int[] users) {
if (users == null || users.length == 0) {
return "";
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.List;
+import java.util.UUID;
/**
//----------------- Model management APIs --------------------------------//
@Override
- public List<KeyphraseSoundModel> listRegisteredKeyphraseSoundModels(
- IVoiceInteractionService service) {
- // Allow the call if this is the current voice interaction service
- // or the caller holds the MANAGE_VOICE_KEYPHRASES permission.
+ public KeyphraseSoundModel getKeyphraseSoundModel(int keyphraseId) {
synchronized (this) {
- boolean permissionGranted =
- mContext.checkCallingPermission(Manifest.permission.MANAGE_VOICE_KEYPHRASES)
- == PackageManager.PERMISSION_GRANTED;
- boolean currentVoiceInteractionService = service != null
- && mImpl != null
- && mImpl.mService != null
- && service.asBinder() == mImpl.mService.asBinder();
-
- if (!permissionGranted && !currentVoiceInteractionService) {
- if (!currentVoiceInteractionService) {
- throw new SecurityException(
- "Caller is not the current voice interaction service");
- }
- if (!permissionGranted) {
- throw new SecurityException("Caller does not hold the permission "
- + Manifest.permission.MANAGE_VOICE_KEYPHRASES);
- }
+ if (mContext.checkCallingPermission(Manifest.permission.MANAGE_VOICE_KEYPHRASES)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Caller does not hold the permission "
+ + Manifest.permission.MANAGE_VOICE_KEYPHRASES);
}
}
final long caller = Binder.clearCallingIdentity();
try {
- return mDbHelper.getKephraseSoundModels();
+ return mDbHelper.getKeyphraseSoundModel(keyphraseId);
} finally {
Binder.restoreCallingIdentity(caller);
}
final long caller = Binder.clearCallingIdentity();
try {
- boolean success = false;
- if (model.keyphrases == null) {
- // If the keyphrases are not present in the model, delete the model.
- success = mDbHelper.deleteKeyphraseSoundModel(model.uuid);
+ if (mDbHelper.updateKeyphraseSoundModel(model)) {
+ synchronized (this) {
+ // Notify the voice interaction service of a change in sound models.
+ if (mImpl != null && mImpl.mService != null) {
+ mImpl.notifySoundModelsChangedLocked();
+ }
+ }
+ return SoundTriggerHelper.STATUS_OK;
} else {
- // Else update the model.
- success = mDbHelper.addOrUpdateKeyphraseSoundModel(model);
+ return SoundTriggerHelper.STATUS_ERROR;
}
- if (success) {
+ } finally {
+ Binder.restoreCallingIdentity(caller);
+ }
+ }
+
+ @Override
+ public int deleteKeyphraseSoundModel(int keyphraseId) {
+ synchronized (this) {
+ if (mContext.checkCallingPermission(Manifest.permission.MANAGE_VOICE_KEYPHRASES)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Caller does not hold the permission "
+ + Manifest.permission.MANAGE_VOICE_KEYPHRASES);
+ }
+ }
+
+ final long caller = Binder.clearCallingIdentity();
+ try {
+ if (mDbHelper.deleteKeyphraseSoundModel(keyphraseId)) {
synchronized (this) {
// Notify the voice interaction service of a change in sound models.
if (mImpl != null && mImpl.mService != null) {
//----------------- SoundTrigger APIs --------------------------------//
@Override
+ public boolean isEnrolledForKeyphrase(IVoiceInteractionService service, int keyphraseId) {
+ synchronized (this) {
+ if (mImpl == null || mImpl.mService == null
+ || service.asBinder() != mImpl.mService.asBinder()) {
+ throw new SecurityException(
+ "Caller is not the current voice interaction service");
+ }
+ }
+
+ final long caller = Binder.clearCallingIdentity();
+ try {
+ KeyphraseSoundModel model = mDbHelper.getKeyphraseSoundModel(keyphraseId);
+ return model != null;
+ } finally {
+ Binder.restoreCallingIdentity(caller);
+ }
+ }
+
+ @Override
public ModuleProperties getDspModuleProperties(IVoiceInteractionService service) {
// Allow the call if this is the current voice interaction service.
synchronized (this) {
@Override
public int startRecognition(IVoiceInteractionService service, int keyphraseId,
- KeyphraseSoundModel soundModel, IRecognitionStatusCallback callback,
- RecognitionConfig recognitionConfig) {
+ IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig) {
// Allow the call if this is the current voice interaction service.
synchronized (this) {
if (mImpl == null || mImpl.mService == null
"Caller is not the current voice interaction service");
}
- final long caller = Binder.clearCallingIdentity();
- try {
+ if (callback == null || recognitionConfig == null) {
+ throw new IllegalArgumentException("Illegal argument(s) in startRecognition");
+ }
+ }
+
+ final long caller = Binder.clearCallingIdentity();
+ try {
+ KeyphraseSoundModel soundModel = mDbHelper.getKeyphraseSoundModel(keyphraseId);
+ if (soundModel == null
+ || soundModel.uuid == null
+ || soundModel.keyphrases == null) {
+ Slog.w(TAG, "No matching sound model found in startRecognition");
+ return SoundTriggerHelper.STATUS_ERROR;
+ } else {
return mSoundTriggerHelper.startRecognition(
keyphraseId, soundModel, callback, recognitionConfig);
- } finally {
- Binder.restoreCallingIdentity(caller);
}
+ } finally {
+ Binder.restoreCallingIdentity(caller);
}
}
throw new SecurityException(
"Caller is not the current voice interaction service");
}
+ }
- final long caller = Binder.clearCallingIdentity();
- try {
- return mSoundTriggerHelper.stopRecognition(keyphraseId, callback);
- } finally {
- Binder.restoreCallingIdentity(caller);
- }
+ final long caller = Binder.clearCallingIdentity();
+ try {
+ return mSoundTriggerHelper.stopRecognition(keyphraseId, callback);
+ } finally {
+ Binder.restoreCallingIdentity(caller);
}
}