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);