diff options
| author | 2023-10-24 22:52:26 +0000 | |
|---|---|---|
| committer | 2023-10-24 22:52:26 +0000 | |
| commit | 8bf88f2361f9b7ca62f75b4981a94966eb6be1c4 (patch) | |
| tree | 3a8b2430d6d34aeb9bb2124f69c8c94a5b74557c | |
| parent | bce1ce8af3ae2f3bbdff6eb978545f465e933d1b (diff) | |
| parent | 33109c19896591c9a700374e520baa5b68936277 (diff) | |
Merge "Expand HDS API to allow egressing training data" into main
23 files changed, 497 insertions, 3 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 1a7810adc5b4..836a0166f3ce 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -315,6 +315,7 @@ package android { 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 @@ package android.service.voice { 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 @@ package android.service.voice { 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 @@ package android.service.voice { 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 779777c7658a..a0b6fb742d19 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3047,6 +3047,7 @@ package android.service.voice { 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 db97d4f52643..dfb1361efec1 100644 --- a/core/java/android/service/voice/AbstractDetector.java +++ b/core/java/android/service/voice/AbstractDetector.java @@ -263,5 +263,12 @@ abstract class AbstractDetector implements HotwordDetector { 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 6a82f6da67b3..875031fb0cb3 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -306,6 +306,7 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { 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 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { } @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 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { 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 ccf8b67826c8..13b6a9a79535 100644 --- a/core/java/android/service/voice/HotwordDetectionService.java +++ b/core/java/android/service/voice/HotwordDetectionService.java @@ -19,6 +19,7 @@ package android.service.voice; 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.ParcelFileDescriptor; 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 @@ public abstract class HotwordDetectionService extends Service 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 5cf245d8624b..420dac185cb4 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 final class HotwordDetectionServiceFailure implements Parcelable { */ 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 32a93eef7fda..16a6dbe2956b 100644 --- a/core/java/android/service/voice/HotwordDetector.java +++ b/core/java/android/service/voice/HotwordDetector.java @@ -19,6 +19,7 @@ package android.service.voice; 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.media.AudioFormat; 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 @@ public interface HotwordDetector { 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 000000000000..76e506cc6728 --- /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 c6b10ff05b08..a9c6af79a0e1 100644 --- a/core/java/android/service/voice/IDspHotwordDetectionCallback.aidl +++ b/core/java/android/service/voice/IDspHotwordDetectionCallback.aidl @@ -18,6 +18,7 @@ package android.service.voice; 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 @@ oneway interface IDspHotwordDetectionCallback { * 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 fab830af9d48..62267729f144 100644 --- a/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl +++ b/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl @@ -20,6 +20,7 @@ import android.media.AudioFormat; 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 @@ oneway interface IMicrophoneHotwordDetectionVoiceInteractionCallback { */ 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 f1bc792696d6..2c68faea5141 100644 --- a/core/java/android/service/voice/SoftwareHotwordDetector.java +++ b/core/java/android/service/voice/SoftwareHotwordDetector.java @@ -18,6 +18,7 @@ package android.service.voice; 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 @@ class SoftwareHotwordDetector extends AbstractDetector { 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 @@ class SoftwareHotwordDetector extends AbstractDetector { } @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 b5448d4374b2..91de894c1d93 100644 --- a/core/java/android/service/voice/VisualQueryDetector.java +++ b/core/java/android/service/voice/VisualQueryDetector.java @@ -369,6 +369,12 @@ public class VisualQueryDetector { 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 d2806217a276..42203d4b0d71 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -18,6 +18,7 @@ package android.service.voice; 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.ServiceManager; 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 @@ public class VoiceInteractionService extends Service { } } + /** 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 ba87caa0697c..a65877c7a951 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.hardware.soundtrigger.SoundTrigger; 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 @@ oneway interface IHotwordRecognitionStatusCallback { 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 314ed69cb885..68e2b48c8f08 100644 --- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl +++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl @@ -359,6 +359,12 @@ interface IVoiceInteractionManagerService { 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 6daa5b934284..9c0ba68800f7 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7768,6 +7768,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 11ae9c35898b..10d04d3ff6b3 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 93b5a40a5fc0..f6c6a64ba764 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 @@ package com.android.server.voiceinteraction; 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_STATU 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.HotwordDetectionService; 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 @@ abstract class DetectorSession { 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 @@ abstract class DetectorSession { } @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 @@ abstract class DetectorSession { 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 @@ abstract class DetectorSession { } /** + * 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 9a4fbdc4516a..6418f3e83114 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.HotwordDetectionService; 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 @@ final class DspTrustedHotwordDetectorSession extends DetectorSession { } } } + + @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 0a70a5f9b947..b098e828fb5f 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 @@ final class HotwordDetectionConnection { new ServiceConnectionFactory(visualQueryDetectionServiceIntent, bindInstantServiceAllowed, DETECTION_SERVICE_TYPE_VISUAL_QUERY); - mLastRestartInstant = Instant.now(); if (mReStartPeriodSeconds <= 0) { @@ -918,7 +917,8 @@ final class HotwordDetectionConnection { 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 f06c99729a19..2e23eff7a179 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.HotwordDetectionService; 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 @@ final class SoftwareTrustedHotwordDetectorSession extends DetectorSession { 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 1c689d0d5ce3..a584fc9b2216 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 @@ public class VoiceInteractionManagerService extends SystemService { } } } - //----------------- 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 6ba77da1d972..3c4b58fa2b69 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.SharedMemory; 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.PrintWriterPrinter; 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 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } } + @VisibleForTesting + void resetHotwordTrainingDataEgressCountForTest() { + HotwordTrainingDataLimitEnforcer.getInstance(mContext.getApplicationContext()) + .resetTrainingDataEgressCount(); + } + void startLocked() { Intent intent = new Intent(VoiceInteractionService.SERVICE_INTERFACE); intent.setComponent(mComponent); |