diff options
author | Sandeep Siddhartha <sansid@google.com> | 2014-07-17 16:21:54 -0700 |
---|---|---|
committer | Sandeep Siddhartha <sansid@google.com> | 2014-07-20 11:22:55 -0700 |
commit | 055897208d659e9734a82def88be4a806ff55448 (patch) | |
tree | 4540186364f0a3fc3a3675119846448215696f68 /services | |
parent | 6eb262c3515c927df19340b3eee8c74bc9478d16 (diff) | |
download | frameworks_base-055897208d659e9734a82def88be4a806ff55448.zip frameworks_base-055897208d659e9734a82def88be4a806ff55448.tar.gz frameworks_base-055897208d659e9734a82def88be4a806ff55448.tar.bz2 |
Move sound trigger calls to VoiceInteractionManagerService
- This ensures that any data being loaded on the DSP comes from the framework
Change-Id: Ie15f0994850ba8f298ca07c49fe0b89e066d9e2b
Diffstat (limited to 'services')
3 files changed, 356 insertions, 17 deletions
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java index 27bec9f..eed2d44 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DatabaseHelper.java @@ -22,8 +22,9 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.hardware.soundtrigger.SoundTrigger; -import android.hardware.soundtrigger.Keyphrase; -import android.hardware.soundtrigger.KeyphraseSoundModel; +import android.hardware.soundtrigger.SoundTrigger.Keyphrase; +import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel; +import android.text.TextUtils; import android.util.Slog; import java.util.ArrayList; @@ -39,7 +40,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { static final String TAG = "SoundModelDBHelper"; private static final String NAME = "sound_model.db"; - private static final int VERSION = 1; + private static final int VERSION = 2; public static interface KeyphraseContract { public static final String TABLE = "keyphrase"; @@ -63,7 +64,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { + KeyphraseContract.TABLE + "(" + KeyphraseContract.KEY_ID + " INTEGER PRIMARY KEY," + KeyphraseContract.KEY_RECOGNITION_MODES + " INTEGER," - + KeyphraseContract.KEY_USERS + " INTEGER," + + KeyphraseContract.KEY_USERS + " TEXT," + KeyphraseContract.KEY_SOUND_MODEL_ID + " TEXT," + KeyphraseContract.KEY_LOCALE + " TEXT," + KeyphraseContract.KEY_HINT_TEXT + " TEXT" + ")"; @@ -119,10 +120,11 @@ public class DatabaseHelper extends SQLiteOpenHelper { private boolean addOrUpdateKeyphrase(SQLiteDatabase db, UUID modelId, Keyphrase keyphrase) { ContentValues values = new ContentValues(); values.put(KeyphraseContract.KEY_ID, keyphrase.id); - values.put(KeyphraseContract.KEY_RECOGNITION_MODES, keyphrase.recognitionModeFlags); + values.put(KeyphraseContract.KEY_RECOGNITION_MODES, keyphrase.recognitionModes); values.put(KeyphraseContract.KEY_SOUND_MODEL_ID, modelId.toString()); - values.put(KeyphraseContract.KEY_HINT_TEXT, keyphrase.hintText); + 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; @@ -193,11 +195,12 @@ public class DatabaseHelper extends SQLiteOpenHelper { do { int id = c.getInt(c.getColumnIndex(KeyphraseContract.KEY_ID)); int modes = c.getInt(c.getColumnIndex(KeyphraseContract.KEY_RECOGNITION_MODES)); - int[] users = {c.getInt(c.getColumnIndex(KeyphraseContract.KEY_USERS))}; + 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)); - keyphrases.add(new Keyphrase(id, hintText, locale, modes, users)); + keyphrases.add(new Keyphrase(id, modes, locale, hintText, users)); } while (c.moveToNext()); } Keyphrase[] keyphraseArr = new Keyphrase[keyphrases.size()]; @@ -205,4 +208,29 @@ public class DatabaseHelper extends SQLiteOpenHelper { c.close(); return keyphraseArr; } + + + private String getCommaSeparatedString(int[] users) { + if (users == null || users.length == 0) { + return ""; + } + String csv = ""; + for (int user : users) { + csv += String.valueOf(user); + csv += ","; + } + return csv.substring(0, csv.length() - 1); + } + + private int[] getArrayForCommaSeparatedString(String text) { + if (TextUtils.isEmpty(text)) { + return null; + } + String[] usersStr = text.split(","); + int[] users = new int[usersStr.length]; + for (int i = 0; i < usersStr.length; i++) { + users[i] = Integer.valueOf(usersStr[i]); + } + return users; + } } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerHelper.java new file mode 100644 index 0000000..6842f7d --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerHelper.java @@ -0,0 +1,246 @@ +/** + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.voiceinteraction; + +import android.hardware.soundtrigger.IRecognitionStatusCallback; +import android.hardware.soundtrigger.SoundTrigger; +import android.hardware.soundtrigger.SoundTrigger.Keyphrase; +import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel; +import android.hardware.soundtrigger.SoundTrigger.ModuleProperties; +import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig; +import android.hardware.soundtrigger.SoundTrigger.RecognitionEvent; +import android.hardware.soundtrigger.SoundTriggerModule; +import android.os.RemoteException; +import android.util.Slog; +import android.util.SparseArray; + +import java.util.ArrayList; + +/** + * Helper for {@link SoundTrigger} APIs. + * Currently this just acts as an abstraction over all SoundTrigger API calls. + * + * @hide + */ +public class SoundTriggerHelper implements SoundTrigger.StatusListener { + static final String TAG = "SoundTriggerHelper"; + // TODO: Remove this. + static final int TEMP_KEYPHRASE_ID = 1; + + /** + * Return codes for {@link #startRecognition(int, KeyphraseSoundModel, + * IRecognitionStatusCallback, RecognitionConfig)}, + * {@link #stopRecognition(int, IRecognitionStatusCallback)} + */ + public static final int STATUS_ERROR = SoundTrigger.STATUS_ERROR; + public static final int STATUS_OK = SoundTrigger.STATUS_OK; + + private static final int INVALID_SOUND_MODEL_HANDLE = -1; + + /** The {@link DspInfo} for the system, or null if none exists. */ + final ModuleProperties moduleProperties; + + /** The properties for the DSP module */ + private final SoundTriggerModule mModule; + + // Use a RemoteCallbackList here? + private final SparseArray<IRecognitionStatusCallback> mActiveListeners; + + private int mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE; + + SoundTriggerHelper() { + ArrayList <ModuleProperties> modules = new ArrayList<>(); + int status = SoundTrigger.listModules(modules); + mActiveListeners = new SparseArray<>(1); + if (status != SoundTrigger.STATUS_OK || modules.size() == 0) { + Slog.w(TAG, "listModules status=" + status + ", # of modules=" + modules.size()); + moduleProperties = null; + mModule = null; + } else { + // TODO: Figure out how to determine which module corresponds to the DSP hardware. + moduleProperties = modules.get(0); + mModule = SoundTrigger.attachModule(moduleProperties.id, this, null); + } + } + + /** + * @return True, if a recognition for the given {@link Keyphrase} is active. + */ + synchronized boolean isKeyphraseActive(Keyphrase keyphrase) { + if (keyphrase == null) { + Slog.w(TAG, "isKeyphraseActive requires a non-null keyphrase"); + return false; + } + return mActiveListeners.get(keyphrase.id) != null; + } + + /** + * Starts recognition for the given keyphraseId. + * + * @param keyphraseId The identifier of the keyphrase for which + * the recognition is to be started. + * @param soundModel The sound model to use for recognition. + * @param listener The listener for the recognition events related to the given keyphrase. + * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}. + */ + synchronized int startRecognition(int keyphraseId, + KeyphraseSoundModel soundModel, + IRecognitionStatusCallback listener, + RecognitionConfig recognitionConfig) { + if (moduleProperties == null || mModule == null) { + Slog.w(TAG, "Attempting startRecognition without the capability"); + return STATUS_ERROR; + } + + IRecognitionStatusCallback oldListener = mActiveListeners.get(keyphraseId); + if (oldListener != null && oldListener != listener) { + if (mCurrentSoundModelHandle != INVALID_SOUND_MODEL_HANDLE) { + Slog.w(TAG, "Canceling previous recognition"); + // TODO: Inspect the return codes here. + mModule.unloadSoundModel(mCurrentSoundModelHandle); + } + try { + mActiveListeners.get(keyphraseId).onDetectionStopped(); + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException in onDetectionStopped"); + } + mActiveListeners.remove(keyphraseId); + } + + int[] handle = new int[] { INVALID_SOUND_MODEL_HANDLE }; + int status = mModule.loadSoundModel(soundModel, handle); + if (status != SoundTrigger.STATUS_OK) { + Slog.w(TAG, "loadSoundModel call failed with " + status); + return STATUS_ERROR; + } + if (handle[0] == INVALID_SOUND_MODEL_HANDLE) { + Slog.w(TAG, "loadSoundModel call returned invalid sound model handle"); + return STATUS_ERROR; + } + + // Start the recognition. + status = mModule.startRecognition(handle[0], recognitionConfig); + if (status != SoundTrigger.STATUS_OK) { + Slog.w(TAG, "startRecognition failed with " + status); + return STATUS_ERROR; + } + + // Everything went well! + mCurrentSoundModelHandle = handle[0]; + // Register the new listener. This replaces the old one. + // There can only be a maximum of one active listener for a keyphrase + // at any given time. + mActiveListeners.put(keyphraseId, listener); + return STATUS_OK; + } + + /** + * Stops recognition for the given {@link Keyphrase} if a recognition is + * currently active. + * + * @param keyphraseId The identifier of the keyphrase for which + * the recognition is to be stopped. + * @param listener The listener for the recognition events related to the given keyphrase. + * + * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}. + */ + synchronized int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener) { + if (moduleProperties == null || mModule == null) { + Slog.w(TAG, "Attempting stopRecognition without the capability"); + return STATUS_ERROR; + } + + IRecognitionStatusCallback currentListener = mActiveListeners.get(keyphraseId); + if (currentListener == null) { + // startRecognition hasn't been called or it failed. + Slog.w(TAG, "Attempting stopRecognition without a successful startRecognition"); + return STATUS_ERROR; + } else if (currentListener != listener) { + // TODO: Figure out if this should match the listener that was passed in during + // startRecognition, or should we allow a different listener to stop the recognition, + // in which case we don't need to pass in a listener here. + Slog.w(TAG, "Attempting stopRecognition for another recognition"); + return STATUS_ERROR; + } else { + // Stop recognition if it's the current one, ignore otherwise. + // TODO: Inspect the return codes here. + int status = mModule.stopRecognition(mCurrentSoundModelHandle); + if (status != SoundTrigger.STATUS_OK) { + Slog.w(TAG, "stopRecognition call failed with " + status); + return STATUS_ERROR; + } + status = mModule.unloadSoundModel(mCurrentSoundModelHandle); + if (status != SoundTrigger.STATUS_OK) { + Slog.w(TAG, "unloadSoundModel call failed with " + status); + return STATUS_ERROR; + } + + mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE; + mActiveListeners.remove(keyphraseId); + return STATUS_OK; + } + } + + //---- SoundTrigger.StatusListener methods + @Override + public void onRecognition(RecognitionEvent event) { + // Check which keyphrase triggered, and fire the appropriate event. + // TODO: Get the keyphrase out of the event and fire events on it. + // For now, as a nasty workaround, we fire all events to the listener for + // keyphrase with TEMP_KEYPHRASE_ID. + IRecognitionStatusCallback listener = null; + synchronized(this) { + // TODO: The keyphrase should come from the recognition event + // as it may be for a different keyphrase than the current one. + listener = mActiveListeners.get(TEMP_KEYPHRASE_ID); + } + if (listener == null) { + Slog.w(TAG, "received onRecognition event without any listener for it"); + return; + } + + switch (event.status) { + case SoundTrigger.RECOGNITION_STATUS_SUCCESS: + // TODO: Pass the captured audio back. + try { + listener.onDetected(null); + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException in onDetected"); + } + break; + case SoundTrigger.RECOGNITION_STATUS_ABORT: + try { + listener.onDetectionStopped(); + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException in onDetectionStopped"); + } + break; + case SoundTrigger.RECOGNITION_STATUS_FAILURE: + try { + listener.onDetectionStopped(); + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException in onDetectionStopped"); + } + break; + } + } + + @Override + public void onServiceDied() { + // TODO: Figure out how to restart the recognition here. + } +} diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 63702ba..2ce4971 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -24,7 +24,10 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.database.ContentObserver; -import android.hardware.soundtrigger.KeyphraseSoundModel; +import android.hardware.soundtrigger.IRecognitionStatusCallback; +import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel; +import android.hardware.soundtrigger.SoundTrigger.ModuleProperties; +import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -56,19 +59,17 @@ public class VoiceInteractionManagerService extends SystemService { static final String TAG = "VoiceInteractionManagerService"; - // TODO: Add descriptive error codes. - public static final int STATUS_ERROR = -1; - public static final int STATUS_OK = 1; - final Context mContext; final ContentResolver mResolver; final DatabaseHelper mDbHelper; + final SoundTriggerHelper mSoundTriggerHelper; public VoiceInteractionManagerService(Context context) { super(context); mContext = context; mResolver = context.getContentResolver(); mDbHelper = new DatabaseHelper(context); + mSoundTriggerHelper = new SoundTriggerHelper(); } @Override @@ -239,6 +240,8 @@ public class VoiceInteractionManagerService extends SystemService { } } + //----------------- Model management APIs --------------------------------// + @Override public List<KeyphraseSoundModel> listRegisteredKeyphraseSoundModels( IVoiceInteractionService service) { @@ -290,15 +293,15 @@ public class VoiceInteractionManagerService extends SystemService { // If the keyphrases are not present in the model, delete the model. if (model.keyphrases == null) { if (mDbHelper.deleteKeyphraseSoundModel(model.uuid)) { - return STATUS_OK; + return SoundTriggerHelper.STATUS_OK; } else { - return STATUS_ERROR; + return SoundTriggerHelper.STATUS_ERROR; } } else { if (mDbHelper.addOrUpdateKeyphraseSoundModel(model)) { - return STATUS_OK; + return SoundTriggerHelper.STATUS_OK; } else { - return STATUS_ERROR; + return SoundTriggerHelper.STATUS_ERROR; } } } finally { @@ -307,6 +310,68 @@ public class VoiceInteractionManagerService extends SystemService { } } + //----------------- SoundTrigger APIs --------------------------------// + @Override + public ModuleProperties getDspModuleProperties(IVoiceInteractionService service) { + // Allow the call if this is the current voice interaction service. + synchronized (this) { + if (mImpl == null || mImpl.mService == null + || service == null || service.asBinder() != mImpl.mService.asBinder()) { + throw new SecurityException( + "Caller is not the current voice interaction service"); + } + + final long caller = Binder.clearCallingIdentity(); + try { + return mSoundTriggerHelper.moduleProperties; + } finally { + Binder.restoreCallingIdentity(caller); + } + } + } + + @Override + public int startRecognition(IVoiceInteractionService service, int keyphraseId, + KeyphraseSoundModel soundModel, IRecognitionStatusCallback callback, + RecognitionConfig recognitionConfig) { + // Allow the call if this is the current voice interaction service. + synchronized (this) { + if (mImpl == null || mImpl.mService == null + || service == null || service.asBinder() != mImpl.mService.asBinder()) { + throw new SecurityException( + "Caller is not the current voice interaction service"); + } + + final long caller = Binder.clearCallingIdentity(); + try { + return mSoundTriggerHelper.startRecognition( + keyphraseId, soundModel, callback, recognitionConfig); + } finally { + Binder.restoreCallingIdentity(caller); + } + } + } + + @Override + public int stopRecognition(IVoiceInteractionService service, int keyphraseId, + IRecognitionStatusCallback callback) { + // Allow the call if this is the current voice interaction service. + synchronized (this) { + if (mImpl == null || mImpl.mService == null + || service == null || service.asBinder() != mImpl.mService.asBinder()) { + 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); + } + } + } + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP) |