diff options
23 files changed, 1289 insertions, 19 deletions
diff --git a/Android.mk b/Android.mk index 1d797c49b3e7..8daa1aa9c114 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 fc1a355632e6..5eed781d76c3 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<HardwarePropertiesManager>() { @Override public HardwarePropertiesManager createService(ContextImpl ctx) { return new HardwarePropertiesManager(); }}); + + registerService(Context.SOUND_TRIGGER_SERVICE, SoundTriggerManager.class, + new CachedServiceFetcher<SoundTriggerManager>() { + @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 27cdd5054653..3142b40ca9bd 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 {} @@ -3195,6 +3196,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 * with the backup mechanism. diff --git a/core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl b/core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl index 2f6dbe7d3794..597efa566d2c 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 e16ea71d9b65..fec64ea338ea 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 d49040987a2e..882908a9f46e 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<GenericSoundModel> CREATOR + = new Parcelable.Creator<GenericSoundModel>() { + 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 */ @@ -1019,6 +1082,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} */ /** Sound Model was updated */ @@ -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 ac7d539bb628..76a401d74559 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 000000000000..9de4a6c62a49 --- /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 56c3fc88cf23..5a2f6015494b 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2860,6 +2860,12 @@ <permission android:name="android.permission.RECEIVE_MEDIA_RESOURCE_USAGE" android:protectionLevel="signature|privileged" /> + <!-- Must be required by system/priv apps when accessing the sound trigger + APIs given by {@link SoundTriggerManager}. + @hide <p>Not for use by third-party applications.</p> --> + <permission android:name="android.permission.MANAGE_SOUND_TRIGGER" + android:protectionLevel="signature|privileged" /> + <application android:process="system" android:persistent="true" android:hasCode="false" diff --git a/media/java/android/media/soundtrigger/SoundTriggerDetector.java b/media/java/android/media/soundtrigger/SoundTriggerDetector.java new file mode 100644 index 000000000000..ebba343fdd3a --- /dev/null +++ b/media/java/android/media/soundtrigger/SoundTriggerDetector.java @@ -0,0 +1,180 @@ +/** + * 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 android.media.soundtrigger; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.hardware.soundtrigger.IRecognitionStatusCallback; +import android.hardware.soundtrigger.SoundTrigger; +import android.os.Handler; +import android.os.ParcelUuid; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.app.ISoundTriggerService; + +import java.io.PrintWriter; +import java.util.UUID; + +/** + * A class that allows interaction with the actual sound trigger detection on the system. + * Sound trigger detection refers to a detectors that match generic sound patterns that are + * not voice-based. The voice-based recognition models should utilize the {@link + * VoiceInteractionService} instead. Access to this class is protected by a permission + * granted only to system or privileged apps. + * + * @hide + */ +public final class SoundTriggerDetector { + private static final boolean DBG = false; + private static final String TAG = "SoundTriggerDetector"; + + private final Object mLock = new Object(); + + private final ISoundTriggerService mSoundTriggerService; + private final UUID mSoundModelId; + private final Callback mCallback; + private final Handler mHandler; + private final RecognitionCallback mRecognitionCallback; + + public abstract class Callback { + /** + * Called when the availability of the sound model changes. + */ + public abstract void onAvailabilityChanged(int status); + + /** + * Called when the sound model has triggered (such as when it matched a + * given sound pattern). + */ + public abstract void onDetected(); + + /** + * Called when the detection fails due to an error. + */ + public abstract void onError(); + + /** + * Called when the recognition is paused temporarily for some reason. + * This is an informational callback, and the clients shouldn't be doing anything here + * except showing an indication on their UI if they have to. + */ + public abstract void onRecognitionPaused(); + + /** + * Called when the recognition is resumed after it was temporarily paused. + * This is an informational callback, and the clients shouldn't be doing anything here + * except showing an indication on their UI if they have to. + */ + public abstract void onRecognitionResumed(); + } + + /** + * This class should be constructed by the {@link SoundTriggerManager}. + * @hide + */ + SoundTriggerDetector(ISoundTriggerService soundTriggerService, UUID soundModelId, + @NonNull Callback callback, @Nullable Handler handler) { + mSoundTriggerService = soundTriggerService; + mSoundModelId = soundModelId; + mCallback = callback; + if (handler == null) { + mHandler = new Handler(); + } else { + mHandler = handler; + } + mRecognitionCallback = new RecognitionCallback(); + } + + /** + * Starts recognition on the associated sound model. Result is indicated via the + * {@link Callback}. + * @return Indicates whether the call succeeded or not. + */ + public boolean startRecognition() { + if (DBG) { + Slog.d(TAG, "startRecognition()"); + } + try { + mSoundTriggerService.startRecognition(new ParcelUuid(mSoundModelId), + mRecognitionCallback); + } catch (RemoteException e) { + return false; + } + return true; + } + + /** + * Stops recognition for the associated model. + */ + public boolean stopRecognition() { + try { + mSoundTriggerService.stopRecognition(new ParcelUuid(mSoundModelId), + mRecognitionCallback); + } catch (RemoteException e) { + return false; + } + return true; + } + + public void dump(String prefix, PrintWriter pw) { + synchronized (mLock) { + // TODO: Dump useful debug information. + } + } + + /** + * Callback that handles events from the lower sound trigger layer. + * @hide + */ + private static class RecognitionCallback extends + IRecognitionStatusCallback.Stub { + + /** + * @hide + */ + @Override + public void onDetected(SoundTrigger.RecognitionEvent event) { + Slog.e(TAG, "onDetected()" + event); + } + + /** + * @hide + */ + @Override + public void onError(int status) { + Slog.e(TAG, "onError()" + status); + } + + /** + * @hide + */ + @Override + public void onRecognitionPaused() { + Slog.e(TAG, "onRecognitionPaused()"); + } + + /** + * @hide + */ + @Override + public void onRecognitionResumed() { + Slog.e(TAG, "onRecognitionResumed()"); + } + } +} diff --git a/media/java/android/media/soundtrigger/SoundTriggerManager.java b/media/java/android/media/soundtrigger/SoundTriggerManager.java new file mode 100644 index 000000000000..4ae8e72ac999 --- /dev/null +++ b/media/java/android/media/soundtrigger/SoundTriggerManager.java @@ -0,0 +1,172 @@ +/** + * 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 android.media.soundtrigger; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Context; +import android.hardware.soundtrigger.SoundTrigger; +import android.os.Handler; +import android.os.ParcelUuid; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.app.ISoundTriggerService; + +import java.util.HashMap; +import java.util.UUID; + +/** + * This class provides management of non-voice (general sound trigger) based sound recognition + * models. Usage of this class is restricted to system or signature applications only. This allows + * OEMs to write apps that can manage non-voice based sound trigger models. + * + * @hide + * TODO: Mark this as a SystemApi and get approval. + */ +public final class SoundTriggerManager { + private static final boolean DBG = false; + private static final String TAG = "SoundTriggerManager"; + + private final Context mContext; + private final ISoundTriggerService mSoundTriggerService; + + // Stores a mapping from the sound model UUID to the SoundTriggerInstance created by + // the createSoundTriggerDetector() call. + private final HashMap<UUID, SoundTriggerDetector> mReceiverInstanceMap; + + /** + * @hide + */ + public SoundTriggerManager(Context context, ISoundTriggerService soundTriggerService ) { + if (DBG) { + Slog.i(TAG, "SoundTriggerManager created."); + } + mSoundTriggerService = soundTriggerService; + mContext = context; + mReceiverInstanceMap = new HashMap<UUID, SoundTriggerDetector>(); + } + + /** + * 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 c186a1232cc7..361c25143b9e 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 000000000000..18a5d59543e4 --- /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 index 31d859f654ba..597f915ecdf9 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 000000000000..0a06bfa9bef9 --- /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 000000000000..10c339586e24 --- /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 2aef1091ab7e..4a54643b3cf5 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 000000000000..7bcab5e53772 --- /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 000000000000..40619da156ee --- /dev/null +++ b/tests/SoundTriggerTestApp/AndroidManifest.xml @@ -0,0 +1,17 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.test.soundtrigger"> + + <uses-permission android:name="android.permission.MANAGE_SOUND_TRIGGER" /> + <application + android:permission="android.permission.MANAGE_SOUND_TRIGGER"> + <activity + android:name="TestSoundTriggerActivity" + android:label="SoundTrigger Test Application" + android:theme="@android:style/Theme.Material.Light.Voice"> + <intent-filter> + <action android:name="com.android.intent.action.MANAGE_SOUND_TRIGGER" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/SoundTriggerTestApp/res/layout/main.xml b/tests/SoundTriggerTestApp/res/layout/main.xml new file mode 100644 index 000000000000..9d2b9d92c016 --- /dev/null +++ b/tests/SoundTriggerTestApp/res/layout/main.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 Google Inc. + + 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + > + + <Button + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/enroll" + android:onClick="onEnrollButtonClicked" + android:padding="20dp" /> + + <Button + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/reenroll" + android:onClick="onReEnrollButtonClicked" + android:padding="20dp" /> + + <Button + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/unenroll" + android:onClick="onUnEnrollButtonClicked" + android:padding="20dp" /> +</LinearLayout>
\ No newline at end of file diff --git a/tests/SoundTriggerTestApp/res/values/strings.xml b/tests/SoundTriggerTestApp/res/values/strings.xml new file mode 100644 index 000000000000..07bac2a263ef --- /dev/null +++ b/tests/SoundTriggerTestApp/res/values/strings.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 Google Inc. + + 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. +--> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + + <string name="enroll">Enroll</string> + <string name="reenroll">Re-enroll</string> + <string name="unenroll">Un-enroll</string> +</resources>
\ No newline at end of file diff --git a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java new file mode 100644 index 000000000000..98713bd2c6f9 --- /dev/null +++ b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java @@ -0,0 +1,115 @@ +/* + * 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.test.soundtrigger; + +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.soundtrigger.SoundTrigger; +import android.hardware.soundtrigger.SoundTrigger.SoundTriggerModel; +import android.media.soundtrigger.SoundTriggerManager; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.ParcelUuid; +import android.util.Log; + +import com.android.internal.app.ISoundTriggerService; + +import java.util.UUID; + +/** + * Utility class for the managing sound trigger sound models. + */ +public class SoundTriggerUtil { + private static final String TAG = "TestSoundTriggerUtil:Hotsound"; + + private final ISoundTriggerService mSoundTriggerService; + private final SoundTriggerManager mSoundTriggerManager; + private final Context mContext; + + public SoundTriggerUtil(Context context) { + mSoundTriggerService = ISoundTriggerService.Stub.asInterface( + ServiceManager.getService(Context.SOUND_TRIGGER_SERVICE)); + mSoundTriggerManager = (SoundTriggerManager) context.getSystemService( + Context.SOUND_TRIGGER_SERVICE); + mContext = context; + } + + /** + * Adds/Updates a sound model. + * The sound model must contain a valid UUID. + * + * @param soundModel The sound model to add/update. + */ + public boolean addOrUpdateSoundModel(SoundTriggerModel soundModel) { + try { + mSoundTriggerService.updateSoundModel(soundModel); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in updateSoundModel", e); + } + return true; + } + + public void addOrUpdateSoundModel(SoundTriggerManager.Model soundModel) { + mSoundTriggerManager.updateModel(soundModel); + } + + /** + * Gets the sound model for the given keyphrase, null if none exists. + * If a sound model for a given keyphrase exists, and it needs to be updated, + * it should be obtained using this method, updated and then passed in to + * {@link #addOrUpdateSoundModel(SoundTriggerModel)} without changing the IDs. + * + * @param modelId The model ID to look-up the sound model for. + * @return The sound model if one was found, null otherwise. + */ + @Nullable + public SoundTriggerModel getSoundModel(UUID modelId) { + SoundTriggerModel model = null; + try { + model = mSoundTriggerService.getSoundModel(new ParcelUuid(modelId)); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in updateKeyphraseSoundModel"); + } + + if (model == null) { + Log.w(TAG, "No models present for the gien keyphrase ID"); + return null; + } else { + return model; + } + } + + /** + * Deletes the sound model for the given keyphrase id. + * + * @param modelId The model ID to look-up the sound model for. + * @return {@code true} if the call succeeds, {@code false} otherwise. + */ + @Nullable + public boolean deleteSoundModel(UUID modelId) { + try { + mSoundTriggerService.deleteSoundModel(new ParcelUuid(modelId)); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in updateSoundModel"); + } + return true; + } + + public void deleteSoundModelUsingManager(UUID modelId) { + mSoundTriggerManager.deleteModel(modelId); + } +} diff --git a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java new file mode 100644 index 000000000000..82890c1717c4 --- /dev/null +++ b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java @@ -0,0 +1,121 @@ +/* + * 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.test.soundtrigger; + +import java.util.Random; +import java.util.UUID; + +import android.app.Activity; +import android.hardware.soundtrigger.SoundTrigger; +import android.hardware.soundtrigger.SoundTrigger.SoundTriggerModel; +import android.media.soundtrigger.SoundTriggerManager; +import android.os.Bundle; +import android.os.UserManager; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +public class TestSoundTriggerActivity extends Activity { + private static final String TAG = "TestSoundTriggerActivity"; + private static final boolean DBG = true; + + private SoundTriggerUtil mSoundTriggerUtil; + private Random mRandom; + private UUID mModelUuid = UUID.randomUUID(); + private UUID mModelUuid2 = UUID.randomUUID(); + private UUID mVendorUuid = UUID.randomUUID(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + if (DBG) Log.d(TAG, "onCreate"); + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + mSoundTriggerUtil = new SoundTriggerUtil(this); + mRandom = new Random(); + } + + /** + * Called when the user clicks the enroll button. + * Performs a fresh enrollment. + */ + public void onEnrollButtonClicked(View v) { + // Generate a fake model to push. + byte[] data = new byte[1024]; + mRandom.nextBytes(data); + SoundTriggerModel model = new SoundTriggerModel(mModelUuid, mVendorUuid, data); + + boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(model); + if (status) { + Toast.makeText( + this, "Successfully created sound trigger model UUID=" + mModelUuid, Toast.LENGTH_SHORT) + .show(); + } else { + Toast.makeText(this, "Failed to enroll!!!" + mModelUuid, Toast.LENGTH_SHORT).show(); + } + + // Test the SoundManager API. + SoundTriggerManager.Model tmpModel = SoundTriggerManager.Model.create(mModelUuid2, + mVendorUuid, data); + mSoundTriggerUtil.addOrUpdateSoundModel(tmpModel); + } + + /** + * Called when the user clicks the un-enroll button. + * Clears the enrollment information for the user. + */ + public void onUnEnrollButtonClicked(View v) { + SoundTriggerModel soundModel = mSoundTriggerUtil.getSoundModel(mModelUuid); + if (soundModel == null) { + Toast.makeText(this, "Sound model not found!!!", Toast.LENGTH_SHORT).show(); + return; + } + boolean status = mSoundTriggerUtil.deleteSoundModel(mModelUuid); + if (status) { + Toast.makeText(this, "Successfully deleted model UUID=" + soundModel.uuid, + Toast.LENGTH_SHORT) + .show(); + } else { + Toast.makeText(this, "Failed to delete sound model!!!", Toast.LENGTH_SHORT).show(); + } + mSoundTriggerUtil.deleteSoundModelUsingManager(mModelUuid2); + } + + /** + * Called when the user clicks the re-enroll button. + * Uses the previously enrolled sound model and makes changes to it before pushing it back. + */ + public void onReEnrollButtonClicked(View v) { + SoundTriggerModel soundModel = mSoundTriggerUtil.getSoundModel(mModelUuid); + if (soundModel == null) { + Toast.makeText(this, "Sound model not found!!!", Toast.LENGTH_SHORT).show(); + return; + } + // Generate a fake model to push. + byte[] data = new byte[2048]; + mRandom.nextBytes(data); + SoundTriggerModel updated = new SoundTriggerModel(soundModel.uuid, + soundModel.vendorUuid, data); + boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(updated); + if (status) { + Toast.makeText(this, "Successfully re-enrolled, model UUID=" + updated.uuid, + Toast.LENGTH_SHORT) + .show(); + } else { + Toast.makeText(this, "Failed to re-enroll!!!", Toast.LENGTH_SHORT).show(); + } + } +} |