From a772e5fc062c8de48cb9c1d61755110f6b2e189b Mon Sep 17 00:00:00 2001 From: Arunesh Mishra Date: Mon, 25 Jan 2016 10:33:11 -0800 Subject: [PATCH] SoundTrigger API improvements. This CL implements the SoundTrigger API improvements as given in b/22860713. Only the java-level parts are implemented in this CL. Key changes include: * Addition of a SoundTriggerManager/SoundTriggerDetector system API to manage the sound-trigger based sound models. * Addition of a SoundTriggerService service that manages all sound models including voice (keyphrase) and sound-trigger based models. * Includes logic to write sound-trigger based models to the database. * VoiceInteractionManager service now uses SoundTriggerService instead of SoundTriggerHelper. Bug: 22860713 Change-Id: I7b5c0ed80702527c4460372efeb5e542d3693a69 --- Android.mk | 1 + .../android/app/SystemServiceRegistry.java | 12 ++ core/java/android/content/Context.java | 11 + .../IRecognitionStatusCallback.aidl | 9 +- .../hardware/soundtrigger/SoundTrigger.aidl | 3 + .../hardware/soundtrigger/SoundTrigger.java | 80 +++++++- .../voice/AlwaysOnHotwordDetector.java | 7 +- .../internal/app/ISoundTriggerService.aidl | 42 ++++ core/res/AndroidManifest.xml | 6 + .../soundtrigger/SoundTriggerDetector.java | 180 ++++++++++++++++ .../soundtrigger/SoundTriggerManager.java | 172 ++++++++++++++++ .../java/com/android/server/SystemServer.java | 3 + .../soundtrigger/SoundTriggerDbHelper.java | 144 +++++++++++++ .../SoundTriggerHelper.java | 2 +- .../soundtrigger/SoundTriggerInternal.java | 84 ++++++++ .../soundtrigger/SoundTriggerService.java | 194 ++++++++++++++++++ .../VoiceInteractionManagerService.java | 28 +-- tests/SoundTriggerTestApp/Android.mk | 12 ++ tests/SoundTriggerTestApp/AndroidManifest.xml | 17 ++ tests/SoundTriggerTestApp/res/layout/main.xml | 43 ++++ .../res/values/strings.xml | 22 ++ .../test/soundtrigger/SoundTriggerUtil.java | 115 +++++++++++ .../TestSoundTriggerActivity.java | 121 +++++++++++ 23 files changed, 1289 insertions(+), 19 deletions(-) create mode 100644 core/java/com/android/internal/app/ISoundTriggerService.aidl create mode 100644 media/java/android/media/soundtrigger/SoundTriggerDetector.java create mode 100644 media/java/android/media/soundtrigger/SoundTriggerManager.java create mode 100644 services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerDbHelper.java rename services/voiceinteraction/java/com/android/server/{voiceinteraction => soundtrigger}/SoundTriggerHelper.java (99%) create mode 100644 services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java create mode 100644 services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java create mode 100644 tests/SoundTriggerTestApp/Android.mk create mode 100644 tests/SoundTriggerTestApp/AndroidManifest.xml create mode 100644 tests/SoundTriggerTestApp/res/layout/main.xml create mode 100644 tests/SoundTriggerTestApp/res/values/strings.xml create mode 100644 tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java create mode 100644 tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java diff --git a/Android.mk b/Android.mk index 1d797c49b3e7d..8daa1aa9c114b 100644 --- a/Android.mk +++ b/Android.mk @@ -288,6 +288,7 @@ LOCAL_SRC_FILES += \ core/java/com/android/internal/app/IBatteryStats.aidl \ core/java/com/android/internal/app/IEphemeralResolver.aidl \ core/java/com/android/internal/app/IProcessStats.aidl \ + core/java/com/android/internal/app/ISoundTriggerService.aidl \ core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl \ core/java/com/android/internal/app/IVoiceInteractionSessionShowCallback.aidl \ core/java/com/android/internal/app/IVoiceInteractor.aidl \ diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index fc1a355632e6c..5eed781d76c36 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -17,6 +17,7 @@ package android.app; import com.android.internal.app.IAppOpsService; +import com.android.internal.app.ISoundTriggerService; import com.android.internal.appwidget.IAppWidgetService; import com.android.internal.os.IDropBoxManagerService; @@ -61,6 +62,7 @@ import android.media.midi.IMidiManager; import android.media.midi.MidiManager; import android.media.projection.MediaProjectionManager; import android.media.session.MediaSessionManager; +import android.media.soundtrigger.SoundTriggerManager; import android.media.tv.ITvInputManager; import android.media.tv.TvInputManager; import android.net.ConnectivityManager; @@ -708,12 +710,22 @@ final class SystemServiceRegistry { public RadioManager createService(ContextImpl ctx) { return new RadioManager(ctx); }}); + registerService(Context.HARDWARE_PROPERTIES_SERVICE, HardwarePropertiesManager.class, new CachedServiceFetcher() { @Override public HardwarePropertiesManager createService(ContextImpl ctx) { return new HardwarePropertiesManager(); }}); + + registerService(Context.SOUND_TRIGGER_SERVICE, SoundTriggerManager.class, + new CachedServiceFetcher() { + @Override + public SoundTriggerManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.SOUND_TRIGGER_SERVICE); + Log.i(TAG, "Creating new instance of SoundTriggerManager object."); + return new SoundTriggerManager(ctx, ISoundTriggerService.Stub.asInterface(b)); + }}); } /** diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 27cdd5054653b..3142b40ca9bd8 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -2613,6 +2613,7 @@ public abstract class Context { MIDI_SERVICE, RADIO_SERVICE, HARDWARE_PROPERTIES_SERVICE, + //@hide: SOUND_TRIGGER_SERVICE, }) @Retention(RetentionPolicy.SOURCE) public @interface ServiceName {} @@ -3194,6 +3195,16 @@ public abstract class Context { */ public static final String VOICE_INTERACTION_MANAGER_SERVICE = "voiceinteraction"; + /** + * Use with {@link #getSystemService} to access the + * {@link com.android.server.voiceinteraction.SoundTriggerService}. + * + * @hide + * @see #getSystemService + */ + public static final String SOUND_TRIGGER_SERVICE = "soundtrigger"; + + /** * Use with {@link #getSystemService} to retrieve an * {@link android.app.backup.IBackupManager IBackupManager} for communicating diff --git a/core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl b/core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl index 2f6dbe7d3794d..597efa566d2c2 100644 --- a/core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl +++ b/core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl @@ -25,9 +25,12 @@ oneway interface IRecognitionStatusCallback { /** * Called when the keyphrase is spoken. * - * @param data Optional trigger audio data, if it was requested and is available. + * @param recognitionEvent Object containing data relating to the + * recognition event such as trigger audio data, if it was requested + * and is available. */ - void onDetected(in SoundTrigger.KeyphraseRecognitionEvent recognitionEvent); + void onDetected(in SoundTrigger.RecognitionEvent recognitionEvent); + /** * Called when the detection fails due to an error. * @@ -42,4 +45,4 @@ oneway interface IRecognitionStatusCallback { * Called when the recognition is resumed after it was temporarily paused. */ void onRecognitionResumed(); -} \ No newline at end of file +} diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.aidl b/core/java/android/hardware/soundtrigger/SoundTrigger.aidl index e16ea71d9b655..fec64ea338ea7 100644 --- a/core/java/android/hardware/soundtrigger/SoundTrigger.aidl +++ b/core/java/android/hardware/soundtrigger/SoundTrigger.aidl @@ -18,8 +18,11 @@ package android.hardware.soundtrigger; parcelable SoundTrigger.ConfidenceLevel; parcelable SoundTrigger.Keyphrase; +parcelable SoundTrigger.RecognitionEvent; parcelable SoundTrigger.KeyphraseRecognitionEvent; +parcelable SoundTrigger.GenericSoundRecognitionEvent; parcelable SoundTrigger.KeyphraseRecognitionExtra; parcelable SoundTrigger.KeyphraseSoundModel; +parcelable SoundTrigger.GenericSoundModel; parcelable SoundTrigger.ModuleProperties; parcelable SoundTrigger.RecognitionConfig; diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java index d49040987a2ee..882908a9f46e7 100644 --- a/core/java/android/hardware/soundtrigger/SoundTrigger.java +++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java @@ -195,6 +195,12 @@ public class SoundTrigger { /** Keyphrase sound model */ public static final int TYPE_KEYPHRASE = 0; + /** + * A generic sound model. Use this type only for non-keyphrase sound models such as + * ones that match a particular sound pattern. + */ + public static final int TYPE_GENERIC_SOUND = 1; + /** Unique sound model identifier */ public final UUID uuid; @@ -458,6 +464,63 @@ public class SoundTrigger { } } + + /***************************************************************************** + * A GenericSoundModel is a specialized {@link SoundModel} for non-voice sound + * patterns. + ****************************************************************************/ + public static class GenericSoundModel extends SoundModel implements Parcelable { + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public GenericSoundModel createFromParcel(Parcel in) { + return GenericSoundModel.fromParcel(in); + } + + public GenericSoundModel[] newArray(int size) { + return new GenericSoundModel[size]; + } + }; + + public GenericSoundModel(UUID uuid, UUID vendorUuid, byte[] data) { + super(uuid, vendorUuid, TYPE_GENERIC_SOUND, data); + } + + @Override + public int describeContents() { + return 0; + } + + private static GenericSoundModel fromParcel(Parcel in) { + UUID uuid = UUID.fromString(in.readString()); + UUID vendorUuid = null; + int length = in.readInt(); + if (length >= 0) { + vendorUuid = UUID.fromString(in.readString()); + } + byte[] data = in.readBlob(); + return new GenericSoundModel(uuid, vendorUuid, data); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(uuid.toString()); + if (vendorUuid == null) { + dest.writeInt(-1); + } else { + dest.writeInt(vendorUuid.toString().length()); + dest.writeString(vendorUuid.toString()); + } + dest.writeBlob(data); + } + + @Override + public String toString() { + return "GenericSoundModel [uuid=" + uuid + ", vendorUuid=" + vendorUuid + + ", type=" + type + ", data=" + (data == null ? 0 : data.length) + "]"; + } + } + /** * Modes for key phrase recognition */ @@ -1018,6 +1081,21 @@ public class SoundTrigger { } } + /** + * Sub-class of RecognitionEvent specifically for sound-trigger based sound + * models(non-keyphrase). Currently does not contain any additional fields. + */ + public static class GenericRecognitionEvent extends RecognitionEvent { + public GenericRecognitionEvent(int status, int soundModelHandle, + boolean captureAvailable, int captureSession, int captureDelayMs, + int capturePreambleMs, boolean triggerInData, AudioFormat captureFormat, + byte[] data) { + super(status, soundModelHandle, captureAvailable, captureSession, + captureDelayMs, capturePreambleMs, triggerInData, captureFormat, + data); + } + } + /** * Status codes for {@link SoundModelEvent} */ @@ -1118,7 +1196,7 @@ public class SoundTrigger { public static final int SERVICE_STATE_DISABLED = 1; /** - * Returns a list of descriptors for all harware modules loaded. + * Returns a list of descriptors for all hardware modules loaded. * @param modules A ModuleProperties array where the list will be returned. * @return - {@link #STATUS_OK} in case of success * - {@link #STATUS_ERROR} in case of unspecified error diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java index ac7d539bb628f..76a401d745598 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -31,6 +31,7 @@ import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra; 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.media.AudioFormat; import android.os.AsyncTask; import android.os.Handler; @@ -616,7 +617,11 @@ public class AlwaysOnHotwordDetector { } @Override - public void onDetected(KeyphraseRecognitionEvent event) { + public void onDetected(RecognitionEvent event) { + if (! (event instanceof KeyphraseRecognitionEvent)) { + Slog.e(TAG, "onDetected() called for a soundtrigger event."); + return; + } if (DBG) { Slog.d(TAG, "onDetected(" + event + ")"); } else { diff --git a/core/java/com/android/internal/app/ISoundTriggerService.aidl b/core/java/com/android/internal/app/ISoundTriggerService.aidl new file mode 100644 index 0000000000000..9de4a6c62a49b --- /dev/null +++ b/core/java/com/android/internal/app/ISoundTriggerService.aidl @@ -0,0 +1,42 @@ +/* + * 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.internal.app; + +import android.hardware.soundtrigger.IRecognitionStatusCallback; +import android.hardware.soundtrigger.SoundTrigger; +import android.os.ParcelUuid; + +/** + * Service interface for a generic sound recognition model. + * @hide + */ +interface ISoundTriggerService { + + + SoundTrigger.GenericSoundModel getSoundModel(in ParcelUuid soundModelId); + + void updateSoundModel(in SoundTrigger.GenericSoundModel soundModel); + + void deleteSoundModel(in ParcelUuid soundModelId); + + void startRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback); + + /** + * Stops recognition. + */ + void stopRecognition(in ParcelUuid soundModelId, in IRecognitionStatusCallback callback); +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 56c3fc88cf23e..5a2f6015494bf 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2860,6 +2860,12 @@ + + + mReceiverInstanceMap; + + /** + * @hide + */ + public SoundTriggerManager(Context context, ISoundTriggerService soundTriggerService ) { + if (DBG) { + Slog.i(TAG, "SoundTriggerManager created."); + } + mSoundTriggerService = soundTriggerService; + mContext = context; + mReceiverInstanceMap = new HashMap(); + } + + /** + * Updates the given sound trigger model. + */ + public void updateModel(Model model) { + try { + mSoundTriggerService.updateSoundModel(model.getGenericSoundModel()); + } catch (RemoteException e) { + } + } + + /** + * Returns the sound trigger model represented by the given UUID. An instance of {@link Model} + * is returned. + */ + public Model getModel(UUID soundModelId) { + try { + return new Model(mSoundTriggerService.getSoundModel( + new ParcelUuid(soundModelId))); + } catch (RemoteException e) { + return null; + } + } + + /** + * Deletes the sound model represented by the provided UUID. + */ + public void deleteModel(UUID soundModelId) { + try { + mSoundTriggerService.deleteSoundModel(new ParcelUuid(soundModelId)); + } catch (RemoteException e) { + } + } + + /** + * Creates an instance of {@link SoundTriggerDetector} which can be used to start/stop + * recognition on the model and register for triggers from the model. Note that this call + * invalidates any previously returned instances for the same sound model Uuid. + * + * @param soundModelId UUID of the sound model to create the receiver object for. + * @param callback Instance of the {@link SoundTriggerDetector#Callback} object for the + * callbacks for the given sound model. + * @param handler The Handler to use for the callback operations. A null value will use the + * current thread's Looper. + * @return Instance of {@link SoundTriggerDetector} or null on error. + */ + @Nullable + public SoundTriggerDetector createSoundTriggerDetector(UUID soundModelId, + @NonNull SoundTriggerDetector.Callback callback, @Nullable Handler handler) { + if (soundModelId == null) { + return null; + } + + SoundTriggerDetector oldInstance = mReceiverInstanceMap.get(soundModelId); + if (oldInstance != null) { + // Shutdown old instance. + } + SoundTriggerDetector newInstance = new SoundTriggerDetector(mSoundTriggerService, + soundModelId, callback, handler); + mReceiverInstanceMap.put(soundModelId, newInstance); + return newInstance; + } + + /** + * Class captures the data and fields that represent a non-keyphrase sound model. Use the + * factory constructor {@link Model#create()} to create an instance. + */ + // We use encapsulation to expose the SoundTrigger.GenericSoundModel as a SystemApi. This + // prevents us from exposing SoundTrigger.GenericSoundModel as an Api. + public static class Model { + + private SoundTrigger.GenericSoundModel mGenericSoundModel; + + /** + * @hide + */ + Model(SoundTrigger.GenericSoundModel soundTriggerModel) { + mGenericSoundModel = soundTriggerModel; + } + + /** + * Factory constructor to create a SoundModel instance for use with methods in this + * class. + */ + public static Model create(UUID modelUuid, UUID vendorUuid, byte[] data) { + return new Model(new SoundTrigger.GenericSoundModel(modelUuid, + vendorUuid, data)); + } + + public UUID getModelUuid() { + return mGenericSoundModel.uuid; + } + + public UUID getVendorUuid() { + return mGenericSoundModel.vendorUuid; + } + + public byte[] getModelData() { + return mGenericSoundModel.data; + } + + /** + * @hide + */ + SoundTrigger.GenericSoundModel getGenericSoundModel() { + return mGenericSoundModel; + } + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index c186a1232cc77..361c25143b9e0 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -84,6 +84,7 @@ import com.android.server.pm.UserManagerService; import com.android.server.power.PowerManagerService; import com.android.server.power.ShutdownThread; import com.android.server.restrictions.RestrictionsManagerService; +import com.android.server.soundtrigger.SoundTriggerService; import com.android.server.statusbar.StatusBarManagerService; import com.android.server.storage.DeviceStorageMonitorService; import com.android.server.telecom.TelecomLoaderService; @@ -959,6 +960,8 @@ public final class SystemServer { mSystemServiceManager.startService(JobSchedulerService.class); + mSystemServiceManager.startService(SoundTriggerService.class); + if (!disableNonCoreServices) { if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_BACKUP)) { mSystemServiceManager.startService(BACKUP_MANAGER_SERVICE_CLASS); diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerDbHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerDbHelper.java new file mode 100644 index 0000000000000..18a5d59543e42 --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerDbHelper.java @@ -0,0 +1,144 @@ +/** + * 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.soundtrigger; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.hardware.soundtrigger.SoundTrigger; +import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel; +import android.text.TextUtils; +import android.util.Slog; + +import java.util.Locale; +import java.util.UUID; + +/** + * Helper to manage the database of the sound models that have been registered on the device. + * + * @hide + */ +public class SoundTriggerDbHelper extends SQLiteOpenHelper { + static final String TAG = "SoundTriggerDbHelper"; + static final boolean DBG = false; + + private static final String NAME = "st_sound_model.db"; + private static final int VERSION = 1; + + // Sound trigger-based sound models. + public static interface GenericSoundModelContract { + public static final String TABLE = "st_sound_model"; + public static final String KEY_MODEL_UUID = "model_uuid"; + public static final String KEY_VENDOR_UUID = "vendor_uuid"; + public static final String KEY_DATA = "data"; + } + + + // Table Create Statement for the sound trigger table + private static final String CREATE_TABLE_ST_SOUND_MODEL = "CREATE TABLE " + + GenericSoundModelContract.TABLE + "(" + + GenericSoundModelContract.KEY_MODEL_UUID + " TEXT PRIMARY KEY," + + GenericSoundModelContract.KEY_DATA + " BLOB" + " )"; + + + public SoundTriggerDbHelper(Context context) { + super(context, NAME, null, VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + // creating required tables + db.execSQL(CREATE_TABLE_ST_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 " + GenericSoundModelContract.TABLE); + onCreate(db); + } + + /** + * Updates the given sound trigger model, adds it, if it doesn't already exist. + * + */ + public boolean updateGenericSoundModel(GenericSoundModel soundModel) { + synchronized(this) { + SQLiteDatabase db = getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(GenericSoundModelContract.KEY_MODEL_UUID, soundModel.uuid.toString()); + values.put(GenericSoundModelContract.KEY_VENDOR_UUID, soundModel.vendorUuid.toString()); + values.put(GenericSoundModelContract.KEY_DATA, soundModel.data); + + try { + return db.insertWithOnConflict(GenericSoundModelContract.TABLE, null, values, + SQLiteDatabase.CONFLICT_REPLACE) != -1; + } finally { + db.close(); + } + + } + } + + public GenericSoundModel getGenericSoundModel(UUID model_uuid) { + synchronized(this) { + + // Find the corresponding sound model ID for the keyphrase. + String selectQuery = "SELECT * FROM " + GenericSoundModelContract.TABLE + + " WHERE " + GenericSoundModelContract.KEY_MODEL_UUID + "= '" + + model_uuid + "'"; + SQLiteDatabase db = getReadableDatabase(); + Cursor c = db.rawQuery(selectQuery, null); + try { + if (c.moveToFirst()) { + do { + byte[] data = c.getBlob(c.getColumnIndex( + GenericSoundModelContract.KEY_DATA)); + String vendor_uuid = c.getString( + c.getColumnIndex(GenericSoundModelContract.KEY_VENDOR_UUID)); + return new GenericSoundModel(model_uuid, UUID.fromString(vendor_uuid), + data); + } while (c.moveToNext()); + } + } finally { + c.close(); + db.close(); + } + } + return null; + } + + public boolean deleteGenericSoundModel(UUID model_uuid) { + synchronized(this) { + GenericSoundModel soundModel = getGenericSoundModel(model_uuid); + if (soundModel == null) { + return false; + } + // Delete all sound models for the given keyphrase and specified user. + SQLiteDatabase db = getWritableDatabase(); + String soundModelClause = GenericSoundModelContract.KEY_MODEL_UUID + + "='" + soundModel.uuid.toString() + "'"; + try { + return db.delete(GenericSoundModelContract.TABLE, soundModelClause, null) != 0; + } finally { + db.close(); + } + } + } +} diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java similarity index 99% rename from services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerHelper.java rename to services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java index 31d859f654bad..597f915ecdf99 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerHelper.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.voiceinteraction; +package com.android.server.soundtrigger; import android.content.BroadcastReceiver; import android.content.Context; diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java new file mode 100644 index 0000000000000..0a06bfa9bef9f --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerInternal.java @@ -0,0 +1,84 @@ +/** + * 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.soundtrigger; + +import android.hardware.soundtrigger.IRecognitionStatusCallback; +import android.hardware.soundtrigger.SoundTrigger; +import android.hardware.soundtrigger.SoundTrigger.Keyphrase; +import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent; +import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra; +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.SoundTrigger.SoundModelEvent; +import android.hardware.soundtrigger.SoundTriggerModule; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Provides a local service for managing voice-related recoginition models. This is primarily used + * by the {@link VoiceInteractionManagerService}. + */ +public abstract class SoundTriggerInternal { + /** + * 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; + + /** The {@link ModuleProperties} for the system, or null if none exists. */ + private ModuleProperties moduleProperties; + + /** + * 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}. + */ + public abstract int startRecognition(int keyphraseId, KeyphraseSoundModel soundModel, + IRecognitionStatusCallback listener, RecognitionConfig recognitionConfig); + + /** + * 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}. + */ + public abstract int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener); + + /** + * Stops all recognitions active currently and clears the internal state. + */ + public abstract void stopAllRecognitions(); + + public ModuleProperties getModuleProperties() { + return moduleProperties; + } + + public abstract void dump(FileDescriptor fd, PrintWriter pw, String[] args); +} diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java new file mode 100644 index 0000000000000..10c339586e24b --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java @@ -0,0 +1,194 @@ +/* + * 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.soundtrigger; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.Manifest; +import android.hardware.soundtrigger.IRecognitionStatusCallback; +import android.hardware.soundtrigger.SoundTrigger; +import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel; +import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig; +import android.hardware.soundtrigger.SoundTrigger.SoundTriggerModel; +import android.os.Parcel; +import android.os.ParcelUuid; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.server.SystemService; +import com.android.internal.app.ISoundTriggerService; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.UUID; + +/** + * A single SystemService to manage all sound/voice-based sound models on the DSP. + * This services provides apis to manage sound trigger-based sound models via + * the ISoundTriggerService interface. This class also publishes a local interface encapsulating + * the functionality provided by {@link SoundTriggerHelper} for use by + * {@link VoiceInteractionManagerService}. + * + * @hide + */ +public class SoundTriggerService extends SystemService { + static final String TAG = "SoundTriggerService"; + static final boolean DEBUG = false; + + final Context mContext; + private final SoundTriggerServiceStub mServiceStub; + private final LocalSoundTriggerService mLocalSoundTriggerService; + private SoundTriggerDbHelper mDbHelper; + + public SoundTriggerService(Context context) { + super(context); + mContext = context; + mServiceStub = new SoundTriggerServiceStub(); + mLocalSoundTriggerService = new LocalSoundTriggerService(context); + } + + @Override + public void onStart() { + publishBinderService(Context.SOUND_TRIGGER_SERVICE, mServiceStub); + publishLocalService(SoundTriggerInternal.class, mLocalSoundTriggerService); + } + + @Override + public void onBootPhase(int phase) { + if (PHASE_SYSTEM_SERVICES_READY == phase) { + mLocalSoundTriggerService.initSoundTriggerHelper(); + } else if (PHASE_THIRD_PARTY_APPS_CAN_START == phase) { + mDbHelper = new SoundTriggerDbHelper(mContext); + } + } + + @Override + public void onStartUser(int userHandle) { + } + + @Override + public void onSwitchUser(int userHandle) { + } + + class SoundTriggerServiceStub extends ISoundTriggerService.Stub { + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + try { + return super.onTransact(code, data, reply, flags); + } catch (RuntimeException e) { + // The activity manager only throws security exceptions, so let's + // log all others. + if (!(e instanceof SecurityException)) { + Slog.wtf(TAG, "SoundTriggerService Crash", e); + } + throw e; + } + } + + @Override + public void startRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback) { + enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER); + if (DEBUG) { + Slog.i(TAG, "startRecognition(): Uuid : " + parcelUuid); + } + } + + @Override + public void stopRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback) { + enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER); + if (DEBUG) { + Slog.i(TAG, "stopRecognition(): Uuid : " + parcelUuid); + } + } + + @Override + public SoundTrigger.SoundTriggerModel getSoundModel(ParcelUuid soundModelId) { + enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER); + if (DEBUG) { + Slog.i(TAG, "getSoundModel(): id = " + soundModelId); + } + SoundTrigger.SoundTriggerModel model = mDbHelper.getSoundTriggerModel(soundModelId.getUuid()); + if (model == null) { + Slog.e(TAG, "Null model in database."); + } + return model; + } + + @Override + public void updateSoundModel(SoundTrigger.SoundTriggerModel soundModel) { + enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER); + if (DEBUG) { + Slog.i(TAG, "updateSoundModel(): model = " + soundModel); + } + mDbHelper.updateSoundTriggerModel(soundModel); + } + + @Override + public void deleteSoundModel(ParcelUuid soundModelId) { + enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER); + if (DEBUG) { + Slog.i(TAG, "deleteSoundModel(): id = " + soundModelId); + } + mDbHelper.deleteSoundTriggerModel(soundModelId.getUuid()); + } + } + + public final class LocalSoundTriggerService extends SoundTriggerInternal { + private final Context mContext; + private SoundTriggerHelper mSoundTriggerHelper; + + LocalSoundTriggerService(Context context) { + mContext = context; + } + + void initSoundTriggerHelper() { + if (mSoundTriggerHelper == null) { + mSoundTriggerHelper = new SoundTriggerHelper(mContext); + } + } + + @Override + public int startRecognition(int keyphraseId, KeyphraseSoundModel soundModel, + IRecognitionStatusCallback listener, RecognitionConfig recognitionConfig) { + return mSoundTriggerHelper.startRecognition(keyphraseId, soundModel, listener, + recognitionConfig); + } + + @Override + public int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener) { + return mSoundTriggerHelper.stopRecognition(keyphraseId, listener); + } + + @Override + public void stopAllRecognitions() { + mSoundTriggerHelper.stopAllRecognitions(); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mSoundTriggerHelper.dump(fd, pw, args); + } + } + + private void enforceCallingPermission(String permission) { + if (mContext.checkCallingOrSelfPermission(permission) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Caller does not hold the permission " + permission); + } + } +} diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 2aef1091ab7eb..4a54643b3cf54 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -62,6 +62,7 @@ import com.android.internal.app.IVoiceInteractor; import com.android.internal.content.PackageMonitor; import com.android.internal.os.BackgroundThread; import com.android.server.LocalServices; +import com.android.server.soundtrigger.SoundTriggerInternal; import com.android.server.SystemService; import com.android.server.UiThread; @@ -79,15 +80,14 @@ public class VoiceInteractionManagerService extends SystemService { final Context mContext; final ContentResolver mResolver; final DatabaseHelper mDbHelper; - final SoundTriggerHelper mSoundTriggerHelper; final ActivityManagerInternal mAmInternal; + SoundTriggerInternal mSoundTriggerInternal; public VoiceInteractionManagerService(Context context) { super(context); mContext = context; mResolver = context.getContentResolver(); mDbHelper = new DatabaseHelper(context); - mSoundTriggerHelper = new SoundTriggerHelper(context); mServiceStub = new VoiceInteractionManagerServiceStub(); mAmInternal = LocalServices.getService(ActivityManagerInternal.class); @@ -115,7 +115,9 @@ public class VoiceInteractionManagerService extends SystemService { @Override public void onBootPhase(int phase) { - if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { + if (PHASE_SYSTEM_SERVICES_READY == phase) { + mSoundTriggerInternal = LocalServices.getService(SoundTriggerInternal.class); + } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { mServiceStub.systemRunning(isSafeMode()); } } @@ -380,7 +382,7 @@ public class VoiceInteractionManagerService extends SystemService { if (force || mImpl == null || mImpl.mUser != mCurUser || !mImpl.mComponent.equals(serviceComponent)) { - mSoundTriggerHelper.stopAllRecognitions(); + mSoundTriggerInternal.stopAllRecognitions(); if (mImpl != null) { mImpl.shutdownLocked(); } @@ -736,9 +738,9 @@ public class VoiceInteractionManagerService extends SystemService { mImpl.notifySoundModelsChangedLocked(); } } - return SoundTriggerHelper.STATUS_OK; + return SoundTriggerInternal.STATUS_OK; } else { - return SoundTriggerHelper.STATUS_ERROR; + return SoundTriggerInternal.STATUS_ERROR; } } finally { Binder.restoreCallingIdentity(caller); @@ -759,7 +761,7 @@ public class VoiceInteractionManagerService extends SystemService { boolean deleted = false; try { deleted = mDbHelper.deleteKeyphraseSoundModel(keyphraseId, callingUid, bcp47Locale); - return deleted ? SoundTriggerHelper.STATUS_OK : SoundTriggerHelper.STATUS_ERROR; + return deleted ? SoundTriggerInternal.STATUS_OK : SoundTriggerInternal.STATUS_ERROR; } finally { if (deleted) { synchronized (this) { @@ -812,7 +814,7 @@ public class VoiceInteractionManagerService extends SystemService { final long caller = Binder.clearCallingIdentity(); try { - return mSoundTriggerHelper.moduleProperties; + return mSoundTriggerInternal.getModuleProperties(); } finally { Binder.restoreCallingIdentity(caller); } @@ -845,9 +847,9 @@ public class VoiceInteractionManagerService extends SystemService { || soundModel.uuid == null || soundModel.keyphrases == null) { Slog.w(TAG, "No matching sound model found in startRecognition"); - return SoundTriggerHelper.STATUS_ERROR; + return SoundTriggerInternal.STATUS_ERROR; } else { - return mSoundTriggerHelper.startRecognition( + return mSoundTriggerInternal.startRecognition( keyphraseId, soundModel, callback, recognitionConfig); } } finally { @@ -869,7 +871,7 @@ public class VoiceInteractionManagerService extends SystemService { final long caller = Binder.clearCallingIdentity(); try { - return mSoundTriggerHelper.stopRecognition(keyphraseId, callback); + return mSoundTriggerInternal.stopRecognition(keyphraseId, callback); } finally { Binder.restoreCallingIdentity(caller); } @@ -1011,7 +1013,7 @@ public class VoiceInteractionManagerService extends SystemService { } mImpl.dumpLocked(fd, pw, args); } - mSoundTriggerHelper.dump(fd, pw, args); + mSoundTriggerInternal.dump(fd, pw, args); } private void enforceCallingPermission(String permission) { @@ -1060,7 +1062,7 @@ public class VoiceInteractionManagerService extends SystemService { // The user is force stopping our current interactor/recognizer. // Clear the current settings and restore default state. synchronized (VoiceInteractionManagerService.this) { - mSoundTriggerHelper.stopAllRecognitions(); + mSoundTriggerInternal.stopAllRecognitions(); if (mImpl != null) { mImpl.shutdownLocked(); mImpl = null; diff --git a/tests/SoundTriggerTestApp/Android.mk b/tests/SoundTriggerTestApp/Android.mk new file mode 100644 index 0000000000000..7bcab5e537728 --- /dev/null +++ b/tests/SoundTriggerTestApp/Android.mk @@ -0,0 +1,12 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_PACKAGE_NAME := SoundTriggerTestApp + +LOCAL_MODULE_TAGS := optional + +LOCAL_PRIVILEGED_MODULE := true + +include $(BUILD_PACKAGE) diff --git a/tests/SoundTriggerTestApp/AndroidManifest.xml b/tests/SoundTriggerTestApp/AndroidManifest.xml new file mode 100644 index 0000000000000..40619da156eeb --- /dev/null +++ b/tests/SoundTriggerTestApp/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/tests/SoundTriggerTestApp/res/layout/main.xml b/tests/SoundTriggerTestApp/res/layout/main.xml new file mode 100644 index 0000000000000..9d2b9d92c0168 --- /dev/null +++ b/tests/SoundTriggerTestApp/res/layout/main.xml @@ -0,0 +1,43 @@ + + + + + +