Expand HDS API to allow egressing training data
Change-Id: Ibde0c39ec1223c816d369a4f2e2793d295e00903
Test: atest --no-bazel-mode CtsVoiceInteractionTestCases:HotwordDetectionServiceBasicTest
Bug: 291599840
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 5d1f6dc..1305f32 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -315,6 +315,7 @@
field @FlaggedApi("android.app.usage.report_usage_stats_permission") public static final String REPORT_USAGE_STATS = "android.permission.REPORT_USAGE_STATS";
field @Deprecated public static final String REQUEST_NETWORK_SCORES = "android.permission.REQUEST_NETWORK_SCORES";
field public static final String REQUEST_NOTIFICATION_ASSISTANT_SERVICE = "android.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE";
+ field @FlaggedApi("android.service.voice.flags.allow_training_data_egress_from_hds") public static final String RESET_HOTWORD_TRAINING_DATA_EGRESS_COUNT = "android.permission.RESET_HOTWORD_TRAINING_DATA_EGRESS_COUNT";
field public static final String RESET_PASSWORD = "android.permission.RESET_PASSWORD";
field public static final String RESTART_WIFI_SUBSYSTEM = "android.permission.RESTART_WIFI_SUBSYSTEM";
field public static final String RESTORE_RUNTIME_PERMISSIONS = "android.permission.RESTORE_RUNTIME_PERMISSIONS";
@@ -12688,6 +12689,7 @@
public static final class HotwordDetectionService.Callback {
method public void onDetected(@NonNull android.service.voice.HotwordDetectedResult);
method public void onRejected(@NonNull android.service.voice.HotwordRejectedResult);
+ method @FlaggedApi("android.service.voice.flags.allow_training_data_egress_from_hds") public void onTrainingData(@NonNull android.service.voice.HotwordTrainingData);
}
public final class HotwordDetectionServiceFailure implements android.os.Parcelable {
@@ -12703,6 +12705,8 @@
field public static final int ERROR_CODE_DETECT_TIMEOUT = 4; // 0x4
field public static final int ERROR_CODE_ON_DETECTED_SECURITY_EXCEPTION = 5; // 0x5
field public static final int ERROR_CODE_ON_DETECTED_STREAM_COPY_FAILURE = 6; // 0x6
+ field @FlaggedApi("android.service.voice.flags.allow_training_data_egress_from_hds") public static final int ERROR_CODE_ON_TRAINING_DATA_EGRESS_LIMIT_EXCEEDED = 8; // 0x8
+ field @FlaggedApi("android.service.voice.flags.allow_training_data_egress_from_hds") public static final int ERROR_CODE_ON_TRAINING_DATA_SECURITY_EXCEPTION = 9; // 0x9
field public static final int ERROR_CODE_REMOTE_EXCEPTION = 7; // 0x7
field public static final int ERROR_CODE_UNKNOWN = 0; // 0x0
}
@@ -12724,6 +12728,7 @@
method public void onRecognitionPaused();
method public void onRecognitionResumed();
method public void onRejected(@NonNull android.service.voice.HotwordRejectedResult);
+ method @FlaggedApi("android.service.voice.flags.allow_training_data_egress_from_hds") public default void onTrainingData(@NonNull android.service.voice.HotwordTrainingData);
method public default void onUnknownFailure(@NonNull String);
}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 384b957..cae802f 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3047,6 +3047,7 @@
method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_HOTWORD_DETECTION) public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetectorForTest(@NonNull String, @NonNull java.util.Locale, @NonNull android.hardware.soundtrigger.SoundTrigger.ModuleProperties, @NonNull java.util.concurrent.Executor, @NonNull android.service.voice.AlwaysOnHotwordDetector.Callback);
method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_HOTWORD_DETECTION) public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetectorForTest(@NonNull String, @NonNull java.util.Locale, @Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull android.hardware.soundtrigger.SoundTrigger.ModuleProperties, @NonNull java.util.concurrent.Executor, @NonNull android.service.voice.AlwaysOnHotwordDetector.Callback);
method @NonNull public final java.util.List<android.hardware.soundtrigger.SoundTrigger.ModuleProperties> listModuleProperties();
+ method @FlaggedApi("android.service.voice.flags.allow_training_data_egress_from_hds") @RequiresPermission(android.Manifest.permission.RESET_HOTWORD_TRAINING_DATA_EGRESS_COUNT) public final void resetHotwordTrainingDataEgressCountForTest();
method public final void setTestModuleForAlwaysOnHotwordDetectorEnabled(boolean);
}
diff --git a/core/java/android/service/voice/AbstractDetector.java b/core/java/android/service/voice/AbstractDetector.java
index db97d4f..dfb1361 100644
--- a/core/java/android/service/voice/AbstractDetector.java
+++ b/core/java/android/service/voice/AbstractDetector.java
@@ -263,5 +263,12 @@
result != null ? result : new HotwordRejectedResult.Builder().build());
}));
}
+
+ @Override
+ public void onTrainingData(HotwordTrainingData data) {
+ Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> {
+ mCallback.onTrainingData(data);
+ }));
+ }
}
}
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index 6a82f6d..875031f 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -306,6 +306,7 @@
private static final int MSG_DETECTION_HOTWORD_DETECTION_SERVICE_FAILURE = 9;
private static final int MSG_DETECTION_SOUND_TRIGGER_FAILURE = 10;
private static final int MSG_DETECTION_UNKNOWN_FAILURE = 11;
+ private static final int MSG_HOTWORD_TRAINING_DATA = 12;
private final String mText;
private final Locale mLocale;
@@ -1653,6 +1654,16 @@
}
@Override
+ public void onTrainingData(@NonNull HotwordTrainingData data) {
+ if (DBG) {
+ Slog.d(TAG, "onTrainingData(" + data + ")");
+ } else {
+ Slog.i(TAG, "onTrainingData");
+ }
+ Message.obtain(mHandler, MSG_HOTWORD_TRAINING_DATA, data).sendToTarget();
+ }
+
+ @Override
public void onHotwordDetectionServiceFailure(
HotwordDetectionServiceFailure hotwordDetectionServiceFailure) {
Slog.v(TAG, "onHotwordDetectionServiceFailure: " + hotwordDetectionServiceFailure);
@@ -1783,6 +1794,9 @@
case MSG_DETECTION_UNKNOWN_FAILURE:
mExternalCallback.onUnknownFailure((String) message.obj);
break;
+ case MSG_HOTWORD_TRAINING_DATA:
+ mExternalCallback.onTrainingData((HotwordTrainingData) message.obj);
+ break;
default:
super.handleMessage(message);
}
diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java
index ccf8b67..13b6a9a 100644
--- a/core/java/android/service/voice/HotwordDetectionService.java
+++ b/core/java/android/service/voice/HotwordDetectionService.java
@@ -19,6 +19,7 @@
import static java.util.Objects.requireNonNull;
import android.annotation.DurationMillisLong;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -39,6 +40,7 @@
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.SharedMemory;
+import android.service.voice.flags.Flags;
import android.speech.IRecognitionServiceManager;
import android.util.Log;
import android.view.contentcapture.ContentCaptureManager;
@@ -443,5 +445,30 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Informs the {@link HotwordDetector} when there is training data.
+ *
+ * <p> A daily limit of 20 is enforced on training data events sent. Number events egressed
+ * are tracked across UTC day (24-hour window) and count is reset at midnight
+ * (UTC 00:00:00). To be informed of failures to egress training data due to limit being
+ * reached, the associated hotword detector should listen for
+ * {@link HotwordDetectionServiceFailure#ERROR_CODE_ON_TRAINING_DATA_EGRESS_LIMIT_EXCEEDED}
+ * events in {@link HotwordDetector.Callback#onFailure(HotwordDetectionServiceFailure)}.
+ *
+ * @param data Training data determined by the service. This is provided to the
+ * {@link HotwordDetector}.
+ */
+ @FlaggedApi(Flags.FLAG_ALLOW_TRAINING_DATA_EGRESS_FROM_HDS)
+ public void onTrainingData(@NonNull HotwordTrainingData data) {
+ requireNonNull(data);
+ try {
+ Log.d(TAG, "onTrainingData");
+ mRemoteCallback.onTrainingData(data);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
}
}
diff --git a/core/java/android/service/voice/HotwordDetectionServiceFailure.java b/core/java/android/service/voice/HotwordDetectionServiceFailure.java
index 5cf245d..420dac1 100644
--- a/core/java/android/service/voice/HotwordDetectionServiceFailure.java
+++ b/core/java/android/service/voice/HotwordDetectionServiceFailure.java
@@ -16,12 +16,14 @@
package android.service.voice;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
+import android.service.voice.flags.Flags;
import android.text.TextUtils;
import java.lang.annotation.Retention;
@@ -79,6 +81,14 @@
*/
public static final int ERROR_CODE_REMOTE_EXCEPTION = 7;
+ /** Indicates failure to egress training data due to limit being exceeded. */
+ @FlaggedApi(Flags.FLAG_ALLOW_TRAINING_DATA_EGRESS_FROM_HDS)
+ public static final int ERROR_CODE_ON_TRAINING_DATA_EGRESS_LIMIT_EXCEEDED = 8;
+
+ /** Indicates failure to egress training data due to security exception. */
+ @FlaggedApi(Flags.FLAG_ALLOW_TRAINING_DATA_EGRESS_FROM_HDS)
+ public static final int ERROR_CODE_ON_TRAINING_DATA_SECURITY_EXCEPTION = 9;
+
/**
* @hide
*/
diff --git a/core/java/android/service/voice/HotwordDetector.java b/core/java/android/service/voice/HotwordDetector.java
index 32a93ee..16a6dbe 100644
--- a/core/java/android/service/voice/HotwordDetector.java
+++ b/core/java/android/service/voice/HotwordDetector.java
@@ -19,6 +19,7 @@
import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
import static android.Manifest.permission.RECORD_AUDIO;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -27,6 +28,7 @@
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.SharedMemory;
+import android.service.voice.flags.Flags;
import java.io.PrintWriter;
@@ -244,6 +246,19 @@
void onRejected(@NonNull HotwordRejectedResult result);
/**
+ * Called by the {@link HotwordDetectionService} to egress training data to the
+ * {@link HotwordDetector}. This data can be used for improving and analyzing hotword
+ * detection models.
+ *
+ * @param data Training data to be egressed provided by the
+ * {@link HotwordDetectionService}.
+ */
+ @FlaggedApi(Flags.FLAG_ALLOW_TRAINING_DATA_EGRESS_FROM_HDS)
+ default void onTrainingData(@NonNull HotwordTrainingData data) {
+ return;
+ }
+
+ /**
* Called when the {@link HotwordDetectionService} or {@link VisualQueryDetectionService} is
* created by the system and given a short amount of time to report their initialization
* state.
diff --git a/core/java/android/service/voice/HotwordTrainingDataLimitEnforcer.java b/core/java/android/service/voice/HotwordTrainingDataLimitEnforcer.java
new file mode 100644
index 0000000..76e506c
--- /dev/null
+++ b/core/java/android/service/voice/HotwordTrainingDataLimitEnforcer.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2023 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.service.voice;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Environment;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Enforces daily limits on the egress of {@link HotwordTrainingData} from the hotword detection
+ * service.
+ *
+ * <p> Egress is tracked across UTC day (24-hour window) and count is reset at
+ * midnight (UTC 00:00:00).
+ *
+ * @hide
+ */
+public class HotwordTrainingDataLimitEnforcer {
+ private static final String TAG = "HotwordTrainingDataLimitEnforcer";
+
+ /**
+ * Number of hotword training data events that are allowed to be egressed per day.
+ */
+ private static final int TRAINING_DATA_EGRESS_LIMIT = 20;
+
+ /**
+ * Name of hotword training data limit shared preference.
+ */
+ private static final String TRAINING_DATA_LIMIT_SHARED_PREF = "TrainingDataSharedPref";
+
+ /**
+ * Key for date associated with
+ * {@link HotwordTrainingDataLimitEnforcer#TRAINING_DATA_EGRESS_COUNT}.
+ */
+ private static final String TRAINING_DATA_EGRESS_DATE = "TRAINING_DATA_EGRESS_DATE";
+
+ /**
+ * Key for number of hotword training data events egressed on
+ * {@link HotwordTrainingDataLimitEnforcer#TRAINING_DATA_EGRESS_DATE}.
+ */
+ private static final String TRAINING_DATA_EGRESS_COUNT = "TRAINING_DATA_EGRESS_COUNT";
+
+ private SharedPreferences mSharedPreferences;
+
+ private static final Object INSTANCE_LOCK = new Object();
+ private final Object mTrainingDataIncrementLock = new Object();
+
+ private static HotwordTrainingDataLimitEnforcer sInstance;
+
+ /** Get singleton HotwordTrainingDataLimitEnforcer instance. */
+ public static @NonNull HotwordTrainingDataLimitEnforcer getInstance(@NonNull Context context) {
+ synchronized (INSTANCE_LOCK) {
+ if (sInstance == null) {
+ sInstance = new HotwordTrainingDataLimitEnforcer(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+ }
+
+ private HotwordTrainingDataLimitEnforcer(Context context) {
+ mSharedPreferences = context.getSharedPreferences(
+ new File(Environment.getDataSystemCeDirectory(UserHandle.USER_SYSTEM),
+ TRAINING_DATA_LIMIT_SHARED_PREF),
+ Context.MODE_PRIVATE);
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public void resetTrainingDataEgressCount() {
+ Log.i(TAG, "Resetting training data egress count!");
+ synchronized (mTrainingDataIncrementLock) {
+ // Clear all training data shared preferences.
+ mSharedPreferences.edit().clear().commit();
+ }
+ }
+
+ /**
+ * Increments training data egress count.
+ * <p> If count exceeds daily training data egress limit, returns false. Else, will return true.
+ */
+ public boolean incrementEgressCount() {
+ synchronized (mTrainingDataIncrementLock) {
+ return incrementTrainingDataEgressCountLocked();
+ }
+ }
+
+ private boolean incrementTrainingDataEgressCountLocked() {
+ SimpleDateFormat dt = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
+ dt.setTimeZone(TimeZone.getTimeZone("UTC"));
+ String currentDate = dt.format(new Date());
+
+ String storedDate = mSharedPreferences.getString(TRAINING_DATA_EGRESS_DATE, "");
+ int storedCount = mSharedPreferences.getInt(TRAINING_DATA_EGRESS_COUNT, 0);
+ Log.i(TAG,
+ TextUtils.formatSimple("There are %s hotword training data events egressed for %s",
+ storedCount, storedDate));
+
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+
+ // If date has not changed from last training data event, increment counter if within
+ // limit.
+ if (storedDate.equals(currentDate)) {
+ if (storedCount < TRAINING_DATA_EGRESS_LIMIT) {
+ Log.i(TAG, "Within hotword training data egress limit, incrementing...");
+ editor.putInt(TRAINING_DATA_EGRESS_COUNT, storedCount + 1);
+ editor.commit();
+ return true;
+ }
+ Log.i(TAG, "Exceeded hotword training data egress limit.");
+ return false;
+ }
+
+ // If date has changed, reset.
+ Log.i(TAG, TextUtils.formatSimple(
+ "Stored date %s is different from current data %s. Resetting counters...",
+ storedDate, currentDate));
+
+ editor.putString(TRAINING_DATA_EGRESS_DATE, currentDate);
+ editor.putInt(TRAINING_DATA_EGRESS_COUNT, 1);
+ editor.commit();
+ return true;
+ }
+}
diff --git a/core/java/android/service/voice/IDspHotwordDetectionCallback.aidl b/core/java/android/service/voice/IDspHotwordDetectionCallback.aidl
index c6b10ff..a9c6af79 100644
--- a/core/java/android/service/voice/IDspHotwordDetectionCallback.aidl
+++ b/core/java/android/service/voice/IDspHotwordDetectionCallback.aidl
@@ -18,6 +18,7 @@
import android.service.voice.HotwordDetectedResult;
import android.service.voice.HotwordRejectedResult;
+import android.service.voice.HotwordTrainingData;
/**
* Callback for returning the detected result from the HotwordDetectionService.
@@ -37,4 +38,10 @@
* Sends {@code result} to the HotwordDetector.
*/
void onRejected(in HotwordRejectedResult result);
+
+ /**
+ * Called by {@link HotwordDetectionService} to egress training data to the
+ * {@link HotwordDetector}.
+ */
+ void onTrainingData(in HotwordTrainingData data);
}
diff --git a/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl b/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl
index fab830a..6226772 100644
--- a/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl
+++ b/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl
@@ -20,6 +20,7 @@
import android.service.voice.HotwordDetectedResult;
import android.service.voice.HotwordDetectionServiceFailure;
import android.service.voice.HotwordRejectedResult;
+import android.service.voice.HotwordTrainingData;
/**
* Callback for returning the detected result from the HotwordDetectionService.
@@ -47,4 +48,10 @@
*/
void onRejected(
in HotwordRejectedResult hotwordRejectedResult);
+
+ /**
+ * Called by {@link HotwordDetectionService} to egress training data to the
+ * {@link HotwordDetector}.
+ */
+ void onTrainingData(in HotwordTrainingData data);
}
diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java
index f1bc792..2c68fae 100644
--- a/core/java/android/service/voice/SoftwareHotwordDetector.java
+++ b/core/java/android/service/voice/SoftwareHotwordDetector.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.RECORD_AUDIO;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.hardware.soundtrigger.SoundTrigger;
@@ -201,6 +202,13 @@
result != null ? result : new HotwordRejectedResult.Builder().build());
}));
}
+
+ @Override
+ public void onTrainingData(@NonNull HotwordTrainingData result) {
+ Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> {
+ mCallback.onTrainingData(result);
+ }));
+ }
}
private static class InitializationStateListener
@@ -238,6 +246,13 @@
}
@Override
+ public void onTrainingData(@NonNull HotwordTrainingData data) {
+ if (DEBUG) {
+ Slog.i(TAG, "Ignored #onTrainingData event");
+ }
+ }
+
+ @Override
public void onHotwordDetectionServiceFailure(
HotwordDetectionServiceFailure hotwordDetectionServiceFailure)
throws RemoteException {
diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java
index b5448d4..91de894 100644
--- a/core/java/android/service/voice/VisualQueryDetector.java
+++ b/core/java/android/service/voice/VisualQueryDetector.java
@@ -369,6 +369,12 @@
Slog.i(TAG, "Ignored #onRejected event");
}
}
+ @Override
+ public void onTrainingData(HotwordTrainingData data) throws RemoteException {
+ if (DEBUG) {
+ Slog.i(TAG, "Ignored #onTrainingData event");
+ }
+ }
@Override
public void onRecognitionPaused() throws RemoteException {
diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java
index d280621..42203d4 100644
--- a/core/java/android/service/voice/VoiceInteractionService.java
+++ b/core/java/android/service/voice/VoiceInteractionService.java
@@ -18,6 +18,7 @@
import android.Manifest;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -48,6 +49,7 @@
import android.os.SharedMemory;
import android.os.SystemProperties;
import android.provider.Settings;
+import android.service.voice.flags.Flags;
import android.util.ArraySet;
import android.util.Log;
@@ -443,6 +445,20 @@
}
}
+ /** Reset hotword training data egressed count.
+ * @hide */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_ALLOW_TRAINING_DATA_EGRESS_FROM_HDS)
+ @RequiresPermission(Manifest.permission.RESET_HOTWORD_TRAINING_DATA_EGRESS_COUNT)
+ public final void resetHotwordTrainingDataEgressCountForTest() {
+ Log.i(TAG, "Resetting hotword training data egress count for test.");
+ try {
+ mSystemService.resetHotwordTrainingDataEgressCountForTest();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/**
* Creates an {@link AlwaysOnHotwordDetector} for the given keyphrase and locale.
* This instance must be retained and used by the client.
diff --git a/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl b/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
index ba87caa..a65877c 100644
--- a/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
+++ b/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
@@ -20,6 +20,7 @@
import android.service.voice.HotwordDetectedResult;
import android.service.voice.HotwordDetectionServiceFailure;
import android.service.voice.HotwordRejectedResult;
+import android.service.voice.HotwordTrainingData;
import android.service.voice.SoundTriggerFailure;
import android.service.voice.VisualQueryDetectionServiceFailure;
import com.android.internal.infra.AndroidFuture;
@@ -59,6 +60,12 @@
void onRejected(in HotwordRejectedResult result);
/**
+ * Called by {@link HotwordDetectionService} to egress training data to the
+ * {@link HotwordDetector}.
+ */
+ void onTrainingData(in HotwordTrainingData data);
+
+ /**
* Called when the detection fails due to an error occurs in the
* {@link HotwordDetectionService}.
*
diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
index 314ed69..68e2b48 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
@@ -359,6 +359,12 @@
in IHotwordRecognitionStatusCallback callback);
/**
+ * Test API to reset training data egress count for test.
+ */
+ @EnforcePermission("RESET_HOTWORD_TRAINING_DATA_EGRESS_COUNT")
+ void resetHotwordTrainingDataEgressCountForTest();
+
+ /**
* Starts to listen the status of visible activity.
*/
void startListeningVisibleActivityChanged(in IBinder token);
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b73a765..db57479 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7767,6 +7767,14 @@
<permission android:name="android.permission.MANAGE_DISPLAYS"
android:protectionLevel="signature" />
+ <!-- @SystemApi Allows apps to reset hotword training data egress count for testing.
+ <p>CTS tests will use UiAutomation.AdoptShellPermissionIdentity() to gain access.
+ <p>Protection level: signature
+ @FlaggedApi("android.service.voice.flags.allow_training_data_egress_from_hds")
+ @hide -->
+ <permission android:name="android.permission.RESET_HOTWORD_TRAINING_DATA_EGRESS_COUNT"
+ android:protectionLevel="signature" />
+
<!-- Attribution for Geofencing service. -->
<attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
<!-- Attribution for Country Detector. -->
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 11ae9c3..10d04d3 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -860,6 +860,10 @@
<!-- Permission required for CTS test - CtsWallpaperTestCases -->
<uses-permission android:name="android.permission.ALWAYS_UPDATE_WALLPAPER" />
+ <!-- Permissions required for CTS test - CtsVoiceInteractionTestCases -->
+ <uses-permission android:name="android.permission.RESET_HOTWORD_TRAINING_DATA_EGRESS_COUNT" />
+ <uses-permission android:name="android.permission.RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA" />
+
<application
android:label="@string/app_label"
android:theme="@android:style/Theme.DeviceDefault.DayNight"
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
index 93b5a40..f6c6a64 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
@@ -19,6 +19,7 @@
import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
import static android.Manifest.permission.LOG_COMPAT_CHANGE;
import static android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG;
+import static android.Manifest.permission.RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_DEFAULT;
@@ -29,6 +30,8 @@
import static android.service.voice.HotwordDetectionService.INITIALIZATION_STATUS_UNKNOWN;
import static android.service.voice.HotwordDetectionService.KEY_INITIALIZATION_STATUS;
import static android.service.voice.HotwordDetectionServiceFailure.ERROR_CODE_COPY_AUDIO_DATA_FAILURE;
+import static android.service.voice.HotwordDetectionServiceFailure.ERROR_CODE_ON_TRAINING_DATA_EGRESS_LIMIT_EXCEEDED;
+import static android.service.voice.HotwordDetectionServiceFailure.ERROR_CODE_ON_TRAINING_DATA_SECURITY_EXCEPTION;
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_ERROR;
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_SUCCESS;
@@ -75,6 +78,8 @@
import android.service.voice.HotwordDetectionServiceFailure;
import android.service.voice.HotwordDetector;
import android.service.voice.HotwordRejectedResult;
+import android.service.voice.HotwordTrainingData;
+import android.service.voice.HotwordTrainingDataLimitEnforcer;
import android.service.voice.IDspHotwordDetectionCallback;
import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
import android.service.voice.VisualQueryDetectionServiceFailure;
@@ -127,6 +132,9 @@
private static final String HOTWORD_DETECTION_OP_MESSAGE =
"Providing hotword detection result to VoiceInteractionService";
+ private static final String HOTWORD_TRAINING_DATA_OP_MESSAGE =
+ "Providing hotword training data to VoiceInteractionService";
+
// The error codes are used for onHotwordDetectionServiceFailure callback.
// Define these due to lines longer than 100 characters.
static final int ONDETECTED_GOT_SECURITY_EXCEPTION =
@@ -512,6 +520,25 @@
}
@Override
+ public void onTrainingData(HotwordTrainingData data)
+ throws RemoteException {
+ sendTrainingData(new TrainingDataEgressCallback() {
+ @Override
+ public void onHotwordDetectionServiceFailure(
+ HotwordDetectionServiceFailure failure)
+ throws RemoteException {
+ callback.onHotwordDetectionServiceFailure(failure);
+ }
+
+ @Override
+ public void onTrainingData(HotwordTrainingData data)
+ throws RemoteException {
+ callback.onTrainingData(data);
+ }
+ }, data);
+ }
+
+ @Override
public void onDetected(HotwordDetectedResult triggerResult)
throws RemoteException {
synchronized (mLock) {
@@ -593,6 +620,82 @@
mVoiceInteractionServiceUid);
}
+ /** Used to send training data.
+ *
+ * @hide
+ */
+ interface TrainingDataEgressCallback {
+ /** Called to send training data */
+ void onTrainingData(HotwordTrainingData trainingData) throws RemoteException;
+
+ /** Called to inform failure to send training data. */
+ void onHotwordDetectionServiceFailure(HotwordDetectionServiceFailure failure) throws
+ RemoteException;
+
+ }
+
+ /** Default implementation to send training data from {@link HotwordDetectionService}
+ * to {@link HotwordDetector}.
+ *
+ * <p> Verifies RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA permission has been
+ * granted and training data egress is within daily limit.
+ *
+ * @param callback used to send training data or inform of failures to send training data.
+ * @param data training data to egress.
+ *
+ * @hide
+ */
+ void sendTrainingData(
+ TrainingDataEgressCallback callback, HotwordTrainingData data) throws RemoteException {
+ Slog.d(TAG, "onTrainingData()");
+
+ // Check training data permission is granted.
+ try {
+ enforcePermissionForTrainingDataDelivery();
+ } catch (SecurityException e) {
+ Slog.w(TAG, "Ignoring training data due to a SecurityException", e);
+ try {
+ callback.onHotwordDetectionServiceFailure(
+ new HotwordDetectionServiceFailure(
+ ERROR_CODE_ON_TRAINING_DATA_SECURITY_EXCEPTION,
+ "Security exception occurred"
+ + "in #onTrainingData method."));
+ } catch (RemoteException e1) {
+ notifyOnDetectorRemoteException();
+ throw e1;
+ }
+ return;
+ }
+
+ // Check whether within daily egress limit.
+ boolean withinEgressLimit = HotwordTrainingDataLimitEnforcer.getInstance(mContext)
+ .incrementEgressCount();
+ if (!withinEgressLimit) {
+ Slog.d(TAG, "Ignoring training data as exceeded egress limit.");
+ try {
+ callback.onHotwordDetectionServiceFailure(
+ new HotwordDetectionServiceFailure(
+ ERROR_CODE_ON_TRAINING_DATA_EGRESS_LIMIT_EXCEEDED,
+ "Training data egress limit exceeded."));
+ } catch (RemoteException e) {
+ notifyOnDetectorRemoteException();
+ throw e;
+ }
+ return;
+ }
+
+ try {
+ Slog.i(TAG, "Egressing training data from hotword trusted process.");
+ if (mDebugHotwordLogging) {
+ Slog.d(TAG, "Egressing hotword training data " + data);
+ }
+ callback.onTrainingData(data);
+ } catch (RemoteException e) {
+ notifyOnDetectorRemoteException();
+ throw e;
+ }
+ }
+
void initialize(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) {
synchronized (mLock) {
if (mInitialized || mDestroyed) {
@@ -781,6 +884,27 @@
}
/**
+ * Enforces permission for training data delivery.
+ *
+ * <p> Throws a {@link SecurityException} if training data egress permission is not granted.
+ */
+ void enforcePermissionForTrainingDataDelivery() {
+ Binder.withCleanCallingIdentity(() -> {
+ synchronized (mLock) {
+ enforcePermissionForDataDelivery(mContext, mVoiceInteractorIdentity,
+ RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA,
+ HOTWORD_TRAINING_DATA_OP_MESSAGE);
+
+ mAppOpsManager.noteOpNoThrow(
+ AppOpsManager.OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA,
+ mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
+ mVoiceInteractorIdentity.attributionTag,
+ HOTWORD_TRAINING_DATA_OP_MESSAGE);
+ }
+ });
+ }
+
+ /**
* Throws a {@link SecurityException} if the given identity has no permission to receive data.
*
* @param context A {@link Context}, used for permission checks.
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
index 9a4fbdc..6418f3e 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
@@ -42,6 +42,7 @@
import android.service.voice.HotwordDetectionServiceFailure;
import android.service.voice.HotwordDetector;
import android.service.voice.HotwordRejectedResult;
+import android.service.voice.HotwordTrainingData;
import android.service.voice.IDspHotwordDetectionCallback;
import android.util.Slog;
@@ -229,6 +230,23 @@
}
}
}
+
+ @Override
+ public void onTrainingData(HotwordTrainingData data) throws RemoteException {
+ sendTrainingData(new TrainingDataEgressCallback() {
+ @Override
+ public void onHotwordDetectionServiceFailure(
+ HotwordDetectionServiceFailure failure) throws RemoteException {
+ externalCallback.onHotwordDetectionServiceFailure(failure);
+ }
+
+ @Override
+ public void onTrainingData(HotwordTrainingData data)
+ throws RemoteException {
+ externalCallback.onTrainingData(data);
+ }
+ }, data);
+ }
};
mValidatingDspTrigger = true;
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 0a70a5f..b098e82 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -214,7 +214,6 @@
new ServiceConnectionFactory(visualQueryDetectionServiceIntent,
bindInstantServiceAllowed, DETECTION_SERVICE_TYPE_VISUAL_QUERY);
-
mLastRestartInstant = Instant.now();
if (mReStartPeriodSeconds <= 0) {
@@ -918,7 +917,8 @@
session = new SoftwareTrustedHotwordDetectorSession(
mRemoteHotwordDetectionService, mLock, mContext, token, callback,
mVoiceInteractionServiceUid, mVoiceInteractorIdentity,
- mScheduledExecutorService, mDebugHotwordLogging, mRemoteExceptionListener);
+ mScheduledExecutorService, mDebugHotwordLogging,
+ mRemoteExceptionListener);
}
mHotwordRecognitionCallback = callback;
mDetectorSessions.put(detectorType, session);
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
index f06c997..2e23eff 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
@@ -40,6 +40,7 @@
import android.service.voice.HotwordDetectionServiceFailure;
import android.service.voice.HotwordDetector;
import android.service.voice.HotwordRejectedResult;
+import android.service.voice.HotwordTrainingData;
import android.service.voice.IDspHotwordDetectionCallback;
import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
import android.service.voice.ISandboxedDetectionService;
@@ -195,6 +196,21 @@
mVoiceInteractionServiceUid);
// onRejected isn't allowed here, and we are not expecting it.
}
+
+ public void onTrainingData(HotwordTrainingData data) throws RemoteException {
+ sendTrainingData(new TrainingDataEgressCallback() {
+ @Override
+ public void onHotwordDetectionServiceFailure(
+ HotwordDetectionServiceFailure failure) throws RemoteException {
+ mSoftwareCallback.onHotwordDetectionServiceFailure(failure);
+ }
+
+ @Override
+ public void onTrainingData(HotwordTrainingData data) throws RemoteException {
+ mSoftwareCallback.onTrainingData(data);
+ }
+ }, data);
+ }
};
mRemoteDetectionService.run(
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 1c689d0..a584fc9 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -1541,7 +1541,31 @@
}
}
}
- //----------------- Model management APIs --------------------------------//
+
+ @Override
+ @android.annotation.EnforcePermission(
+ android.Manifest.permission.RESET_HOTWORD_TRAINING_DATA_EGRESS_COUNT)
+ public void resetHotwordTrainingDataEgressCountForTest() {
+ super.resetHotwordTrainingDataEgressCountForTest_enforcePermission();
+ synchronized (this) {
+ enforceIsCurrentVoiceInteractionService();
+
+ if (mImpl == null) {
+ Slog.w(TAG, "resetHotwordTrainingDataEgressCountForTest without running"
+ + " voice interaction service");
+ return;
+ }
+ final long caller = Binder.clearCallingIdentity();
+ try {
+ mImpl.resetHotwordTrainingDataEgressCountForTest();
+ } finally {
+ Binder.restoreCallingIdentity(caller);
+ }
+
+ }
+ }
+
+ //----------------- Model management APIs --------------------------------//
@Override
public KeyphraseSoundModel getKeyphraseSoundModel(int keyphraseId, String bcp47Locale) {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 6ba77da..3c4b58f 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -60,6 +60,7 @@
import android.os.SystemProperties;
import android.os.UserHandle;
import android.service.voice.HotwordDetector;
+import android.service.voice.HotwordTrainingDataLimitEnforcer;
import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
import android.service.voice.IVisualQueryDetectionVoiceInteractionCallback;
import android.service.voice.IVoiceInteractionService;
@@ -72,6 +73,7 @@
import android.util.Slog;
import android.view.IWindowManager;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IHotwordRecognitionStatusCallback;
import com.android.internal.app.IVisualQueryDetectionAttentionListener;
import com.android.internal.app.IVoiceActionCheckCallback;
@@ -991,6 +993,12 @@
}
}
+ @VisibleForTesting
+ void resetHotwordTrainingDataEgressCountForTest() {
+ HotwordTrainingDataLimitEnforcer.getInstance(mContext.getApplicationContext())
+ .resetTrainingDataEgressCount();
+ }
+
void startLocked() {
Intent intent = new Intent(VoiceInteractionService.SERVICE_INTERFACE);
intent.setComponent(mComponent);