diff options
10 files changed, 413 insertions, 39 deletions
diff --git a/core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl b/core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl index 038d7ef36b71..0bf4f25314e1 100644 --- a/core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl +++ b/core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl @@ -16,6 +16,8 @@ package android.hardware.soundtrigger; +import android.hardware.soundtrigger.SoundTrigger; + /** * @hide */ @@ -27,7 +29,7 @@ oneway interface IRecognitionStatusCallback { * TODO: See if the data being passed in works well, if not use shared memory. * This *MUST* not exceed 100K. */ - void onDetected(in byte[] data); + void onDetected(in SoundTrigger.RecognitionEvent recognitionEvent); /** * Called when the detection for the associated keyphrase stops. */ diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.aidl b/core/java/android/hardware/soundtrigger/SoundTrigger.aidl index 837691a9c2c2..9adc6bc6e88b 100644 --- a/core/java/android/hardware/soundtrigger/SoundTrigger.aidl +++ b/core/java/android/hardware/soundtrigger/SoundTrigger.aidl @@ -21,4 +21,5 @@ parcelable SoundTrigger.Keyphrase; parcelable SoundTrigger.KeyphraseRecognitionExtra; parcelable SoundTrigger.KeyphraseSoundModel; parcelable SoundTrigger.ModuleProperties; -parcelable SoundTrigger.RecognitionConfig;
\ No newline at end of file +parcelable SoundTrigger.RecognitionConfig; +parcelable SoundTrigger.RecognitionEvent;
\ No newline at end of file diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java index 9a5cd9b814c4..3e8436851638 100644 --- a/core/java/android/hardware/soundtrigger/SoundTrigger.java +++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java @@ -347,12 +347,7 @@ public class SoundTrigger { private static KeyphraseSoundModel fromParcel(Parcel in) { UUID uuid = UUID.fromString(in.readString()); - byte[] data = null; - int dataLength = in.readInt(); - if (dataLength >= 0) { - data = new byte[dataLength]; - in.readByteArray(data); - } + byte[] data = in.readBlob(); Keyphrase[] keyphrases = in.createTypedArray(Keyphrase.CREATOR); return new KeyphraseSoundModel(uuid, data, keyphrases); } @@ -365,12 +360,7 @@ public class SoundTrigger { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(uuid.toString()); - if (data != null) { - dest.writeInt(data.length); - dest.writeByteArray(data); - } else { - dest.writeInt(-1); - } + dest.writeBlob(data); dest.writeTypedArray(keyphrases, 0); } @@ -406,7 +396,7 @@ public class SoundTrigger { * {@link StatusListener#onRecognition(RecognitionEvent)} * callback upon recognition success or failure. */ - public static class RecognitionEvent { + public static class RecognitionEvent implements Parcelable { /** Recognition status e.g {@link #RECOGNITION_STATUS_SUCCESS} */ public final int status; /** Sound Model corresponding to this event callback */ @@ -425,7 +415,7 @@ public class SoundTrigger { * typically during enrollment. */ public final byte[] data; - RecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, + public RecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, int captureSession, int captureDelayMs, int capturePreambleMs, byte[] data) { this.status = status; this.soundModelHandle = soundModelHandle; @@ -435,6 +425,85 @@ public class SoundTrigger { this.capturePreambleMs = capturePreambleMs; this.data = data; } + + public static final Parcelable.Creator<RecognitionEvent> CREATOR + = new Parcelable.Creator<RecognitionEvent>() { + public RecognitionEvent createFromParcel(Parcel in) { + return RecognitionEvent.fromParcel(in); + } + + public RecognitionEvent[] newArray(int size) { + return new RecognitionEvent[size]; + } + }; + + private static RecognitionEvent fromParcel(Parcel in) { + int status = in.readInt(); + int soundModelHandle = in.readInt(); + boolean captureAvailable = in.readByte() == 1; + int captureSession = in.readInt(); + int captureDelayMs = in.readInt(); + int capturePreambleMs = in.readInt(); + byte[] data = in.readBlob(); + return new RecognitionEvent(status, soundModelHandle, captureAvailable, captureSession, + captureDelayMs, capturePreambleMs, data); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(status); + dest.writeInt(soundModelHandle); + dest.writeByte((byte) (captureAvailable ? 1 : 0)); + dest.writeInt(captureSession); + dest.writeInt(captureDelayMs); + dest.writeInt(capturePreambleMs); + dest.writeBlob(data); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (captureAvailable ? 1231 : 1237); + result = prime * result + captureDelayMs; + result = prime * result + capturePreambleMs; + result = prime * result + captureSession; + result = prime * result + Arrays.hashCode(data); + result = prime * result + soundModelHandle; + result = prime * result + status; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RecognitionEvent other = (RecognitionEvent) obj; + if (captureAvailable != other.captureAvailable) + return false; + if (captureDelayMs != other.captureDelayMs) + return false; + if (capturePreambleMs != other.capturePreambleMs) + return false; + if (captureSession != other.captureSession) + return false; + if (!Arrays.equals(data, other.data)) + return false; + if (soundModelHandle != other.soundModelHandle) + return false; + if (status != other.status) + return false; + return true; + } } /** @@ -475,12 +544,7 @@ public class SoundTrigger { boolean captureRequested = in.readByte() == 1; KeyphraseRecognitionExtra[] keyphrases = in.createTypedArray(KeyphraseRecognitionExtra.CREATOR); - byte[] data = null; - int dataLength = in.readInt(); - if (dataLength >= 0) { - data = new byte[dataLength]; - in.readByteArray(data); - } + byte[] data = in.readBlob(); return new RecognitionConfig(captureRequested, keyphrases, data); } @@ -488,12 +552,7 @@ public class SoundTrigger { public void writeToParcel(Parcel dest, int flags) { dest.writeByte((byte) (captureRequested ? 1 : 0)); dest.writeTypedArray(keyphrases, 0); - if (data != null) { - dest.writeInt(data.length); - dest.writeByteArray(data); - } else { - dest.writeInt(-1); - } + dest.writeBlob(data); } @Override diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 59cd97c445f4..a1c2aa1d3e60 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -485,6 +485,7 @@ public final class Parcel { * growing {@link #dataCapacity} if needed. * @param b Bytes to place into the parcel. * {@hide} + * {@SystemApi} */ public final void writeBlob(byte[] b) { nativeWriteBlob(mNativePtr, b, 0, (b != null) ? b.length : 0); @@ -1714,6 +1715,7 @@ public final class Parcel { /** * Read a blob of data from the parcel and return it as a byte array. * {@hide} + * {@SystemApi} */ public final byte[] readBlob() { return nativeReadBlob(mNativePtr); diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java index d077a17072e9..a8c08d55c003 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -27,6 +27,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.os.AsyncTask; import android.os.Handler; import android.os.Message; @@ -380,10 +381,10 @@ public class AlwaysOnHotwordDetector { } @Override - public void onDetected(byte[] data) { + public void onDetected(RecognitionEvent recognitionEvent) { Slog.i(TAG, "onDetected"); Message message = Message.obtain(mHandler, MSG_HOTWORD_DETECTED); - message.obj = data; + message.obj = recognitionEvent.data; message.sendToTarget(); } diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp index 3ba481ed3f21..44863cc7ef4c 100644 --- a/core/jni/android_os_Parcel.cpp +++ b/core/jni/android_os_Parcel.cpp @@ -194,6 +194,14 @@ static void android_os_Parcel_writeBlob(JNIEnv* env, jclass clazz, jlong nativeP return; } + if (data == NULL) { + const status_t err = parcel->writeInt32(-1); + if (err != NO_ERROR) { + signalExceptionForError(env, clazz, err); + } + return; + } + const status_t err = parcel->writeInt32(length); if (err != NO_ERROR) { signalExceptionForError(env, clazz, err); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerHelper.java index 86dca79bd9c1..1e4a518c1c16 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerHelper.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerHelper.java @@ -238,20 +238,13 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { switch (event.status) { case SoundTrigger.RECOGNITION_STATUS_SUCCESS: - // TODO: Pass the captured audio back. try { - listener.onDetected(null); + listener.onDetected(event); } catch (RemoteException e) { Slog.w(TAG, "RemoteException in onDetected"); } break; - case SoundTrigger.RECOGNITION_STATUS_ABORT: - try { - listener.onDetectionStopped(); - } catch (RemoteException e) { - Slog.w(TAG, "RemoteException in onDetectionStopped"); - } - break; + case SoundTrigger.RECOGNITION_STATUS_ABORT: // fall-through case SoundTrigger.RECOGNITION_STATUS_FAILURE: try { listener.onDetectionStopped(); diff --git a/tests/SoundTriggerTests/Android.mk b/tests/SoundTriggerTests/Android.mk new file mode 100644 index 000000000000..407a9d70d93f --- /dev/null +++ b/tests/SoundTriggerTests/Android.mk @@ -0,0 +1,27 @@ +# +# 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. +# +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_JAVA_LIBRARIES := android.test.runner + +LOCAL_PACKAGE_NAME := SoundTriggerTests + +include $(BUILD_PACKAGE) diff --git a/tests/SoundTriggerTests/AndroidManifest.xml b/tests/SoundTriggerTests/AndroidManifest.xml new file mode 100644 index 000000000000..5e5a108d263e --- /dev/null +++ b/tests/SoundTriggerTests/AndroidManifest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android.hardware.soundtrigger"> + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="android.test.InstrumentationTestRunner" + android:targetPackage="android.hardware.soundtrigger" + android:label="Tests for android.hardware.soundtrigger" /> +</manifest> diff --git a/tests/SoundTriggerTests/src/android/hardware/soundtrigger/SoundTriggerTest.java b/tests/SoundTriggerTests/src/android/hardware/soundtrigger/SoundTriggerTest.java new file mode 100644 index 000000000000..5d32c66e42df --- /dev/null +++ b/tests/SoundTriggerTests/src/android/hardware/soundtrigger/SoundTriggerTest.java @@ -0,0 +1,256 @@ +/* + * 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.hardware.soundtrigger; + +import android.hardware.soundtrigger.SoundTrigger; +import android.hardware.soundtrigger.SoundTrigger.Keyphrase; +import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel; +import android.hardware.soundtrigger.SoundTrigger.RecognitionEvent; +import android.os.Parcel; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.SmallTest; + +import java.util.Arrays; +import java.util.Random; +import java.util.UUID; + +public class SoundTriggerTest extends InstrumentationTestCase { + private Random mRandom = new Random(); + + @SmallTest + public void testKeyphraseParcelUnparcel_noUsers() throws Exception { + Keyphrase keyphrase = new Keyphrase(1, 0, "en-US", "hello", null); + + // Write to a parcel + Parcel parcel = Parcel.obtain(); + keyphrase.writeToParcel(parcel, 0); + + // Read from it + parcel.setDataPosition(0); + Keyphrase unparceled = Keyphrase.CREATOR.createFromParcel(parcel); + + // Verify that they are the same + assertEquals(keyphrase.id, unparceled.id); + assertNull(unparceled.users); + assertEquals(keyphrase.locale, unparceled.locale); + assertEquals(keyphrase.text, unparceled.text); + } + + @SmallTest + public void testKeyphraseParcelUnparcel_zeroUsers() throws Exception { + Keyphrase keyphrase = new Keyphrase(1, 0, "en-US", "hello", new int[0]); + + // Write to a parcel + Parcel parcel = Parcel.obtain(); + keyphrase.writeToParcel(parcel, 0); + + // Read from it + parcel.setDataPosition(0); + Keyphrase unparceled = Keyphrase.CREATOR.createFromParcel(parcel); + + // Verify that they are the same + assertEquals(keyphrase.id, unparceled.id); + assertTrue(Arrays.equals(keyphrase.users, unparceled.users)); + assertEquals(keyphrase.locale, unparceled.locale); + assertEquals(keyphrase.text, unparceled.text); + } + + @SmallTest + public void testKeyphraseParcelUnparcel_pos() throws Exception { + Keyphrase keyphrase = new Keyphrase(1, 0, "en-US", "hello", new int[] {1, 2, 3, 4, 5}); + + // Write to a parcel + Parcel parcel = Parcel.obtain(); + keyphrase.writeToParcel(parcel, 0); + + // Read from it + parcel.setDataPosition(0); + Keyphrase unparceled = Keyphrase.CREATOR.createFromParcel(parcel); + + // Verify that they are the same + assertEquals(keyphrase.id, unparceled.id); + assertTrue(Arrays.equals(keyphrase.users, unparceled.users)); + assertEquals(keyphrase.locale, unparceled.locale); + assertEquals(keyphrase.text, unparceled.text); + } + + @SmallTest + public void testKeyphraseSoundModelParcelUnparcel_noData() throws Exception { + Keyphrase[] keyphrases = new Keyphrase[2]; + keyphrases[0] = new Keyphrase(1, 0, "en-US", "hello", new int[] {0}); + keyphrases[1] = new Keyphrase(2, 0, "fr-FR", "there", new int[] {1, 2}); + KeyphraseSoundModel ksm = new KeyphraseSoundModel(UUID.randomUUID(), null, keyphrases); + + // Write to a parcel + Parcel parcel = Parcel.obtain(); + ksm.writeToParcel(parcel, 0); + + // Read from it + parcel.setDataPosition(0); + KeyphraseSoundModel unparceled = KeyphraseSoundModel.CREATOR.createFromParcel(parcel); + + // Verify that they are the same + assertEquals(ksm.uuid, unparceled.uuid); + assertNull(unparceled.data); + assertEquals(ksm.type, unparceled.type); + assertTrue(Arrays.equals(keyphrases, unparceled.keyphrases)); + } + + @SmallTest + public void testKeyphraseSoundModelParcelUnparcel_zeroData() throws Exception { + Keyphrase[] keyphrases = new Keyphrase[2]; + keyphrases[0] = new Keyphrase(1, 0, "en-US", "hello", new int[] {0}); + keyphrases[1] = new Keyphrase(2, 0, "fr-FR", "there", new int[] {1, 2}); + KeyphraseSoundModel ksm = new KeyphraseSoundModel(UUID.randomUUID(), new byte[0], + keyphrases); + + // Write to a parcel + Parcel parcel = Parcel.obtain(); + ksm.writeToParcel(parcel, 0); + + // Read from it + parcel.setDataPosition(0); + KeyphraseSoundModel unparceled = KeyphraseSoundModel.CREATOR.createFromParcel(parcel); + + // Verify that they are the same + assertEquals(ksm.uuid, unparceled.uuid); + assertEquals(ksm.type, unparceled.type); + assertTrue(Arrays.equals(ksm.keyphrases, unparceled.keyphrases)); + assertTrue(Arrays.equals(ksm.data, unparceled.data)); + } + + @SmallTest + public void testKeyphraseSoundModelParcelUnparcel_noKeyphrases() throws Exception { + byte[] data = new byte[10]; + mRandom.nextBytes(data); + KeyphraseSoundModel ksm = new KeyphraseSoundModel(UUID.randomUUID(), data, null); + + // Write to a parcel + Parcel parcel = Parcel.obtain(); + ksm.writeToParcel(parcel, 0); + + // Read from it + parcel.setDataPosition(0); + KeyphraseSoundModel unparceled = KeyphraseSoundModel.CREATOR.createFromParcel(parcel); + + // Verify that they are the same + assertEquals(ksm.uuid, unparceled.uuid); + assertEquals(ksm.type, unparceled.type); + assertNull(unparceled.keyphrases); + assertTrue(Arrays.equals(ksm.data, unparceled.data)); + } + + @SmallTest + public void testKeyphraseSoundModelParcelUnparcel_zeroKeyphrases() throws Exception { + byte[] data = new byte[10]; + mRandom.nextBytes(data); + KeyphraseSoundModel ksm = new KeyphraseSoundModel(UUID.randomUUID(), data, + new Keyphrase[0]); + + // Write to a parcel + Parcel parcel = Parcel.obtain(); + ksm.writeToParcel(parcel, 0); + + // Read from it + parcel.setDataPosition(0); + KeyphraseSoundModel unparceled = KeyphraseSoundModel.CREATOR.createFromParcel(parcel); + + // Verify that they are the same + assertEquals(ksm.uuid, unparceled.uuid); + assertEquals(ksm.type, unparceled.type); + assertTrue(Arrays.equals(ksm.keyphrases, unparceled.keyphrases)); + assertTrue(Arrays.equals(ksm.data, unparceled.data)); + } + + @LargeTest + public void testKeyphraseSoundModelParcelUnparcel_largeData() throws Exception { + Keyphrase[] keyphrases = new Keyphrase[2]; + keyphrases[0] = new Keyphrase(1, 0, "en-US", "hello", new int[] {0}); + keyphrases[1] = new Keyphrase(2, 0, "fr-FR", "there", new int[] {1, 2}); + byte[] data = new byte[200 * 1024]; + mRandom.nextBytes(data); + KeyphraseSoundModel ksm = new KeyphraseSoundModel(UUID.randomUUID(), data, keyphrases); + + // Write to a parcel + Parcel parcel = Parcel.obtain(); + ksm.writeToParcel(parcel, 0); + + // Read from it + parcel.setDataPosition(0); + KeyphraseSoundModel unparceled = KeyphraseSoundModel.CREATOR.createFromParcel(parcel); + + // Verify that they are the same + assertEquals(ksm.uuid, unparceled.uuid); + assertEquals(ksm.type, unparceled.type); + assertTrue(Arrays.equals(ksm.data, unparceled.data)); + assertTrue(Arrays.equals(ksm.keyphrases, unparceled.keyphrases)); + } + + @SmallTest + public void testRecognitionEventParcelUnparcel_noData() throws Exception { + RecognitionEvent re = new RecognitionEvent(SoundTrigger.RECOGNITION_STATUS_SUCCESS, 1, + true, 2, 3, 4, null); + + // Write to a parcel + Parcel parcel = Parcel.obtain(); + re.writeToParcel(parcel, 0); + + // Read from it + parcel.setDataPosition(0); + RecognitionEvent unparceled = RecognitionEvent.CREATOR.createFromParcel(parcel); + + // Verify that they are the same + assertEquals(re, unparceled); + } + + @SmallTest + public void testRecognitionEventParcelUnparcel_zeroData() throws Exception { + RecognitionEvent re = new RecognitionEvent(SoundTrigger.RECOGNITION_STATUS_FAILURE, 1, + true, 2, 3, 4, new byte[1]); + + // Write to a parcel + Parcel parcel = Parcel.obtain(); + re.writeToParcel(parcel, 0); + + // Read from it + parcel.setDataPosition(0); + RecognitionEvent unparceled = RecognitionEvent.CREATOR.createFromParcel(parcel); + + // Verify that they are the same + assertEquals(re, unparceled); + } + + @SmallTest + public void testRecognitionEventParcelUnparcel_largeData() throws Exception { + byte[] data = new byte[200 * 1024]; + mRandom.nextBytes(data); + RecognitionEvent re = new RecognitionEvent(SoundTrigger.RECOGNITION_STATUS_ABORT, 1, + false, 2, 3, 4, data); + + // Write to a parcel + Parcel parcel = Parcel.obtain(); + re.writeToParcel(parcel, 0); + + // Read from it + parcel.setDataPosition(0); + RecognitionEvent unparceled = RecognitionEvent.CREATOR.createFromParcel(parcel); + + // Verify that they are the same + assertEquals(re, unparceled); + } +} |