summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt17
-rw-r--r--core/api/system-current.txt8
-rw-r--r--core/api/test-current.txt1
-rw-r--r--core/java/android/app/Activity.java6
-rw-r--r--core/java/android/app/Dialog.java16
-rw-r--r--core/java/android/app/wearable/IWearableSensingManager.aidl5
-rw-r--r--core/java/android/app/wearable/WearableSensingManager.java86
-rw-r--r--core/java/android/security/flags.aconfig3
-rw-r--r--core/java/android/service/voice/AlwaysOnHotwordDetector.java74
-rw-r--r--core/java/android/service/voice/HotwordDetectionService.java16
-rw-r--r--core/java/android/service/voice/SoftwareHotwordDetector.java7
-rw-r--r--core/java/android/service/voice/VisualQueryDetector.java7
-rw-r--r--core/java/android/service/voice/VoiceInteractionManagerInternal.java42
-rw-r--r--core/java/android/service/wearable/IWearableSensingService.aidl4
-rw-r--r--core/java/android/service/wearable/WearableSensingService.java149
-rw-r--r--core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl7
-rw-r--r--core/tests/BroadcastRadioTests/Android.bp1
-rw-r--r--keystore/java/android/security/keystore/KeyGenParameterSpec.java6
-rw-r--r--keystore/java/android/security/keystore/KeyProtection.java6
-rw-r--r--keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java2
-rw-r--r--keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java2
-rw-r--r--media/java/android/media/MediaCodec.java139
-rw-r--r--media/java/android/media/MediaCodecInfo.java15
-rw-r--r--media/jni/android_media_MediaCodec.cpp211
-rw-r--r--media/jni/android_media_MediaCodec.h16
-rw-r--r--native/android/input.cpp17
-rw-r--r--native/android/libandroid.map.txt1
-rw-r--r--services/core/java/com/android/server/notification/GroupHelper.java57
-rwxr-xr-xservices/core/java/com/android/server/notification/NotificationManagerService.java14
-rw-r--r--services/core/java/com/android/server/wearable/RemoteWearableSensingService.java49
-rw-r--r--services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java122
-rw-r--r--services/core/java/com/android/server/wearable/WearableSensingManagerService.java36
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java115
-rwxr-xr-xservices/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java19
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java142
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java20
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java47
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java19
38 files changed, 1368 insertions, 136 deletions
diff --git a/core/api/current.txt b/core/api/current.txt
index 391867d8e7a5..5e8ccdb27f72 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -22497,6 +22497,7 @@ package android.media {
method @NonNull public static android.view.Surface createPersistentInputSurface();
method public int dequeueInputBuffer(long);
method public int dequeueOutputBuffer(@NonNull android.media.MediaCodec.BufferInfo, long);
+ method @FlaggedApi("android.media.codec.null_output_surface") public void detachOutputSurface();
method protected void finalize();
method public void flush();
method @NonNull public String getCanonicalName();
@@ -22520,6 +22521,7 @@ package android.media {
method public void queueInputBuffer(int, int, int, long, int) throws android.media.MediaCodec.CryptoException;
method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") public void queueInputBuffers(int, @NonNull java.util.ArrayDeque<android.media.MediaCodec.BufferInfo>);
method public void queueSecureInputBuffer(int, int, @NonNull android.media.MediaCodec.CryptoInfo, long, int) throws android.media.MediaCodec.CryptoException;
+ method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") public void queueSecureInputBuffers(int, @NonNull java.util.ArrayDeque<android.media.MediaCodec.BufferInfo>, @NonNull java.util.ArrayDeque<android.media.MediaCodec.CryptoInfo>);
method public void release();
method public void releaseOutputBuffer(int, boolean);
method public void releaseOutputBuffer(int, long);
@@ -22544,6 +22546,7 @@ package android.media {
field public static final int BUFFER_FLAG_KEY_FRAME = 1; // 0x1
field public static final int BUFFER_FLAG_PARTIAL_FRAME = 8; // 0x8
field @Deprecated public static final int BUFFER_FLAG_SYNC_FRAME = 1; // 0x1
+ field @FlaggedApi("android.media.codec.null_output_surface") public static final int CONFIGURE_FLAG_DETACHED_SURFACE = 8; // 0x8
field public static final int CONFIGURE_FLAG_ENCODE = 1; // 0x1
field public static final int CONFIGURE_FLAG_USE_BLOCK_MODEL = 2; // 0x2
field public static final int CONFIGURE_FLAG_USE_CRYPTO_ASYNC = 4; // 0x4
@@ -22695,6 +22698,7 @@ package android.media {
method @NonNull public android.media.MediaCodec.QueueRequest setIntegerParameter(@NonNull String, int);
method @NonNull public android.media.MediaCodec.QueueRequest setLinearBlock(@NonNull android.media.MediaCodec.LinearBlock, int, int);
method @NonNull public android.media.MediaCodec.QueueRequest setLongParameter(@NonNull String, long);
+ method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") @NonNull public android.media.MediaCodec.QueueRequest setMultiFrameEncryptedLinearBlock(@NonNull android.media.MediaCodec.LinearBlock, @NonNull java.util.ArrayDeque<android.media.MediaCodec.BufferInfo>, @NonNull java.util.ArrayDeque<android.media.MediaCodec.CryptoInfo>);
method @NonNull public android.media.MediaCodec.QueueRequest setPresentationTimeUs(long);
method @NonNull public android.media.MediaCodec.QueueRequest setStringParameter(@NonNull String, @NonNull String);
}
@@ -22789,6 +22793,7 @@ package android.media {
field @Deprecated public static final int COLOR_QCOM_FormatYUV420SemiPlanar = 2141391872; // 0x7fa30c00
field @Deprecated public static final int COLOR_TI_FormatYUV420PackedSemiPlanar = 2130706688; // 0x7f000100
field public static final String FEATURE_AdaptivePlayback = "adaptive-playback";
+ field @FlaggedApi("android.media.codec.null_output_surface") public static final String FEATURE_DetachedSurface = "detached-surface";
field @FlaggedApi("android.media.codec.dynamic_color_aspects") public static final String FEATURE_DynamicColorAspects = "dynamic-color-aspects";
field public static final String FEATURE_DynamicTimestamp = "dynamic-timestamp";
field public static final String FEATURE_EncodingStatistics = "encoding-statistics";
@@ -39649,7 +39654,7 @@ package android.security.keystore {
method @Nullable public java.util.Date getKeyValidityStart();
method @NonNull public String getKeystoreAlias();
method public int getMaxUsageCount();
- method @FlaggedApi("android.security.mgf1_digest_setter") @NonNull public java.util.Set<java.lang.String> getMgf1Digests();
+ method @FlaggedApi("android.security.mgf1_digest_setter_v2") @NonNull public java.util.Set<java.lang.String> getMgf1Digests();
method public int getPurposes();
method @NonNull public String[] getSignaturePaddings();
method public int getUserAuthenticationType();
@@ -39657,7 +39662,7 @@ package android.security.keystore {
method public boolean isDevicePropertiesAttestationIncluded();
method @NonNull public boolean isDigestsSpecified();
method public boolean isInvalidatedByBiometricEnrollment();
- method @FlaggedApi("android.security.mgf1_digest_setter") @NonNull public boolean isMgf1DigestsSpecified();
+ method @FlaggedApi("android.security.mgf1_digest_setter_v2") @NonNull public boolean isMgf1DigestsSpecified();
method public boolean isRandomizedEncryptionRequired();
method public boolean isStrongBoxBacked();
method public boolean isUnlockedDeviceRequired();
@@ -39689,7 +39694,7 @@ package android.security.keystore {
method @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setKeyValidityForOriginationEnd(java.util.Date);
method @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setKeyValidityStart(java.util.Date);
method @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setMaxUsageCount(int);
- method @FlaggedApi("android.security.mgf1_digest_setter") @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setMgf1Digests(@NonNull java.lang.String...);
+ method @FlaggedApi("android.security.mgf1_digest_setter_v2") @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setMgf1Digests(@NonNull java.lang.String...);
method @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setRandomizedEncryptionRequired(boolean);
method @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setSignaturePaddings(java.lang.String...);
method @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setUnlockedDeviceRequired(boolean);
@@ -39794,14 +39799,14 @@ package android.security.keystore {
method @Nullable public java.util.Date getKeyValidityForOriginationEnd();
method @Nullable public java.util.Date getKeyValidityStart();
method public int getMaxUsageCount();
- method @FlaggedApi("android.security.mgf1_digest_setter") @NonNull public java.util.Set<java.lang.String> getMgf1Digests();
+ method @FlaggedApi("android.security.mgf1_digest_setter_v2") @NonNull public java.util.Set<java.lang.String> getMgf1Digests();
method public int getPurposes();
method @NonNull public String[] getSignaturePaddings();
method public int getUserAuthenticationType();
method public int getUserAuthenticationValidityDurationSeconds();
method public boolean isDigestsSpecified();
method public boolean isInvalidatedByBiometricEnrollment();
- method @FlaggedApi("android.security.mgf1_digest_setter") @NonNull public boolean isMgf1DigestsSpecified();
+ method @FlaggedApi("android.security.mgf1_digest_setter_v2") @NonNull public boolean isMgf1DigestsSpecified();
method public boolean isRandomizedEncryptionRequired();
method public boolean isUnlockedDeviceRequired();
method public boolean isUserAuthenticationRequired();
@@ -39823,7 +39828,7 @@ package android.security.keystore {
method @NonNull public android.security.keystore.KeyProtection.Builder setKeyValidityForOriginationEnd(java.util.Date);
method @NonNull public android.security.keystore.KeyProtection.Builder setKeyValidityStart(java.util.Date);
method @NonNull public android.security.keystore.KeyProtection.Builder setMaxUsageCount(int);
- method @FlaggedApi("android.security.mgf1_digest_setter") @NonNull public android.security.keystore.KeyProtection.Builder setMgf1Digests(@Nullable java.lang.String...);
+ method @FlaggedApi("android.security.mgf1_digest_setter_v2") @NonNull public android.security.keystore.KeyProtection.Builder setMgf1Digests(@Nullable java.lang.String...);
method @NonNull public android.security.keystore.KeyProtection.Builder setRandomizedEncryptionRequired(boolean);
method @NonNull public android.security.keystore.KeyProtection.Builder setSignaturePaddings(java.lang.String...);
method @NonNull public android.security.keystore.KeyProtection.Builder setUnlockedDeviceRequired(boolean);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 4719561089d2..e86c5129496a 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3193,6 +3193,8 @@ package android.app.wearable {
method @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void provideDataStream(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void provideWearableConnection(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @FlaggedApi("android.app.wearable.enable_data_request_observer_api") @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void registerDataRequestObserver(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("android.app.wearable.enable_hotword_wearable_sensing_api") @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void startHotwordRecognition(@Nullable android.content.ComponentName, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("android.app.wearable.enable_hotword_wearable_sensing_api") @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void stopHotwordRecognition(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @FlaggedApi("android.app.wearable.enable_data_request_observer_api") @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void unregisterDataRequestObserver(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
field public static final int STATUS_ACCESS_DENIED = 5; // 0x5
field @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") public static final int STATUS_CHANNEL_ERROR = 7; // 0x7
@@ -13190,6 +13192,7 @@ package android.service.voice {
method @Nullable public android.service.voice.HotwordDetectedResult getHotwordDetectedResult();
method @NonNull public java.util.List<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra> getKeyphraseRecognitionExtras();
method @Deprecated @Nullable public byte[] getTriggerAudio();
+ method @FlaggedApi("android.app.wearable.enable_hotword_wearable_sensing_api") public boolean isRecognitionStopped();
field public static final int DATA_FORMAT_RAW = 0; // 0x0
field public static final int DATA_FORMAT_TRIGGER_AUDIO = 1; // 0x1
}
@@ -13296,6 +13299,7 @@ package android.service.voice {
method public void onUpdateState(@Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory, long, @Nullable java.util.function.IntConsumer);
field @Deprecated public static final int INITIALIZATION_STATUS_SUCCESS = 0; // 0x0
field @Deprecated public static final int INITIALIZATION_STATUS_UNKNOWN = 100; // 0x64
+ field @FlaggedApi("android.app.wearable.enable_hotword_wearable_sensing_api") public static final String KEY_SYSTEM_WILL_CLOSE_AUDIO_STREAM_AFTER_CALLBACK = "android.service.voice.HotwordDetectionService.KEY_SYSTEM_WILL_CLOSE_AUDIO_STREAM_AFTER_CALLBACK";
field public static final String SERVICE_INTERFACE = "android.service.voice.HotwordDetectionService";
}
@@ -13580,7 +13584,11 @@ package android.service.wearable {
method @BinderThread public abstract void onQueryServiceStatus(@NonNull java.util.Set<java.lang.Integer>, @NonNull String, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionServiceStatus>);
method @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") @BinderThread public void onSecureWearableConnectionProvided(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @BinderThread public abstract void onStartDetection(@NonNull android.app.ambientcontext.AmbientContextEventRequest, @NonNull String, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionServiceStatus>, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionResult>);
+ method @FlaggedApi("android.app.wearable.enable_hotword_wearable_sensing_api") @BinderThread public void onStartHotwordRecognition(@NonNull java.util.function.Consumer<android.service.voice.HotwordAudioStream>, @NonNull java.util.function.Consumer<java.lang.Integer>);
method public abstract void onStopDetection(@NonNull String);
+ method @FlaggedApi("android.app.wearable.enable_hotword_wearable_sensing_api") @BinderThread public void onStopHotwordAudioStream();
+ method @FlaggedApi("android.app.wearable.enable_hotword_wearable_sensing_api") @BinderThread public void onStopHotwordRecognition(@NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("android.app.wearable.enable_hotword_wearable_sensing_api") @BinderThread public void onValidatedByHotwordDetectionService();
field public static final String SERVICE_INTERFACE = "android.service.wearable.WearableSensingService";
}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index cd84c8455439..fa232faf07d0 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3172,6 +3172,7 @@ package android.service.voice {
method @NonNull public android.service.voice.AlwaysOnHotwordDetector.EventPayload.Builder setDataFormat(int);
method @NonNull public android.service.voice.AlwaysOnHotwordDetector.EventPayload.Builder setHalEventReceivedMillis(long);
method @NonNull public android.service.voice.AlwaysOnHotwordDetector.EventPayload.Builder setHotwordDetectedResult(@NonNull android.service.voice.HotwordDetectedResult);
+ method @FlaggedApi("android.app.wearable.enable_hotword_wearable_sensing_api") @NonNull public android.service.voice.AlwaysOnHotwordDetector.EventPayload.Builder setIsRecognitionStopped(boolean);
method @NonNull public android.service.voice.AlwaysOnHotwordDetector.EventPayload.Builder setKeyphraseRecognitionExtras(@NonNull java.util.List<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra>);
}
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index ab9a4ecd8506..23fe731701b6 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -3927,6 +3927,7 @@ public class Activity extends ContextThemeWrapper
if (keyCode == KeyEvent.KEYCODE_ESCAPE && mWindow.shouldCloseOnTouchOutside()) {
event.startTracking();
+ finish();
return true;
}
@@ -4027,10 +4028,7 @@ public class Activity extends ContextThemeWrapper
}
if (keyCode == KeyEvent.KEYCODE_ESCAPE
- && mWindow.shouldCloseOnTouchOutside()
- && event.isTracking()
- && !event.isCanceled()) {
- finish();
+ && event.isTracking()) {
return true;
}
diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java
index d0d76a4c8285..0e201384812d 100644
--- a/core/java/android/app/Dialog.java
+++ b/core/java/android/app/Dialog.java
@@ -672,7 +672,16 @@ public class Dialog implements DialogInterface, Window.Callback,
*/
@Override
public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ event.startTracking();
+ return true;
+ }
+ if (keyCode == KeyEvent.KEYCODE_ESCAPE) {
+ if (mCancelable) {
+ cancel();
+ } else {
+ dismiss();
+ }
event.startTracking();
return true;
}
@@ -712,11 +721,6 @@ public class Dialog implements DialogInterface, Window.Callback,
}
break;
case KeyEvent.KEYCODE_ESCAPE:
- if (mCancelable) {
- cancel();
- } else {
- dismiss();
- }
return true;
}
}
diff --git a/core/java/android/app/wearable/IWearableSensingManager.aidl b/core/java/android/app/wearable/IWearableSensingManager.aidl
index 3cbc8a21d2d8..f67802279e26 100644
--- a/core/java/android/app/wearable/IWearableSensingManager.aidl
+++ b/core/java/android/app/wearable/IWearableSensingManager.aidl
@@ -17,6 +17,7 @@
package android.app.wearable;
import android.app.PendingIntent;
+import android.content.ComponentName;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteCallback;
@@ -38,4 +39,8 @@ interface IWearableSensingManager {
void registerDataRequestObserver(int dataType, in PendingIntent dataRequestPendingIntent, in RemoteCallback statusCallback);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
void unregisterDataRequestObserver(int dataType, in PendingIntent dataRequestPendingIntent, in RemoteCallback statusCallback);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
+ void startHotwordRecognition(in ComponentName targetVisComponentName, in RemoteCallback statusCallback);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
+ void stopHotwordRecognition(in RemoteCallback statusCallback);
} \ No newline at end of file
diff --git a/core/java/android/app/wearable/WearableSensingManager.java b/core/java/android/app/wearable/WearableSensingManager.java
index 3b281e9c1ae2..637f6776bd1b 100644
--- a/core/java/android/app/wearable/WearableSensingManager.java
+++ b/core/java/android/app/wearable/WearableSensingManager.java
@@ -28,6 +28,7 @@ import android.annotation.SystemService;
import android.app.PendingIntent;
import android.app.ambientcontext.AmbientContextEvent;
import android.companion.CompanionDeviceManager;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
@@ -92,9 +93,13 @@ public class WearableSensingManager {
public static final int STATUS_SUCCESS = 1;
/**
- * The value of the status code that indicates one or more of the
- * requested events are not supported.
+ * The value of the status code that indicates one or more of the requested events are not
+ * supported.
*/
+ // TODO(b/324635656): Deprecate this status code. Update Javadoc:
+ // @deprecated WearableSensingManager does not deal with events. Use {@link
+ // STATUS_UNSUPPORTED_OPERATION} instead for operations not supported by the implementation of
+ // {@link WearableSensingService}.
public static final int STATUS_UNSUPPORTED = 2;
/**
@@ -382,6 +387,83 @@ public class WearableSensingManager {
}
}
+ /**
+ * Requests the wearable to start hotword recognition.
+ *
+ * <p>When this method is called, the system will attempt to provide a {@link
+ * android.service.wearable.WearableHotwordAudioConsumer} to {@link WearableSensingService}.
+ * After first-stage hotword is detected on a wearable, {@link WearableSensingService} should
+ * send the hotword audio to the {@link android.service.wearable.WearableHotwordAudioConsumer},
+ * which will forward the data to the {@link android.service.voice.HotwordDetectionService} for
+ * second-stage hotword validation. If hotword is detected there, the audio data will be
+ * forwarded to the {@link android.service.voice.VoiceInteractionService}.
+ *
+ * <p>If the {@code targetVisComponentName} provided here is not null, when {@link
+ * WearableSensingService} sends hotword audio to the {@link
+ * android.service.wearable.WearableHotwordAudioConsumer}, the system will check whether the
+ * {@link android.service.voice.VoiceInteractionService} at that time is {@code
+ * targetVisComponentName}. If not, the system will call {@link
+ * WearableSensingService#onActiveHotwordAudioStopRequested()} and will not forward the audio
+ * data to the current {@link android.service.voice.HotwordDetectionService} nor {@link
+ * android.service.voice.VoiceInteractionService}. The system will not send a status code to
+ * {@code statusConsumer} regarding the {@code targetVisComponentName} check. The caller is
+ * responsible for determining whether the system's {@link
+ * android.service.voice.VoiceInteractionService} is the same as {@code targetVisComponentName}.
+ * The check here is just a protection against race conditions.
+ *
+ * <p>Calling this method again will send a new {@link
+ * android.service.wearable.WearableHotwordAudioConsumer} to {@link WearableSensingService}. For
+ * audio data sent to the new consumer, the system will perform the above check using the newly
+ * provided {@code targetVisComponentName}. The {@link WearableSensingService} should not
+ * continue to use the previous consumers after receiving a new one.
+ *
+ * <p>If the {@code statusConsumer} returns {@link STATUS_SUCCESS}, the caller should call
+ * {@link #stopListeningForHotword(Executor, Consumer)} when it wants the wearable to stop
+ * listening for hotword. If the {@code statusConsumer} returns any other status code, a failure
+ * has occurred and calling {@link #stopListeningForHotword(Executor, Consumer)} is not
+ * required. The system will not retry listening automatically. The caller should call this
+ * method again if they want to retry.
+ *
+ * <p>If a failure occurred after the {@link statusConsumer} returns {@link STATUS_SUCCESS},
+ * {@link statusConsumer} will be invoked again with a status code other than {@link
+ * STATUS_SUCCESS}.
+ *
+ * @param targetVisComponentName The ComponentName of the target VoiceInteractionService.
+ * @param executor Executor on which to run the consumer callback.
+ * @param statusConsumer A consumer that handles the status codes.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
+ @RequiresPermission(Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)
+ public void startHotwordRecognition(
+ @Nullable ComponentName targetVisComponentName,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull @StatusCode Consumer<Integer> statusConsumer) {
+ try {
+ mService.startHotwordRecognition(
+ targetVisComponentName, createStatusCallback(executor, statusConsumer));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Requests the wearable to stop hotword recognition.
+ *
+ * @param executor Executor on which to run the consumer callback.
+ * @param statusConsumer A consumer that handles the status codes.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
+ @RequiresPermission(Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)
+ public void stopHotwordRecognition(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull @StatusCode Consumer<Integer> statusConsumer) {
+ try {
+ mService.stopHotwordRecognition(createStatusCallback(executor, statusConsumer));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
private static RemoteCallback createStatusCallback(
Executor executor, Consumer<Integer> statusConsumer) {
return new RemoteCallback(
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index 43163b3b9051..76314546b4f0 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -15,10 +15,11 @@ flag {
}
flag {
- name: "mgf1_digest_setter"
+ name: "mgf1_digest_setter_v2"
namespace: "hardware_backed_security"
description: "Feature flag for mgf1 digest setter in key generation and import parameters."
bug: "308378912"
+ is_fixed_read_only: true
}
flag {
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index 94d851603064..a08264e625df 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -22,6 +22,7 @@ import static android.service.voice.SoundTriggerFailure.ERROR_CODE_UNKNOWN;
import static android.service.voice.VoiceInteractionService.MULTIPLE_ACTIVE_HOTWORD_DETECTORS;
import android.annotation.ElapsedRealtimeLong;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -432,7 +433,10 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
@ElapsedRealtimeLong
private final long mHalEventReceivedMillis;
- private EventPayload(boolean captureAvailable,
+ private final boolean mIsRecognitionStopped;
+
+ private EventPayload(
+ boolean captureAvailable,
@Nullable AudioFormat audioFormat,
int captureSession,
@DataFormat int dataFormat,
@@ -440,7 +444,8 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
@Nullable HotwordDetectedResult hotwordDetectedResult,
@Nullable ParcelFileDescriptor audioStream,
@NonNull List<KeyphraseRecognitionExtra> keyphraseExtras,
- @ElapsedRealtimeLong long halEventReceivedMillis) {
+ @ElapsedRealtimeLong long halEventReceivedMillis,
+ boolean isRecognitionStopped) {
mCaptureAvailable = captureAvailable;
mCaptureSession = captureSession;
mAudioFormat = audioFormat;
@@ -450,6 +455,7 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
mAudioStream = audioStream;
mKephraseExtras = keyphraseExtras;
mHalEventReceivedMillis = halEventReceivedMillis;
+ mIsRecognitionStopped = isRecognitionStopped;
}
/**
@@ -592,6 +598,12 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
return mHalEventReceivedMillis;
}
+ /** Returns whether the system has stopped hotword recognition because of this detection. */
+ @FlaggedApi(android.app.wearable.Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
+ public boolean isRecognitionStopped() {
+ return mIsRecognitionStopped;
+ }
+
/**
* Builder class for {@link EventPayload} objects
*
@@ -610,6 +622,8 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
private List<KeyphraseRecognitionExtra> mKeyphraseExtras = Collections.emptyList();
@ElapsedRealtimeLong
private long mHalEventReceivedMillis = -1;
+ // default to true to keep prior behavior
+ private boolean mIsRecognitionStopped = true;
public Builder() {}
@@ -746,13 +760,31 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
}
/**
+ * Sets whether the system has stopped hotword recognition because of this detection.
+ */
+ @FlaggedApi(android.app.wearable.Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
+ @NonNull
+ public Builder setIsRecognitionStopped(boolean isRecognitionStopped) {
+ mIsRecognitionStopped = isRecognitionStopped;
+ return this;
+ }
+
+ /**
* Builds an {@link EventPayload} instance
*/
@NonNull
public EventPayload build() {
- return new EventPayload(mCaptureAvailable, mAudioFormat, mCaptureSession,
- mDataFormat, mData, mHotwordDetectedResult, mAudioStream,
- mKeyphraseExtras, mHalEventReceivedMillis);
+ return new EventPayload(
+ mCaptureAvailable,
+ mAudioFormat,
+ mCaptureSession,
+ mDataFormat,
+ mData,
+ mHotwordDetectedResult,
+ mAudioStream,
+ mKeyphraseExtras,
+ mHalEventReceivedMillis,
+ mIsRecognitionStopped);
}
}
}
@@ -786,14 +818,20 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
/**
* Called when the keyphrase is spoken.
- * This implicitly stops listening for the keyphrase once it's detected.
- * Clients should start a recognition again once they are done handling this
- * detection.
*
- * @param eventPayload Payload data for the detection event.
- * This may contain the trigger audio, if requested when calling
- * {@link AlwaysOnHotwordDetector#startRecognition(int)}.
+ * <p>This implicitly stops listening for the keyphrase once it's detected. Clients should
+ * start a recognition again once they are done handling this detection.
+ *
+ * @param eventPayload Payload data for the detection event. This may contain the trigger
+ * audio, if requested when calling {@link
+ * AlwaysOnHotwordDetector#startRecognition(int)}.
*/
+ // TODO(b/324635656): Update Javadoc for 24Q3 release:
+ // 1. Prepend to the first paragraph:
+ // If {@code eventPayload.isRecognitionStopped()} returns true, this...
+ // 2. Append to the description for @param eventPayload:
+ // ...or if the audio comes from {@link
+ // android.service.wearable.WearableSensingService}.
public abstract void onDetected(@NonNull EventPayload eventPayload);
/**
@@ -1632,6 +1670,20 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
}
@Override
+ public void onKeyphraseDetectedFromExternalSource(HotwordDetectedResult result) {
+ Slog.i(TAG, "onKeyphraseDetectedFromExternalSource");
+ EventPayload.Builder eventPayloadBuilder = new EventPayload.Builder();
+ if (android.app.wearable.Flags.enableHotwordWearableSensingApi()) {
+ eventPayloadBuilder.setIsRecognitionStopped(false);
+ }
+ Message.obtain(
+ mHandler,
+ MSG_HOTWORD_DETECTED,
+ eventPayloadBuilder.setHotwordDetectedResult(result).build())
+ .sendToTarget();
+ }
+
+ @Override
public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) {
Slog.w(TAG, "Generic sound trigger event detected at AOHD: " + event);
}
diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java
index ccf8b67826c8..60e9de72f154 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;
@@ -79,6 +80,16 @@ public abstract class HotwordDetectionService extends Service
private static final long UPDATE_TIMEOUT_MILLIS = 20000;
/**
+ * The PersistableBundle options key used in {@link #onDetect(ParcelFileDescriptor, AudioFormat,
+ * PersistableBundle, Callback)} to indicate whether the system will close the audio stream
+ * after {@code Callback} is invoked.
+ */
+ @FlaggedApi(android.app.wearable.Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
+ public static final String KEY_SYSTEM_WILL_CLOSE_AUDIO_STREAM_AFTER_CALLBACK =
+ "android.service.voice.HotwordDetectionService."
+ + "KEY_SYSTEM_WILL_CLOSE_AUDIO_STREAM_AFTER_CALLBACK";
+
+ /**
* Feature flag for Attention Service.
*
* @hide
@@ -364,6 +375,11 @@ public abstract class HotwordDetectionService extends Service
* PersistableBundle)}.
* @param callback The callback to use for responding to the detection request.
*/
+ // TODO(b/324635656): Update Javadoc for 24Q3 release. Change the last paragraph to:
+ // <p>Upon invoking the {@code callback}, the system will send the detection result to
+ // the {@link HotwordDetector}'s callback. If {@code
+ // options.getBoolean(KEY_SYSTEM_WILL_CLOSE_AUDIO_STREAM_AFTER_CALLBACK, true)} returns true,
+ // the system will also close the {@code audioStream} after {@code callback} is invoked.
public void onDetect(
@NonNull ParcelFileDescriptor audioStream,
@NonNull AudioFormat audioFormat,
diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java
index f1bc792696d6..a835b0f57998 100644
--- a/core/java/android/service/voice/SoftwareHotwordDetector.java
+++ b/core/java/android/service/voice/SoftwareHotwordDetector.java
@@ -223,6 +223,13 @@ class SoftwareHotwordDetector extends AbstractDetector {
}
@Override
+ public void onKeyphraseDetectedFromExternalSource(HotwordDetectedResult result) {
+ if (DEBUG) {
+ Slog.i(TAG, "Ignored #onKeyphraseDetectedFromExternalSource event");
+ }
+ }
+
+ @Override
public void onGenericSoundTriggerDetected(
SoundTrigger.GenericRecognitionEvent recognitionEvent) throws RemoteException {
if (DEBUG) {
diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java
index 23847fe76ecc..1eb4d6730e1e 100644
--- a/core/java/android/service/voice/VisualQueryDetector.java
+++ b/core/java/android/service/voice/VisualQueryDetector.java
@@ -414,6 +414,13 @@ public class VisualQueryDetector {
}
@Override
+ public void onKeyphraseDetectedFromExternalSource(HotwordDetectedResult result) {
+ if (DEBUG) {
+ Slog.i(TAG, "Ignored #onKeyphraseDetectedFromExternalSource event");
+ }
+ }
+
+ @Override
public void onGenericSoundTriggerDetected(
SoundTrigger.GenericRecognitionEvent recognitionEvent) throws RemoteException {
if (DEBUG) {
diff --git a/core/java/android/service/voice/VoiceInteractionManagerInternal.java b/core/java/android/service/voice/VoiceInteractionManagerInternal.java
index 270f848598d7..7d2773387c1a 100644
--- a/core/java/android/service/voice/VoiceInteractionManagerInternal.java
+++ b/core/java/android/service/voice/VoiceInteractionManagerInternal.java
@@ -19,14 +19,17 @@ package android.service.voice;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.content.ComponentName;
+import android.media.AudioFormat;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
import com.android.internal.annotations.Immutable;
/**
- * @hide
- * Private interface to the VoiceInteractionManagerService for use by ActivityManagerService.
+ * @hide Private interface to the VoiceInteractionManagerService for use within system_server.
*/
public abstract class VoiceInteractionManagerInternal {
@@ -77,6 +80,25 @@ public abstract class VoiceInteractionManagerInternal {
public abstract void onPreCreatedUserConversion(@UserIdInt int userId);
/**
+ * Called by {@link com.android.server.wearable.WearableSensingManagerPerUserService} when a
+ * wearable starts sending audio data for hotword detection.
+ *
+ * @param audioStream The audio data.
+ * @param audioFormat The format of the audio data.
+ * @param options Options supporting hotword detection.
+ * @param targetVisComponentName The target VoiceInteractionService ComponentName
+ * @param userId The user ID of the calling wearable service
+ * @param callback The callback to notify the caller of the hotword detection result.
+ */
+ public abstract void startListeningFromWearable(
+ ParcelFileDescriptor audioStream,
+ AudioFormat audioFormat,
+ PersistableBundle options,
+ ComponentName targetVisComponentName,
+ int userId,
+ WearableHotwordDetectionCallback callback);
+
+ /**
* Provides the uids of the currently active
* {@link android.service.voice.HotwordDetectionService} and its owning package. The
* HotwordDetectionService is an isolated service, so it has a separate uid.
@@ -101,4 +123,20 @@ public abstract class VoiceInteractionManagerInternal {
return mOwnerUid;
}
}
+
+ /**
+ * Callback for returning the detected hotword result to the wearable.
+ *
+ * @hide
+ */
+ public interface WearableHotwordDetectionCallback {
+ /** Called when hotword is detected. */
+ void onDetected();
+
+ /** Called when hotword is not detected. */
+ void onRejected();
+
+ /** Called when an unexpected error occurs. */
+ void onError(String errorMessage);
+ }
}
diff --git a/core/java/android/service/wearable/IWearableSensingService.aidl b/core/java/android/service/wearable/IWearableSensingService.aidl
index f67dcff3ebfe..22d8fda9cb8e 100644
--- a/core/java/android/service/wearable/IWearableSensingService.aidl
+++ b/core/java/android/service/wearable/IWearableSensingService.aidl
@@ -33,6 +33,10 @@ oneway interface IWearableSensingService {
void provideData(in PersistableBundle data, in SharedMemory sharedMemory, in RemoteCallback callback);
void registerDataRequestObserver(int dataType, in RemoteCallback dataRequestCallback, int dataRequestObserverId, in String packageName, in RemoteCallback statusCallback);
void unregisterDataRequestObserver(int dataType, int dataRequestObserverId, in String packageName, in RemoteCallback statusCallback);
+ void startHotwordRecognition(in RemoteCallback wearableHotwordCallback, in RemoteCallback statusCallback);
+ void stopHotwordRecognition(in RemoteCallback statusCallback);
+ void onValidatedByHotwordDetectionService();
+ void stopActiveHotwordAudio();
void startDetection(in AmbientContextEventRequest request, in String packageName,
in RemoteCallback detectionResultCallback, in RemoteCallback statusCallback);
void stopDetection(in String packageName);
diff --git a/core/java/android/service/wearable/WearableSensingService.java b/core/java/android/service/wearable/WearableSensingService.java
index bb6e030456a7..808c3ae7b6bc 100644
--- a/core/java/android/service/wearable/WearableSensingService.java
+++ b/core/java/android/service/wearable/WearableSensingService.java
@@ -37,6 +37,7 @@ import android.os.RemoteCallback;
import android.os.SharedMemory;
import android.service.ambientcontext.AmbientContextDetectionResult;
import android.service.ambientcontext.AmbientContextDetectionServiceStatus;
+import android.service.voice.HotwordAudioStream;
import android.util.Slog;
import android.util.SparseArray;
@@ -85,6 +86,14 @@ public abstract class WearableSensingService extends Service {
"android.app.wearable.WearableSensingStatusBundleKey";
/**
+ * The bundle key for hotword audio stream, used in {@code RemoteCallback#sendResult}.
+ *
+ * @hide
+ */
+ public static final String HOTWORD_AUDIO_STREAM_BUNDLE_KEY =
+ "android.app.wearable.HotwordAudioStreamBundleKey";
+
+ /**
* The {@link Intent} that must be declared as handled by the service. To be supported, the
* service must also require the
* {@link android.Manifest.permission#BIND_WEARABLE_SENSING_SERVICE}
@@ -181,6 +190,50 @@ public abstract class WearableSensingService extends Service {
dataType, packageName, dataRequester, statusConsumer);
}
+ @Override
+ public void startHotwordRecognition(
+ RemoteCallback wearableHotwordCallback, RemoteCallback statusCallback) {
+ Consumer<HotwordAudioStream> hotwordAudioConsumer =
+ (hotwordAudioStream) -> {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(
+ HOTWORD_AUDIO_STREAM_BUNDLE_KEY, hotwordAudioStream);
+ wearableHotwordCallback.sendResult(bundle);
+ };
+ Consumer<Integer> statusConsumer =
+ response -> {
+ Bundle bundle = new Bundle();
+ bundle.putInt(STATUS_RESPONSE_BUNDLE_KEY, response);
+ statusCallback.sendResult(bundle);
+ };
+ WearableSensingService.this.onStartHotwordRecognition(
+ hotwordAudioConsumer, statusConsumer);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void stopHotwordRecognition(RemoteCallback statusCallback) {
+ Consumer<Integer> statusConsumer =
+ response -> {
+ Bundle bundle = new Bundle();
+ bundle.putInt(STATUS_RESPONSE_BUNDLE_KEY, response);
+ statusCallback.sendResult(bundle);
+ };
+ WearableSensingService.this.onStopHotwordRecognition(statusConsumer);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onValidatedByHotwordDetectionService() {
+ WearableSensingService.this.onValidatedByHotwordDetectionService();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void stopActiveHotwordAudio() {
+ WearableSensingService.this.onStopHotwordAudioStream();
+ }
+
/** {@inheritDoc} */
@Override
public void startDetection(
@@ -377,6 +430,100 @@ public abstract class WearableSensingService extends Service {
}
/**
+ * Called when the wearable is requested to start hotword recognition.
+ *
+ * <p>This method is expected to be overridden by a derived class. The implementation should
+ * store the {@code hotwordAudioConsumer} and send it the audio data when first-stage hotword is
+ * detected from the wearable. It should also send a {@link
+ * WearableSensingManager#STATUS_SUCCESS} status code to the {@code statusConsumer} unless it
+ * encounters an error condition described by a status code listed in {@link
+ * WearableSensingManager}, such as {@link WearableSensingManager#STATUS_WEARABLE_UNAVAILABLE},
+ * in which case it should return the corresponding status code.
+ *
+ * <p>The implementation should also store the {@code statusConsumer}. If the wearable stops
+ * listening for hotword for any reason other than {@link #onStopListeningForHotword(Consumer)}
+ * being invoked, it should send an appropriate status code listed in {@link
+ * WearableSensingManager} to {@code statusConsumer}. If the error condition cannot be described
+ * by any of those status codes, it should send a {@link WearableSensingManager#STATUS_UNKNOWN}.
+ *
+ * <p>If this method is called again, the implementation should use the new {@code
+ * hotwordAudioConsumer} and discard any previous ones it received.
+ *
+ * <p>At this time, the {@code timestamp} field in the {@link HotwordAudioStream} is not used
+ * and will be discarded by the system.
+ *
+ * @param hotwordAudioConsumer The consumer for the wearable hotword audio data.
+ * @param statusConsumer The consumer for the service status.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
+ @BinderThread
+ public void onStartHotwordRecognition(
+ @NonNull Consumer<HotwordAudioStream> hotwordAudioConsumer,
+ @NonNull Consumer<Integer> statusConsumer) {
+ if (Flags.enableUnsupportedOperationStatusCode()) {
+ statusConsumer.accept(WearableSensingManager.STATUS_UNSUPPORTED_OPERATION);
+ }
+ }
+
+ /**
+ * Called when the wearable is requested to stop hotword recognition.
+ *
+ * <p>This method is expected to be overridden by a derived class. It should send a {@link
+ * WearableSensingManager#STATUS_SUCCESS} status code to the {@code statusConsumer} unless it
+ * encounters an error condition described by a status code listed in {@link
+ * WearableSensingManager}, such as {@link WearableSensingManager#STATUS_WEARABLE_UNAVAILABLE},
+ * in which case it should return the corresponding status code.
+ *
+ * @param statusConsumer The consumer for the service status.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
+ @BinderThread
+ public void onStopHotwordRecognition(@NonNull Consumer<Integer> statusConsumer) {
+ if (Flags.enableUnsupportedOperationStatusCode()) {
+ statusConsumer.accept(WearableSensingManager.STATUS_UNSUPPORTED_OPERATION);
+ }
+ }
+
+ /**
+ * Called when hotword audio data sent to the {@code hotwordAudioConsumer} in {@link
+ * #onStartListeningForHotword(Consumer, Consumer)} is accepted by the
+ * {@link android.service.voice.HotwordDetectionService} as valid hotword.
+ *
+ * <p>After the implementation of this class sends the hotword audio data to the {@code
+ * hotwordAudioConsumer} in {@link #onStartListeningForHotword(Consumer,
+ * Consumer)}, the system will forward the data into {@link
+ * android.service.voice.HotwordDetectionService} (which runs in an isolated process) for
+ * second-stage hotword detection. If accepted as valid hotword there, this method will be
+ * called, and then the system will send the data to the currently active {@link
+ * android.service.voice.AlwaysOnHotwordDetector} (which may not run in an isolated process).
+ *
+ * <p>This method is expected to be overridden by a derived class. The implementation must
+ * request the wearable to turn on the microphone indicator to notify the user that audio data
+ * is being used outside of the isolated environment.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
+ @BinderThread
+ public void onValidatedByHotwordDetectionService() {}
+
+ /**
+ * Called when the currently active hotword audio stream is no longer needed.
+ *
+ * <p>This method can be called as a result of hotword rejection by {@link
+ * android.service.voice.HotwordDetectionService}, or the {@link
+ * android.service.voice.AlwaysOnHotwordDetector} closing the data stream it received, or a
+ * non-recoverable error occurred before the data reaches the {@link
+ * android.service.voice.HotwordDetectionService} or the {@link
+ * android.service.voice.AlwaysOnHotwordDetector}.
+ *
+ * <p>This method is expected to be overridden by a derived class. The implementation should
+ * stop sending hotword audio data to the {@code hotwordAudioConsumer} in {@link
+ * #onStartListeningForHotword(Consumer, Consumer)}
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
+ @BinderThread
+ public void onStopHotwordAudioStream() {}
+
+ /**
* Called when a client app requests starting detection of the events in the request. The
* implementation should keep track of whether the user has explicitly consented to detecting
* the events using on-going ambient sensor (e.g. microphone), and agreed to share the
@@ -460,4 +607,6 @@ public abstract class WearableSensingService extends Service {
statusCallback.sendResult(bundle);
};
}
+
+
}
diff --git a/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl b/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
index ba87caa0697c..5cb5963112a6 100644
--- a/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
+++ b/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
@@ -40,6 +40,13 @@ oneway interface IHotwordRecognitionStatusCallback {
in SoundTrigger.KeyphraseRecognitionEvent recognitionEvent,
in HotwordDetectedResult result);
+ /**
+ * Called when the keyphrase is detected from audio coming from an external source.
+ *
+ * @param result Successful detection result payload.
+ */
+ void onKeyphraseDetectedFromExternalSource(in HotwordDetectedResult result);
+
/**
* Called when a generic sound trigger event is witnessed.
*
diff --git a/core/tests/BroadcastRadioTests/Android.bp b/core/tests/BroadcastRadioTests/Android.bp
index 6be553b99c3c..beffb9aac12b 100644
--- a/core/tests/BroadcastRadioTests/Android.bp
+++ b/core/tests/BroadcastRadioTests/Android.bp
@@ -18,6 +18,7 @@ package {
// all of the 'license_kinds' from "frameworks_base_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
+ default_team: "trendy_team_aaos_framework",
default_applicable_licenses: ["frameworks_base_license"],
}
diff --git a/keystore/java/android/security/keystore/KeyGenParameterSpec.java b/keystore/java/android/security/keystore/KeyGenParameterSpec.java
index 4982f3732089..244fe3033dca 100644
--- a/keystore/java/android/security/keystore/KeyGenParameterSpec.java
+++ b/keystore/java/android/security/keystore/KeyGenParameterSpec.java
@@ -618,7 +618,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu
* @see #isMgf1DigestsSpecified()
*/
@NonNull
- @FlaggedApi(android.security.Flags.FLAG_MGF1_DIGEST_SETTER)
+ @FlaggedApi(android.security.Flags.FLAG_MGF1_DIGEST_SETTER_V2)
public @KeyProperties.DigestEnum Set<String> getMgf1Digests() {
if (mMgf1Digests.isEmpty()) {
throw new IllegalStateException("Mask generation function (MGF) not specified");
@@ -633,7 +633,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu
* @see #getMgf1Digests()
*/
@NonNull
- @FlaggedApi(android.security.Flags.FLAG_MGF1_DIGEST_SETTER)
+ @FlaggedApi(android.security.Flags.FLAG_MGF1_DIGEST_SETTER_V2)
public boolean isMgf1DigestsSpecified() {
return !mMgf1Digests.isEmpty();
}
@@ -1292,7 +1292,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu
* <p>See {@link KeyProperties}.{@code DIGEST} constants.
*/
@NonNull
- @FlaggedApi(android.security.Flags.FLAG_MGF1_DIGEST_SETTER)
+ @FlaggedApi(android.security.Flags.FLAG_MGF1_DIGEST_SETTER_V2)
public Builder setMgf1Digests(@NonNull @KeyProperties.DigestEnum String... mgf1Digests) {
mMgf1Digests = Set.of(mgf1Digests);
return this;
diff --git a/keystore/java/android/security/keystore/KeyProtection.java b/keystore/java/android/security/keystore/KeyProtection.java
index 7b6b2d142f95..2495d1a85864 100644
--- a/keystore/java/android/security/keystore/KeyProtection.java
+++ b/keystore/java/android/security/keystore/KeyProtection.java
@@ -401,7 +401,7 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs {
* @see #isMgf1DigestsSpecified()
*/
@NonNull
- @FlaggedApi(android.security.Flags.FLAG_MGF1_DIGEST_SETTER)
+ @FlaggedApi(android.security.Flags.FLAG_MGF1_DIGEST_SETTER_V2)
public @KeyProperties.DigestEnum Set<String> getMgf1Digests() {
if (mMgf1Digests.isEmpty()) {
throw new IllegalStateException("Mask generation function (MGF) not specified");
@@ -416,7 +416,7 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs {
* @see #getMgf1Digests()
*/
@NonNull
- @FlaggedApi(android.security.Flags.FLAG_MGF1_DIGEST_SETTER)
+ @FlaggedApi(android.security.Flags.FLAG_MGF1_DIGEST_SETTER_V2)
public boolean isMgf1DigestsSpecified() {
return !mMgf1Digests.isEmpty();
}
@@ -799,7 +799,7 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs {
* <p>See {@link KeyProperties}.{@code DIGEST} constants.
*/
@NonNull
- @FlaggedApi(android.security.Flags.FLAG_MGF1_DIGEST_SETTER)
+ @FlaggedApi(android.security.Flags.FLAG_MGF1_DIGEST_SETTER_V2)
public Builder setMgf1Digests(@Nullable @KeyProperties.DigestEnum String... mgf1Digests) {
mMgf1Digests = Set.of(mgf1Digests);
return this;
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
index 83ddfc5cf1a1..e6c652c14c71 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
@@ -974,7 +974,7 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato
private static boolean getMgf1DigestSetterFlag() {
try {
- return Flags.mgf1DigestSetter();
+ return Flags.mgf1DigestSetterV2();
} catch (SecurityException e) {
Log.w(TAG, "Cannot read MGF1 Digest setter flag value", e);
return false;
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
index 2d8c5a380c6b..e6a63b9c4c17 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
@@ -259,7 +259,7 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi {
private static boolean getMgf1DigestSetterFlag() {
try {
- return Flags.mgf1DigestSetter();
+ return Flags.mgf1DigestSetterV2();
} catch (SecurityException e) {
Log.w(NAME, "Cannot read MGF1 Digest setter flag value", e);
return false;
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index 4ee25c4bcf60..ac94a6f6ad3c 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -16,6 +16,7 @@
package android.media;
+import static android.media.codec.Flags.FLAG_NULL_OUTPUT_SURFACE;
import static android.media.codec.Flags.FLAG_REGION_OF_INTEREST;
import static com.android.media.codec.flags.Flags.FLAG_LARGE_AUDIO_FRAME;
@@ -2213,6 +2214,18 @@ final public class MediaCodec {
*/
public static final int CONFIGURE_FLAG_USE_CRYPTO_ASYNC = 4;
+ /**
+ * Configure the codec with a detached output surface.
+ * <p>
+ * This flag is only defined for a video decoder. MediaCodec
+ * configured with this flag will be in Surface mode even though
+ * the surface parameter is null.
+ *
+ * @see detachOutputSurface
+ */
+ @FlaggedApi(FLAG_NULL_OUTPUT_SURFACE)
+ public static final int CONFIGURE_FLAG_DETACHED_SURFACE = 8;
+
/** @hide */
@IntDef(
flag = true,
@@ -2395,6 +2408,31 @@ final public class MediaCodec {
private native void native_setSurface(@NonNull Surface surface);
/**
+ * Detach the current output surface of a codec.
+ * <p>
+ * Detaches the currently associated output Surface from the
+ * MediaCodec decoder. This allows the SurfaceView or other
+ * component holding the Surface to be safely destroyed or
+ * modified without affecting the decoder's operation. After
+ * calling this method (and after it returns), the decoder will
+ * enter detached-Surface mode and will no longer render
+ * output.
+ *
+ * @throws IllegalStateException if the codec was not
+ * configured in surface mode.
+ * @see CONFIGURE_FLAG_DETACHED_SURFACE
+ */
+ @FlaggedApi(FLAG_NULL_OUTPUT_SURFACE)
+ public void detachOutputSurface() {
+ if (!mHasSurface) {
+ throw new IllegalStateException("codec was not configured for an output surface");
+ }
+ // note: we still have a surface in detached mode, so keep mHasSurface
+ // we also technically allow calling detachOutputSurface multiple times in a row
+ // native_detachSurface();
+ }
+
+ /**
* Create a persistent input surface that can be used with codecs that normally have an input
* surface, such as video encoders. A persistent input can be reused by subsequent
* {@link MediaCodec} or {@link MediaRecorder} instances, but can only be used by at
@@ -3212,6 +3250,51 @@ final public class MediaCodec {
}
}
+ /**
+ * Similar to {@link #queueInputBuffers queueInputBuffers} but submits multiple access units
+ * in a buffer that is potentially encrypted.
+ * <strong>Check out further notes at {@link #queueInputBuffers queueInputBuffers}.</strong>
+ *
+ * @param index The index of a client-owned input buffer previously returned
+ * in a call to {@link #dequeueInputBuffer}.
+ * @param bufferInfos ArrayDeque of {@link MediaCodec.BufferInfo} that describes the
+ * contents in the buffer. The ArrayDeque and the BufferInfo objects provided
+ * can be recycled by the caller for re-use.
+ * @param cryptoInfos ArrayDeque of {@link MediaCodec.CryptoInfo} objects to facilitate the
+ * decryption of the contents. The ArrayDeque and the CryptoInfo objects
+ * provided can be reused immediately after the call returns. These objects
+ * should correspond to bufferInfo objects to ensure correct decryption.
+ * @throws IllegalStateException if not in the Executing state or not in asynchronous mode.
+ * @throws MediaCodec.CodecException upon codec error.
+ * @throws IllegalArgumentException upon if bufferInfos is empty, contains null, or if the
+ * access units are not contiguous.
+ * @throws CryptoException if an error occurs while attempting to decrypt the buffer.
+ * An error code associated with the exception helps identify the
+ * reason for the failure.
+ */
+ @FlaggedApi(FLAG_LARGE_AUDIO_FRAME)
+ public final void queueSecureInputBuffers(
+ int index,
+ @NonNull ArrayDeque<BufferInfo> bufferInfos,
+ @NonNull ArrayDeque<CryptoInfo> cryptoInfos) {
+ synchronized(mBufferLock) {
+ if (mBufferMode == BUFFER_MODE_BLOCK) {
+ throw new IncompatibleWithBlockModelException("queueSecureInputBuffers() "
+ + "is not compatible with CONFIGURE_FLAG_USE_BLOCK_MODEL. "
+ + "Please use getQueueRequest() to queue buffers");
+ }
+ invalidateByteBufferLocked(mCachedInputBuffers, index, true /* input */);
+ mDequeuedInputBuffers.remove(index);
+ }
+ try {
+ native_queueSecureInputBuffers(
+ index, bufferInfos.toArray(), cryptoInfos.toArray());
+ } catch (CryptoException | IllegalStateException | IllegalArgumentException e) {
+ revalidateByteBuffer(mCachedInputBuffers, index, true /* input */);
+ throw e;
+ }
+ }
+
private native final void native_queueSecureInputBuffer(
int index,
int offset,
@@ -3219,6 +3302,11 @@ final public class MediaCodec {
long presentationTimeUs,
int flags) throws CryptoException;
+ private native final void native_queueSecureInputBuffers(
+ int index,
+ @NonNull Object[] bufferInfos,
+ @NonNull Object[] cryptoInfos) throws CryptoException, CodecException;
+
/**
* Returns the index of an input buffer to be filled with valid data
* or -1 if no such buffer is currently available.
@@ -3464,7 +3552,7 @@ final public class MediaCodec {
mLinearBlock = block;
mOffset = offset;
mSize = size;
- mCryptoInfo = null;
+ mCryptoInfos.clear();
return this;
}
@@ -3498,7 +3586,44 @@ final public class MediaCodec {
mLinearBlock = block;
mOffset = offset;
mSize = size;
- mCryptoInfo = cryptoInfo;
+ mCryptoInfos.clear();
+ mCryptoInfos.add(cryptoInfo);
+ return this;
+ }
+
+ /**
+ * Set an encrypted linear block to this queue request. Exactly one buffer must be
+ * set for a queue request before calling {@link #queue}. The block can contain multiple
+ * access units and if present should be laid out contiguously and without gaps.
+ *
+ * @param block The linear block object
+ * @param bufferInfos ArrayDeque of {@link MediaCodec.BufferInfo} that describes the
+ * contents in the buffer. The ArrayDeque and the BufferInfo objects
+ * provided can be recycled by the caller for re-use.
+ * @param cryptoInfos ArrayDeque of {@link MediaCodec.CryptoInfo} that describes the
+ * structure of the encrypted input samples. The ArrayDeque and the
+ * BufferInfo objects provided can be recycled by the caller for re-use.
+ * @return this object
+ * @throws IllegalStateException if a buffer is already set
+ * @throws IllegalArgumentException upon if bufferInfos is empty, contains null, or if the
+ * access units are not contiguous.
+ */
+ @FlaggedApi(FLAG_LARGE_AUDIO_FRAME)
+ public @NonNull QueueRequest setMultiFrameEncryptedLinearBlock(
+ @NonNull LinearBlock block,
+ @NonNull ArrayDeque<MediaCodec.BufferInfo> bufferInfos,
+ @NonNull ArrayDeque<MediaCodec.CryptoInfo> cryptoInfos) {
+ if (!isAccessible()) {
+ throw new IllegalStateException("The request is stale");
+ }
+ if (mLinearBlock != null || mHardwareBuffer != null) {
+ throw new IllegalStateException("Cannot set block twice");
+ }
+ mLinearBlock = block;
+ mBufferInfos.clear();
+ mBufferInfos.addAll(bufferInfos);
+ mCryptoInfos.clear();
+ mCryptoInfos.addAll(cryptoInfos);
return this;
}
@@ -3710,8 +3835,10 @@ final public class MediaCodec {
mBufferInfos.add(info);
}
if (mLinearBlock != null) {
+
mCodec.native_queueLinearBlock(
- mIndex, mLinearBlock, mCryptoInfo,
+ mIndex, mLinearBlock,
+ mCryptoInfos.isEmpty() ? null : mCryptoInfos.toArray(),
mBufferInfos.toArray(),
mTuningKeys, mTuningValues);
} else if (mHardwareBuffer != null) {
@@ -3726,11 +3853,11 @@ final public class MediaCodec {
mLinearBlock = null;
mOffset = 0;
mSize = 0;
- mCryptoInfo = null;
mHardwareBuffer = null;
mPresentationTimeUs = 0;
mFlags = 0;
mBufferInfos.clear();
+ mCryptoInfos.clear();
mTuningKeys.clear();
mTuningValues.clear();
return this;
@@ -3750,11 +3877,11 @@ final public class MediaCodec {
private LinearBlock mLinearBlock = null;
private int mOffset = 0;
private int mSize = 0;
- private MediaCodec.CryptoInfo mCryptoInfo = null;
private HardwareBuffer mHardwareBuffer = null;
private long mPresentationTimeUs = 0;
private @BufferFlag int mFlags = 0;
private final ArrayDeque<BufferInfo> mBufferInfos = new ArrayDeque<>();
+ private final ArrayDeque<CryptoInfo> mCryptoInfos = new ArrayDeque<>();
private final ArrayList<String> mTuningKeys = new ArrayList<>();
private final ArrayList<Object> mTuningValues = new ArrayList<>();
@@ -3764,7 +3891,7 @@ final public class MediaCodec {
private native void native_queueLinearBlock(
int index,
@NonNull LinearBlock block,
- @Nullable CryptoInfo cryptoInfo,
+ @Nullable Object[] cryptoInfos,
@NonNull Object[] bufferInfos,
@NonNull ArrayList<String> keys,
@NonNull ArrayList<Object> values);
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java
index 86f89ab89f63..3174c377b91c 100644
--- a/media/java/android/media/MediaCodecInfo.java
+++ b/media/java/android/media/MediaCodecInfo.java
@@ -20,6 +20,7 @@ import static android.media.Utils.intersectSortedDistinctRanges;
import static android.media.Utils.sortDistinctRanges;
import static android.media.codec.Flags.FLAG_DYNAMIC_COLOR_ASPECTS;
import static android.media.codec.Flags.FLAG_HLG_EDITING;
+import static android.media.codec.Flags.FLAG_NULL_OUTPUT_SURFACE;
import static android.media.codec.Flags.FLAG_REGION_OF_INTEREST;
import android.annotation.FlaggedApi;
@@ -778,6 +779,17 @@ public final class MediaCodecInfo {
public static final String FEATURE_Roi = "region-of-interest";
/**
+ * <b>video decoder only</b>: codec supports detaching the
+ * output surface when in Surface mode.
+ * <p> If true, the codec can be configured in Surface mode
+ * without an actual surface (in detached surface mode).
+ * @see MediaCodec#CONFIGURE_FLAG_DETACHED_SURFACE
+ */
+ @SuppressLint("AllUpper")
+ @FlaggedApi(FLAG_NULL_OUTPUT_SURFACE)
+ public static final String FEATURE_DetachedSurface = "detached-surface";
+
+ /**
* Query codec feature capabilities.
* <p>
* These features are supported to be used by the codec. These
@@ -814,6 +826,9 @@ public final class MediaCodecInfo {
if (android.media.codec.Flags.dynamicColorAspects()) {
features.add(new Feature(FEATURE_DynamicColorAspects, (1 << 8), true));
}
+ if (android.media.codec.Flags.nullOutputSurface()) {
+ features.add(new Feature(FEATURE_DetachedSurface, (1 << 9), true));
+ }
// feature to exclude codec from REGULAR codec list
features.add(new Feature(FEATURE_SpecialCodec, (1 << 30), false, true));
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index 8cdd59e51ffe..8396005b1b63 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -458,6 +458,24 @@ status_t JMediaCodec::queueSecureInputBuffer(
presentationTimeUs, flags, errorDetailMsg);
}
+status_t JMediaCodec::queueSecureInputBuffers(
+ size_t index,
+ size_t offset,
+ size_t size,
+ const sp<RefBase> &auInfos_,
+ const sp<RefBase> &cryptoInfos_,
+ AString *errorDetailMsg) {
+ sp<BufferInfosWrapper> auInfos((BufferInfosWrapper *)auInfos_.get());
+ sp<CryptoInfosWrapper> cryptoInfos((CryptoInfosWrapper *)cryptoInfos_.get());
+ return mCodec->queueSecureInputBuffers(
+ index,
+ offset,
+ size,
+ auInfos,
+ cryptoInfos,
+ errorDetailMsg);
+}
+
status_t JMediaCodec::queueBuffer(
size_t index, const std::shared_ptr<C2Buffer> &buffer,
const sp<RefBase> &infos, const sp<AMessage> &tunings, AString *errorDetailMsg) {
@@ -470,19 +488,16 @@ status_t JMediaCodec::queueEncryptedLinearBlock(
size_t index,
const sp<hardware::HidlMemory> &buffer,
size_t offset,
- const CryptoPlugin::SubSample *subSamples,
- size_t numSubSamples,
- const uint8_t key[16],
- const uint8_t iv[16],
- CryptoPlugin::Mode mode,
- const CryptoPlugin::Pattern &pattern,
+ size_t size,
const sp<RefBase> &infos,
+ const sp<RefBase> &cryptoInfos_,
const sp<AMessage> &tunings,
AString *errorDetailMsg) {
sp<BufferInfosWrapper> auInfo((BufferInfosWrapper *)infos.get());
+ sp<CryptoInfosWrapper> cryptoInfos((CryptoInfosWrapper *)cryptoInfos_.get());
return mCodec->queueEncryptedBuffer(
- index, buffer, offset, subSamples, numSubSamples, key, iv, mode, pattern,
- auInfo, tunings, errorDetailMsg);
+ index, buffer, offset, size, auInfo, cryptoInfos,
+ tunings, errorDetailMsg);
}
status_t JMediaCodec::dequeueInputBuffer(size_t *index, int64_t timeoutUs) {
@@ -2262,6 +2277,61 @@ struct NativeCryptoInfo {
CryptoPlugin::Pattern mPattern;
};
+// This class takes away all dependencies on java(env and jni) and
+// could be used for taking cryptoInfo objects to MediaCodec.
+struct MediaCodecCryptoInfo: public CodecCryptoInfo {
+ explicit MediaCodecCryptoInfo(const NativeCryptoInfo &cryptoInfo) {
+ if (cryptoInfo.mErr == OK) {
+ mNumSubSamples = cryptoInfo.mNumSubSamples;
+ mMode = cryptoInfo.mMode;
+ mPattern = cryptoInfo.mPattern;
+ if (cryptoInfo.mKey != nullptr) {
+ mKeyBuffer = ABuffer::CreateAsCopy(cryptoInfo.mKey, 16);
+ mKey = (uint8_t*)(mKeyBuffer.get() != nullptr ? mKeyBuffer.get()->data() : nullptr);
+ }
+ if (cryptoInfo.mIv != nullptr) {
+ mIvBuffer = ABuffer::CreateAsCopy(cryptoInfo.mIv, 16);
+ mIv = (uint8_t*)(mIvBuffer.get() != nullptr ? mIvBuffer.get()->data() : nullptr);
+ }
+ if (cryptoInfo.mSubSamples != nullptr) {
+ mSubSamplesBuffer = new ABuffer(sizeof(CryptoPlugin::SubSample) * mNumSubSamples);
+ if (mSubSamplesBuffer.get()) {
+ CryptoPlugin::SubSample * samples =
+ (CryptoPlugin::SubSample *)(mSubSamplesBuffer.get()->data());
+ for (int s = 0 ; s < mNumSubSamples ; s++) {
+ samples[s].mNumBytesOfClearData =
+ cryptoInfo.mSubSamples[s].mNumBytesOfClearData;
+ samples[s].mNumBytesOfEncryptedData =
+ cryptoInfo.mSubSamples[s].mNumBytesOfEncryptedData;
+ }
+ mSubSamples = (CryptoPlugin::SubSample *)mSubSamplesBuffer.get()->data();
+ }
+ }
+
+ }
+ }
+
+ explicit MediaCodecCryptoInfo(jint size) {
+ mSubSamplesBuffer = new ABuffer(sizeof(CryptoPlugin::SubSample) * 1);
+ mNumSubSamples = 1;
+ if (mSubSamplesBuffer.get()) {
+ CryptoPlugin::SubSample * samples =
+ (CryptoPlugin::SubSample *)(mSubSamplesBuffer.get()->data());
+ samples[0].mNumBytesOfClearData = size;
+ samples[0].mNumBytesOfEncryptedData = 0;
+ mSubSamples = (CryptoPlugin::SubSample *)mSubSamplesBuffer.get()->data();
+ }
+ }
+ ~MediaCodecCryptoInfo() {}
+
+protected:
+ // all backup buffers for the base object.
+ sp<ABuffer> mKeyBuffer;
+ sp<ABuffer> mIvBuffer;
+ sp<ABuffer> mSubSamplesBuffer;
+
+};
+
static void android_media_MediaCodec_queueSecureInputBuffer(
JNIEnv *env,
jobject thiz,
@@ -2430,6 +2500,99 @@ static void android_media_MediaCodec_queueSecureInputBuffer(
codec->getExceptionMessage(errorDetailMsg.c_str()).c_str(), codec->getCrypto());
}
+static status_t extractCryptoInfosFromObjectArray(JNIEnv * const env,
+ jint * const totalSize,
+ std::vector<std::unique_ptr<CodecCryptoInfo>> * const cryptoInfoObjs,
+ const jobjectArray &objArray,
+ AString * const errorDetailMsg) {
+ if (env == nullptr
+ || cryptoInfoObjs == nullptr
+ || totalSize == nullptr) {
+ if (errorDetailMsg) {
+ *errorDetailMsg = "Error: Null Parameters provided for extracting CryptoInfo";
+ }
+ return BAD_VALUE;
+ }
+ const jsize numEntries = env->GetArrayLength(objArray);
+ if (numEntries <= 0) {
+ if (errorDetailMsg) {
+ *errorDetailMsg = "Error: No CryptoInfo found while queuing for large frame input";
+ }
+ return BAD_VALUE;
+ }
+ cryptoInfoObjs->clear();
+ *totalSize = 0;
+ jint size = 0;
+ for (jsize i = 0; i < numEntries ; i++) {
+ jobject param = env->GetObjectArrayElement(objArray, i);
+ if (param == NULL) {
+ if (errorDetailMsg) {
+ *errorDetailMsg = "Error: Null Parameters provided for extracting CryptoInfo";
+ }
+ return BAD_VALUE;
+ }
+ NativeCryptoInfo nativeInfo(env, param);
+ std::unique_ptr<CodecCryptoInfo> info(new MediaCodecCryptoInfo(nativeInfo));
+ for (int i = 0; i < info->mNumSubSamples; i++) {
+ size += info->mSubSamples[i].mNumBytesOfClearData;
+ size += info->mSubSamples[i].mNumBytesOfEncryptedData;
+ }
+ cryptoInfoObjs->push_back(std::move(info));
+ }
+ *totalSize = size;
+ return OK;
+}
+
+
+static void android_media_MediaCodec_queueSecureInputBuffers(
+ JNIEnv *env,
+ jobject thiz,
+ jint index,
+ jobjectArray bufferInfosObjs,
+ jobjectArray cryptoInfoObjs) {
+ ALOGV("android_media_MediaCodec_queueSecureInputBuffers");
+
+ sp<JMediaCodec> codec = getMediaCodec(env, thiz);
+
+ if (codec == NULL || codec->initCheck() != OK) {
+ throwExceptionAsNecessary(env, INVALID_OPERATION, codec);
+ return;
+ }
+ sp<BufferInfosWrapper> auInfos =
+ new BufferInfosWrapper{decltype(auInfos->value)()};
+ sp<CryptoInfosWrapper> cryptoInfos =
+ new CryptoInfosWrapper{decltype(cryptoInfos->value)()};
+ AString errorDetailMsg;
+ jint initialOffset = 0;
+ jint totalSize = 0;
+ status_t err = extractInfosFromObject(
+ env,
+ &initialOffset,
+ &totalSize,
+ &auInfos->value,
+ bufferInfosObjs,
+ &errorDetailMsg);
+ if (err == OK) {
+ err = extractCryptoInfosFromObjectArray(env,
+ &totalSize,
+ &cryptoInfos->value,
+ cryptoInfoObjs,
+ &errorDetailMsg);
+ }
+ if (err == OK) {
+ err = codec->queueSecureInputBuffers(
+ index,
+ initialOffset,
+ totalSize,
+ auInfos,
+ cryptoInfos,
+ &errorDetailMsg);
+ }
+ throwExceptionAsNecessary(
+ env, err, ACTION_CODE_FATAL,
+ codec->getExceptionMessage(errorDetailMsg.c_str()).c_str(), codec->getCrypto());
+}
+
static jobject android_media_MediaCodec_mapHardwareBuffer(JNIEnv *env, jclass, jobject bufferObj) {
ALOGV("android_media_MediaCodec_mapHardwareBuffer");
AHardwareBuffer *hardwareBuffer = android_hardware_HardwareBuffer_getNativeHardwareBuffer(
@@ -2762,7 +2925,7 @@ static void extractBufferFromContext(
static void android_media_MediaCodec_native_queueLinearBlock(
JNIEnv *env, jobject thiz, jint index, jobject bufferObj,
- jobject cryptoInfoObj, jobjectArray objArray, jobject keys, jobject values) {
+ jobjectArray cryptoInfoArray, jobjectArray objArray, jobject keys, jobject values) {
ALOGV("android_media_MediaCodec_native_queueLinearBlock");
sp<JMediaCodec> codec = getMediaCodec(env, thiz);
@@ -2780,8 +2943,8 @@ static void android_media_MediaCodec_native_queueLinearBlock(
"error occurred while converting tunings from Java to native");
return;
}
- jint totalSize;
- jint initialOffset;
+ jint totalSize = 0;
+ jint initialOffset = 0;
std::vector<AccessUnitInfo> infoVec;
AString errorDetailMsg;
err = extractInfosFromObject(env,
@@ -2832,8 +2995,19 @@ static void android_media_MediaCodec_native_queueLinearBlock(
"MediaCodec.LinearBlock#obtain method to obtain a compatible buffer.");
return;
}
- auto cryptoInfo =
- cryptoInfoObj ? NativeCryptoInfo{env, cryptoInfoObj} : NativeCryptoInfo{totalSize};
+ sp<CryptoInfosWrapper> cryptoInfos = new CryptoInfosWrapper{decltype(cryptoInfos->value)()};
+ jint sampleSize = 0;
+ if (cryptoInfoArray != nullptr) {
+ extractCryptoInfosFromObjectArray(env,
+ &sampleSize,
+ &cryptoInfos->value,
+ cryptoInfoArray,
+ &errorDetailMsg);
+ } else {
+ sampleSize = totalSize;
+ std::unique_ptr<CodecCryptoInfo> cryptoInfo{new MediaCodecCryptoInfo(totalSize)};
+ cryptoInfos->value.push_back(std::move(cryptoInfo));
+ }
if (env->ExceptionCheck()) {
// Creation of cryptoInfo failed. Let the exception bubble up.
return;
@@ -2842,11 +3016,9 @@ static void android_media_MediaCodec_native_queueLinearBlock(
index,
memory,
initialOffset,
- cryptoInfo.mSubSamples, cryptoInfo.mNumSubSamples,
- (const uint8_t *)cryptoInfo.mKey, (const uint8_t *)cryptoInfo.mIv,
- cryptoInfo.mMode,
- cryptoInfo.mPattern,
+ sampleSize,
infos,
+ cryptoInfos,
tunings,
&errorDetailMsg);
ALOGI_IF(err != OK, "queueEncryptedLinearBlock returned err = %d", err);
@@ -3950,6 +4122,9 @@ static const JNINativeMethod gMethods[] = {
{ "native_queueSecureInputBuffer", "(IILandroid/media/MediaCodec$CryptoInfo;JI)V",
(void *)android_media_MediaCodec_queueSecureInputBuffer },
+ { "native_queueSecureInputBuffers", "(I[Ljava/lang/Object;[Ljava/lang/Object;)V",
+ (void *)android_media_MediaCodec_queueSecureInputBuffers },
+
{ "native_mapHardwareBuffer",
"(Landroid/hardware/HardwareBuffer;)Landroid/media/Image;",
(void *)android_media_MediaCodec_mapHardwareBuffer },
@@ -3957,7 +4132,7 @@ static const JNINativeMethod gMethods[] = {
{ "native_closeMediaImage", "(J)V", (void *)android_media_MediaCodec_closeMediaImage },
{ "native_queueLinearBlock",
- "(ILandroid/media/MediaCodec$LinearBlock;Landroid/media/MediaCodec$CryptoInfo;"
+ "(ILandroid/media/MediaCodec$LinearBlock;[Ljava/lang/Object;"
"[Ljava/lang/Object;Ljava/util/ArrayList;Ljava/util/ArrayList;)V",
(void *)android_media_MediaCodec_native_queueLinearBlock },
diff --git a/media/jni/android_media_MediaCodec.h b/media/jni/android_media_MediaCodec.h
index 02708efdea3a..abb23f516156 100644
--- a/media/jni/android_media_MediaCodec.h
+++ b/media/jni/android_media_MediaCodec.h
@@ -114,6 +114,14 @@ struct JMediaCodec : public AHandler {
uint32_t flags,
AString *errorDetailMsg);
+ status_t queueSecureInputBuffers(
+ size_t index,
+ size_t offset,
+ size_t size,
+ const sp<RefBase> &auInfos,
+ const sp<RefBase> &cryptoInfos,
+ AString *errorDetailMsg);
+
status_t queueBuffer(
size_t index, const std::shared_ptr<C2Buffer> &buffer,
const sp<RefBase> &infos, const sp<AMessage> &tunings,
@@ -123,13 +131,9 @@ struct JMediaCodec : public AHandler {
size_t index,
const sp<hardware::HidlMemory> &buffer,
size_t offset,
- const CryptoPlugin::SubSample *subSamples,
- size_t numSubSamples,
- const uint8_t key[16],
- const uint8_t iv[16],
- CryptoPlugin::Mode mode,
- const CryptoPlugin::Pattern &pattern,
+ size_t size,
const sp<RefBase> &infos,
+ const sp<RefBase> &cryptoInfos,
const sp<AMessage> &tunings,
AString *errorDetailMsg);
diff --git a/native/android/input.cpp b/native/android/input.cpp
index 64e8efeaa4e8..6efb0280ac02 100644
--- a/native/android/input.cpp
+++ b/native/android/input.cpp
@@ -314,6 +314,23 @@ const AInputEvent* AMotionEvent_fromJava(JNIEnv* env, jobject motionEvent) {
return event;
}
+jobject AInputEvent_toJava(JNIEnv* env, const AInputEvent* aInputEvent) {
+ LOG_ALWAYS_FATAL_IF(aInputEvent == nullptr, "Expected aInputEvent to be non-null");
+ const int32_t eventType = AInputEvent_getType(aInputEvent);
+ switch (eventType) {
+ case AINPUT_EVENT_TYPE_MOTION:
+ return android::android_view_MotionEvent_obtainAsCopy(env,
+ static_cast<const MotionEvent&>(
+ *aInputEvent));
+ case AINPUT_EVENT_TYPE_KEY:
+ return android::android_view_KeyEvent_fromNative(env,
+ static_cast<const KeyEvent&>(
+ *aInputEvent));
+ default:
+ LOG_ALWAYS_FATAL("Unexpected event type %d in AInputEvent_toJava.", eventType);
+ }
+}
+
void AInputQueue_attachLooper(AInputQueue* queue, ALooper* looper,
int ident, ALooper_callbackFunc callback, void* data) {
InputQueue* iq = static_cast<InputQueue*>(queue);
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index 960510879a4c..3302265fb80c 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -90,6 +90,7 @@ LIBANDROID {
AInputEvent_getSource;
AInputEvent_getType;
AInputEvent_release; # introduced=31
+ AInputEvent_toJava; # introduced=35
AInputQueue_attachLooper;
AInputQueue_detachLooper;
AInputQueue_finishEvent;
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index e349fa36368d..babb6c219714 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -22,6 +22,8 @@ import static android.app.Notification.FLAG_GROUP_SUMMARY;
import static android.app.Notification.FLAG_LOCAL_ONLY;
import static android.app.Notification.FLAG_NO_CLEAR;
import static android.app.Notification.FLAG_ONGOING_EVENT;
+import static android.app.Notification.VISIBILITY_PRIVATE;
+import static android.app.Notification.VISIBILITY_PUBLIC;
import android.annotation.NonNull;
import android.app.Notification;
@@ -145,7 +147,8 @@ public class GroupHelper {
mUngroupedNotifications.getOrDefault(key, new ArrayMap<>());
NotificationAttributes attr = new NotificationAttributes(sbn.getNotification().flags,
- sbn.getNotification().getSmallIcon(), sbn.getNotification().color);
+ sbn.getNotification().getSmallIcon(), sbn.getNotification().color,
+ sbn.getNotification().visibility);
children.put(sbn.getKey(), attr);
mUngroupedNotifications.put(key, children);
@@ -158,25 +161,29 @@ public class GroupHelper {
if (notificationsToGroup.size() > 0) {
if (autogroupSummaryExists) {
NotificationAttributes attr = new NotificationAttributes(flags,
- sbn.getNotification().getSmallIcon(), sbn.getNotification().color);
+ sbn.getNotification().getSmallIcon(), sbn.getNotification().color,
+ VISIBILITY_PRIVATE);
if (Flags.autogroupSummaryIconUpdate()) {
- attr = updateAutobundledSummaryIcon(sbn.getPackageName(), childrenAttr, attr);
+ attr = updateAutobundledSummaryAttributes(sbn.getPackageName(), childrenAttr,
+ attr);
}
mCallback.updateAutogroupSummary(sbn.getUserId(), sbn.getPackageName(), attr);
} else {
Icon summaryIcon = sbn.getNotification().getSmallIcon();
int summaryIconColor = sbn.getNotification().color;
+ int summaryVisibility = VISIBILITY_PRIVATE;
if (Flags.autogroupSummaryIconUpdate()) {
- // Calculate the initial summary icon and icon color
- NotificationAttributes iconAttr = getAutobundledSummaryIconAndColor(
+ // Calculate the initial summary icon, icon color and visibility
+ NotificationAttributes iconAttr = getAutobundledSummaryAttributes(
sbn.getPackageName(), childrenAttr);
summaryIcon = iconAttr.icon;
summaryIconColor = iconAttr.iconColor;
+ summaryVisibility = iconAttr.visibility;
}
NotificationAttributes attr = new NotificationAttributes(flags, summaryIcon,
- summaryIconColor);
+ summaryIconColor, summaryVisibility);
mCallback.addAutoGroupSummary(sbn.getUserId(), sbn.getPackageName(), sbn.getKey(),
attr);
}
@@ -238,18 +245,19 @@ public class GroupHelper {
mCallback.removeAutoGroupSummary(userId, sbn.getPackageName());
} else {
NotificationAttributes attr = new NotificationAttributes(summaryFlags,
- sbn.getNotification().getSmallIcon(), sbn.getNotification().color);
- boolean iconUpdated = false;
+ sbn.getNotification().getSmallIcon(), sbn.getNotification().color,
+ VISIBILITY_PRIVATE);
+ boolean attributesUpdated = false;
if (Flags.autogroupSummaryIconUpdate()) {
- NotificationAttributes newAttr = updateAutobundledSummaryIcon(sbn.getPackageName(),
- childrenAttrs, attr);
+ NotificationAttributes newAttr = updateAutobundledSummaryAttributes(
+ sbn.getPackageName(), childrenAttrs, attr);
if (!newAttr.equals(attr)) {
- iconUpdated = true;
+ attributesUpdated = true;
attr = newAttr;
}
}
- if (updateSummaryFlags || iconUpdated) {
+ if (updateSummaryFlags || attributesUpdated) {
mCallback.updateAutogroupSummary(userId, sbn.getPackageName(), attr);
}
}
@@ -268,12 +276,13 @@ public class GroupHelper {
}
}
- NotificationAttributes getAutobundledSummaryIconAndColor(@NonNull String packageName,
+ NotificationAttributes getAutobundledSummaryAttributes(@NonNull String packageName,
@NonNull List<NotificationAttributes> childrenAttr) {
Icon newIcon = null;
boolean childrenHaveSameIcon = true;
int newColor = Notification.COLOR_INVALID;
boolean childrenHaveSameColor = true;
+ int newVisibility = VISIBILITY_PRIVATE;
// Both the icon drawable and the icon background color are updated according to this rule:
// - if all child icons are identical => use the common icon
@@ -296,6 +305,10 @@ public class GroupHelper {
childrenHaveSameColor = false;
}
}
+ // Check for visibility. If at least one child is public, then set to public
+ if (state.visibility == VISIBILITY_PUBLIC) {
+ newVisibility = VISIBILITY_PUBLIC;
+ }
}
if (!childrenHaveSameIcon) {
newIcon = getMonochromeAppIcon(packageName);
@@ -304,13 +317,13 @@ public class GroupHelper {
newColor = COLOR_DEFAULT;
}
- return new NotificationAttributes(0, newIcon, newColor);
+ return new NotificationAttributes(0, newIcon, newColor, newVisibility);
}
- NotificationAttributes updateAutobundledSummaryIcon(@NonNull String packageName,
+ NotificationAttributes updateAutobundledSummaryAttributes(@NonNull String packageName,
@NonNull List<NotificationAttributes> childrenAttr,
@NonNull NotificationAttributes oldAttr) {
- NotificationAttributes newAttr = getAutobundledSummaryIconAndColor(packageName,
+ NotificationAttributes newAttr = getAutobundledSummaryAttributes(packageName,
childrenAttr);
Icon newIcon = newAttr.icon;
int newColor = newAttr.iconColor;
@@ -321,7 +334,7 @@ public class GroupHelper {
newColor = oldAttr.iconColor;
}
- return new NotificationAttributes(oldAttr.flags, newIcon, newColor);
+ return new NotificationAttributes(oldAttr.flags, newIcon, newColor, newAttr.visibility);
}
/**
@@ -358,17 +371,20 @@ public class GroupHelper {
public final int flags;
public final int iconColor;
public final Icon icon;
+ public final int visibility;
- public NotificationAttributes(int flags, Icon icon, int iconColor) {
+ public NotificationAttributes(int flags, Icon icon, int iconColor, int visibility) {
this.flags = flags;
this.icon = icon;
this.iconColor = iconColor;
+ this.visibility = visibility;
}
public NotificationAttributes(@NonNull NotificationAttributes attr) {
this.flags = attr.flags;
this.icon = attr.icon;
this.iconColor = attr.iconColor;
+ this.visibility = attr.visibility;
}
@Override
@@ -379,12 +395,13 @@ public class GroupHelper {
if (!(o instanceof NotificationAttributes that)) {
return false;
}
- return flags == that.flags && iconColor == that.iconColor && icon.sameAs(that.icon);
+ return flags == that.flags && iconColor == that.iconColor && icon.sameAs(that.icon)
+ && visibility == that.visibility;
}
@Override
public int hashCode() {
- return Objects.hash(flags, iconColor, icon);
+ return Objects.hash(flags, iconColor, icon, visibility);
}
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index ea4e67a17e50..d751186b2869 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1038,15 +1038,17 @@ public class NotificationManagerService extends SystemService {
}
int oldFlags = summary.getSbn().getNotification().flags;
- boolean iconUpdated =
+ boolean attributesUpdated =
!summaryAttr.icon.sameAs(summary.getSbn().getNotification().getSmallIcon())
- || summaryAttr.iconColor != summary.getSbn().getNotification().color;
+ || summaryAttr.iconColor != summary.getSbn().getNotification().color
+ || summaryAttr.visibility != summary.getSbn().getNotification().visibility;
- if (oldFlags != summaryAttr.flags || iconUpdated) {
+ if (oldFlags != summaryAttr.flags || attributesUpdated) {
summary.getSbn().getNotification().flags =
summaryAttr.flags != GroupHelper.FLAG_INVALID ? summaryAttr.flags : oldFlags;
summary.getSbn().getNotification().setSmallIcon(summaryAttr.icon);
summary.getSbn().getNotification().color = summaryAttr.iconColor;
+ summary.getSbn().getNotification().visibility = summaryAttr.visibility;
mHandler.post(new EnqueueNotificationRunnable(userId, summary, isAppForeground,
mPostNotificationTrackerFactory.newTracker(null)));
}
@@ -2939,7 +2941,8 @@ public class NotificationManagerService extends SystemService {
public void addAutoGroupSummary(int userId, String pkg, String triggeringKey,
NotificationAttributes summaryAttr) {
NotificationRecord r = createAutoGroupSummary(userId, pkg, triggeringKey,
- summaryAttr.flags, summaryAttr.icon, summaryAttr.iconColor);
+ summaryAttr.flags, summaryAttr.icon, summaryAttr.iconColor,
+ summaryAttr.visibility);
if (r != null) {
final boolean isAppForeground =
mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND;
@@ -6725,7 +6728,7 @@ public class NotificationManagerService extends SystemService {
// Creates a 'fake' summary for a package that has exceeded the solo-notification limit.
NotificationRecord createAutoGroupSummary(int userId, String pkg, String triggeringKey,
- int flagsToSet, Icon summaryIcon, int summaryIconColor) {
+ int flagsToSet, Icon summaryIcon, int summaryIconColor, int summaryVisibilty) {
NotificationRecord summaryRecord = null;
boolean isPermissionFixed = mPermissionHelper.isPermissionFixed(pkg, userId);
synchronized (mNotificationLock) {
@@ -6760,6 +6763,7 @@ public class NotificationManagerService extends SystemService {
.setGroup(GroupHelper.AUTOGROUP_KEY)
.setFlag(flagsToSet, true)
.setColor(summaryIconColor)
+ .setVisibility(summaryVisibilty)
.build();
summaryNotification.extras.putAll(extras);
Intent appIntent = getContext().getPackageManager().getLaunchIntentForPackage(pkg);
diff --git a/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java b/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java
index 62a637ebde13..3077fb8aaee9 100644
--- a/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java
+++ b/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java
@@ -257,6 +257,55 @@ final class RemoteWearableSensingService extends ServiceConnector.Impl<IWearable
statusCallback));
}
+ /**
+ * Request the wearable to start hotword recognition.
+ *
+ * @param wearableHotwordCallback The callback to send hotword audio data and format to.
+ * @param statusCallback The callback for service status.
+ */
+ public void startHotwordRecognition(
+ RemoteCallback wearableHotwordCallback, RemoteCallback statusCallback) {
+ if (DEBUG) {
+ Slog.i(TAG, "Starting to listen for hotword.");
+ }
+ var unused =
+ post(
+ service ->
+ service.startHotwordRecognition(
+ wearableHotwordCallback, statusCallback));
+ }
+
+ /**
+ * Request the wearable to stop hotword recognition.
+ *
+ * @param statusCallback The callback for service status.
+ */
+ public void stopHotwordRecognition(RemoteCallback statusCallback) {
+ if (DEBUG) {
+ Slog.i(TAG, "Stopping hotword recognition.");
+ }
+ var unused = post(service -> service.stopHotwordRecognition(statusCallback));
+ }
+
+ /**
+ * Signals to the {@link WearableSensingService} that hotword audio data is accepted by the
+ * {@link android.service.voice.HotwordDetectionService} as valid hotword.
+ */
+ public void onValidatedByHotwordDetectionService() {
+ if (DEBUG) {
+ Slog.i(TAG, "Requesting hotword audio data egress.");
+ }
+ var unused = post(service -> service.onValidatedByHotwordDetectionService());
+ }
+
+ /** Stops the active hotword audio stream from the wearable. */
+ public void stopActiveHotwordAudio() {
+ if (DEBUG) {
+ Slog.i(TAG, "Stopping hotword audio.");
+ }
+ var unused = post(service -> service.stopActiveHotwordAudio());
+ }
+
private static class SecureWearableConnectionContext {
final ParcelFileDescriptor mSecureWearableConnection;
final RemoteCallback mStatusCallback;
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
index 9ba4433523ee..2b43203628d9 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
@@ -16,6 +16,8 @@
package com.android.server.wearable;
+import static android.service.wearable.WearableSensingService.HOTWORD_AUDIO_STREAM_BUNDLE_KEY;
+
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -28,18 +30,23 @@ import android.companion.CompanionDeviceManager;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
+import android.os.Binder;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.SharedMemory;
+import android.service.voice.HotwordAudioStream;
+import android.service.voice.VoiceInteractionManagerInternal;
+import android.service.voice.VoiceInteractionManagerInternal.WearableHotwordDetectionCallback;
import android.system.OsConstants;
import android.util.IndentingPrintWriter;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
import com.android.server.infra.AbstractPerUserSystemService;
import java.io.IOException;
@@ -58,6 +65,7 @@ final class WearableSensingManagerPerUserService extends
@VisibleForTesting
RemoteWearableSensingService mRemoteService;
+ @Nullable private VoiceInteractionManagerInternal mVoiceInteractionManagerInternal;
private ComponentName mComponentName;
private final Object mSecureChannelLock = new Object();
@@ -99,6 +107,15 @@ final class WearableSensingManagerPerUserService extends
}
}
+ @GuardedBy("mLock")
+ private boolean ensureVoiceInteractionManagerInternalInitiated() {
+ if (mVoiceInteractionManagerInternal == null) {
+ mVoiceInteractionManagerInternal =
+ LocalServices.getService(VoiceInteractionManagerInternal.class);
+ }
+ return mVoiceInteractionManagerInternal != null;
+ }
+
/**
* get the currently bound component name.
*/
@@ -334,4 +351,109 @@ final class WearableSensingManagerPerUserService extends
dataType, dataRequestObserverId, packageName, statusCallback);
}
}
+
+ /** Handles starting hotword listening. */
+ public void onStartHotwordRecognition(
+ ComponentName targetVisComponentName, RemoteCallback statusCallback) {
+ synchronized (mLock) {
+ if (!setUpServiceIfNeeded()) {
+ Slog.w(TAG, "Detection service is not available at this moment.");
+ notifyStatusCallback(
+ statusCallback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ if (!ensureVoiceInteractionManagerInternalInitiated()) {
+ Slog.w(TAG, "Voice interaction manager is not available at this moment.");
+ notifyStatusCallback(
+ statusCallback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ ensureRemoteServiceInitiated();
+ mRemoteService.startHotwordRecognition(
+ createWearableHotwordCallback(targetVisComponentName), statusCallback);
+ }
+ }
+
+ /** Handles stopping hotword listening. */
+ public void onStopHotwordRecognition(RemoteCallback statusCallback) {
+ synchronized (mLock) {
+ if (!setUpServiceIfNeeded()) {
+ Slog.w(TAG, "Detection service is not available at this moment.");
+ notifyStatusCallback(
+ statusCallback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ ensureRemoteServiceInitiated();
+ mRemoteService.stopHotwordRecognition(statusCallback);
+ }
+ }
+
+ private void onValidatedByHotwordDetectionService() {
+ synchronized (mLock) {
+ if (!setUpServiceIfNeeded()) {
+ Slog.w(TAG, "Wearable sensing service is not available at this moment.");
+ return;
+ }
+ ensureRemoteServiceInitiated();
+ mRemoteService.onValidatedByHotwordDetectionService();
+ }
+ }
+
+ private void stopActiveHotwordAudio() {
+ synchronized (mLock) {
+ if (!setUpServiceIfNeeded()) {
+ Slog.w(TAG, "Wearable sensing service is not available at this moment.");
+ return;
+ }
+ ensureRemoteServiceInitiated();
+ mRemoteService.stopActiveHotwordAudio();
+ }
+ }
+
+ private RemoteCallback createWearableHotwordCallback(ComponentName targetVisComponentName) {
+ return new RemoteCallback(
+ result -> {
+ HotwordAudioStream hotwordAudioStream =
+ result.getParcelable(
+ HOTWORD_AUDIO_STREAM_BUNDLE_KEY, HotwordAudioStream.class);
+ if (hotwordAudioStream == null) {
+ Slog.w(TAG, "No hotword audio stream received, unable to process hotword.");
+ return;
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mVoiceInteractionManagerInternal.startListeningFromWearable(
+ hotwordAudioStream.getAudioStreamParcelFileDescriptor(),
+ hotwordAudioStream.getAudioFormat(),
+ hotwordAudioStream.getMetadata(),
+ targetVisComponentName,
+ getUserId(),
+ createHotwordDetectionCallback());
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ });
+ }
+
+ private WearableHotwordDetectionCallback createHotwordDetectionCallback() {
+ return new WearableHotwordDetectionCallback() {
+ @Override
+ public void onDetected() {
+ Slog.i(TAG, "hotwordDetectionCallback onDetected.");
+ onValidatedByHotwordDetectionService();
+ }
+
+ @Override
+ public void onRejected() {
+ Slog.i(TAG, "hotwordDetectionCallback onRejected.");
+ stopActiveHotwordAudio();
+ }
+
+ @Override
+ public void onError(String errorMessage) {
+ Slog.i(TAG, "hotwordDetectionCallback onError. ErrorMessage: " + errorMessage);
+ stopActiveHotwordAudio();
+ }
+ };
+ }
}
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
index 78952fa4590b..00c3026b1194 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
@@ -496,6 +496,42 @@ public class WearableSensingManagerService extends
}
@Override
+ public void startHotwordRecognition(
+ ComponentName targetVisComponentName, RemoteCallback statusCallback) {
+ Slog.i(TAG, "WearableSensingManagerInternal startHotwordRecognition.");
+ Objects.requireNonNull(statusCallback);
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE, TAG);
+ if (!mIsServiceEnabled) {
+ Slog.w(TAG, "Service not available.");
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ statusCallback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ callPerUserServiceIfExist(
+ service ->
+ service.onStartHotwordRecognition(
+ targetVisComponentName, statusCallback),
+ statusCallback);
+ }
+
+ @Override
+ public void stopHotwordRecognition(RemoteCallback statusCallback) {
+ Slog.i(TAG, "WearableSensingManagerInternal stopHotwordRecognition.");
+ Objects.requireNonNull(statusCallback);
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE, TAG);
+ if (!mIsServiceEnabled) {
+ Slog.w(TAG, "Service not available.");
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ statusCallback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ callPerUserServiceIfExist(
+ service -> service.onStopHotwordRecognition(statusCallback), statusCallback);
+ }
+
+ @Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
new WearableSensingShellCommand(WearableSensingManagerService.this).exec(
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index 5eb76e352ea2..f0779144ad18 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -22,6 +22,9 @@ import static android.app.Notification.FLAG_CAN_COLORIZE;
import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
import static android.app.Notification.FLAG_NO_CLEAR;
import static android.app.Notification.FLAG_ONGOING_EVENT;
+import static android.app.Notification.VISIBILITY_PRIVATE;
+import static android.app.Notification.VISIBILITY_PUBLIC;
+import static android.app.Notification.VISIBILITY_SECRET;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static com.android.server.notification.GroupHelper.BASE_FLAGS;
@@ -81,6 +84,8 @@ public class GroupHelperTest extends UiServiceTestCase {
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
+ private final int DEFAULT_VISIBILITY = VISIBILITY_PRIVATE;
+
private @Mock GroupHelper.Callback mCallback;
private @Mock PackageManager mPackageManager;
@@ -127,7 +132,7 @@ public class GroupHelperTest extends UiServiceTestCase {
}
private NotificationAttributes getNotificationAttributes(int flags) {
- return new NotificationAttributes(flags, mSmallIcon, COLOR_DEFAULT);
+ return new NotificationAttributes(flags, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY);
}
@Test
@@ -704,7 +709,8 @@ public class GroupHelperTest extends UiServiceTestCase {
final Icon icon = mock(Icon.class);
when(icon.sameAs(icon)).thenReturn(true);
final int iconColor = Color.BLUE;
- final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor);
+ final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
+ DEFAULT_VISIBILITY);
// Add notifications with same icon and color
for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
@@ -744,7 +750,7 @@ public class GroupHelperTest extends UiServiceTestCase {
doReturn(monochromeIcon).when(groupHelper).getMonochromeAppIcon(eq(pkg));
final NotificationAttributes initialAttr = new NotificationAttributes(BASE_FLAGS,
- initialIcon, initialIconColor);
+ initialIcon, initialIconColor, DEFAULT_VISIBILITY);
// Add notifications with same icon and color
for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
@@ -769,7 +775,42 @@ public class GroupHelperTest extends UiServiceTestCase {
// Summary should be updated to the default color and the icon to the monochrome icon
NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, monochromeIcon,
- COLOR_DEFAULT);
+ COLOR_DEFAULT, DEFAULT_VISIBILITY);
+ verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(newAttr));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+ public void testAddSummary_diffVisibility() {
+ final String pkg = "package";
+ final Icon icon = mock(Icon.class);
+ when(icon.sameAs(icon)).thenReturn(true);
+ final int iconColor = Color.BLUE;
+ final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
+ VISIBILITY_PRIVATE);
+
+ // Add notifications with same icon and color and default visibility (private)
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+ icon, iconColor);
+ mGroupHelper.onNotificationPosted(sbn, false);
+ }
+ // Check that the summary has private visibility
+ verify(mCallback, times(1)).addAutoGroupSummary(
+ anyInt(), eq(pkg), anyString(), eq(attr));
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+
+ // After auto-grouping, add new notification with public visibility
+ StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT,
+ String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor);
+ sbn.getNotification().visibility = VISIBILITY_PUBLIC;
+ mGroupHelper.onNotificationPosted(sbn, true);
+
+ // Check that the summary visibility was updated
+ NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
+ VISIBILITY_PUBLIC);
verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(newAttr));
}
@@ -781,7 +822,7 @@ public class GroupHelperTest extends UiServiceTestCase {
when(initialIcon.sameAs(initialIcon)).thenReturn(true);
final int initialIconColor = Color.BLUE;
final NotificationAttributes initialAttr = new NotificationAttributes(
- GroupHelper.FLAG_INVALID, initialIcon, initialIconColor);
+ GroupHelper.FLAG_INVALID, initialIcon, initialIconColor, DEFAULT_VISIBILITY);
// Add AUTOGROUP_AT_COUNT-1 notifications with same icon and color
ArrayList<StatusBarNotification> notifications = new ArrayList<>();
@@ -817,11 +858,12 @@ public class GroupHelperTest extends UiServiceTestCase {
// Create notifications with the same icon
List<NotificationAttributes> childrenAttr = new ArrayList<>();
for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
- childrenAttr.add(new NotificationAttributes(0, icon, COLOR_DEFAULT));
+ childrenAttr.add(new NotificationAttributes(0, icon, COLOR_DEFAULT,
+ DEFAULT_VISIBILITY));
}
//Check that the generated summary icon is the same as the child notifications'
- Icon summaryIcon = mGroupHelper.getAutobundledSummaryIconAndColor(pkg, childrenAttr).icon;
+ Icon summaryIcon = mGroupHelper.getAutobundledSummaryAttributes(pkg, childrenAttr).icon;
assertThat(summaryIcon).isEqualTo(icon);
}
@@ -837,11 +879,12 @@ public class GroupHelperTest extends UiServiceTestCase {
// Create notifications with different icons
List<NotificationAttributes> childrenAttr = new ArrayList<>();
for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
- childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), COLOR_DEFAULT));
+ childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), COLOR_DEFAULT,
+ DEFAULT_VISIBILITY));
}
// Check that the generated summary icon is the monochrome icon
- Icon summaryIcon = groupHelper.getAutobundledSummaryIconAndColor(pkg, childrenAttr).icon;
+ Icon summaryIcon = groupHelper.getAutobundledSummaryAttributes(pkg, childrenAttr).icon;
assertThat(summaryIcon).isEqualTo(monochromeIcon);
}
@@ -853,11 +896,12 @@ public class GroupHelperTest extends UiServiceTestCase {
// Create notifications with the same icon color
List<NotificationAttributes> childrenAttr = new ArrayList<>();
for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
- childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor));
+ childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor,
+ DEFAULT_VISIBILITY));
}
// Check that the generated summary icon color is the same as the child notifications'
- int summaryIconColor = mGroupHelper.getAutobundledSummaryIconAndColor(pkg,
+ int summaryIconColor = mGroupHelper.getAutobundledSummaryAttributes(pkg,
childrenAttr).iconColor;
assertThat(summaryIconColor).isEqualTo(iconColor);
}
@@ -869,17 +913,62 @@ public class GroupHelperTest extends UiServiceTestCase {
// Create notifications with different icon colors
List<NotificationAttributes> childrenAttr = new ArrayList<>();
for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
- childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), i));
+ childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), i,
+ DEFAULT_VISIBILITY));
}
// Check that the generated summary icon color is the default color
- int summaryIconColor = mGroupHelper.getAutobundledSummaryIconAndColor(pkg,
+ int summaryIconColor = mGroupHelper.getAutobundledSummaryAttributes(pkg,
childrenAttr).iconColor;
assertThat(summaryIconColor).isEqualTo(Notification.COLOR_DEFAULT);
}
@Test
@EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+ public void testAutobundledSummaryVisibility_hasPublicChildren() {
+ final String pkg = "package";
+ final int iconColor = Color.BLUE;
+ // Create notifications with private and public visibility
+ List<NotificationAttributes> childrenAttr = new ArrayList<>();
+ childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor,
+ VISIBILITY_PUBLIC));
+ for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+ childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor,
+ VISIBILITY_PRIVATE));
+ }
+
+ // Check that the generated summary visibility is public
+ int summaryVisibility = mGroupHelper.getAutobundledSummaryAttributes(pkg,
+ childrenAttr).visibility;
+ assertThat(summaryVisibility).isEqualTo(VISIBILITY_PUBLIC);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+ public void testAutobundledSummaryVisibility_noPublicChildren() {
+ final String pkg = "package";
+ final int iconColor = Color.BLUE;
+ int visibility = VISIBILITY_PRIVATE;
+ // Create notifications with either private or secret visibility
+ List<NotificationAttributes> childrenAttr = new ArrayList<>();
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ if (i % 2 == 0) {
+ visibility = VISIBILITY_PRIVATE;
+ } else {
+ visibility = VISIBILITY_SECRET;
+ }
+ childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor,
+ visibility));
+ }
+
+ // Check that the generated summary visibility is private
+ int summaryVisibility = mGroupHelper.getAutobundledSummaryAttributes(pkg,
+ childrenAttr).visibility;
+ assertThat(summaryVisibility).isEqualTo(VISIBILITY_PRIVATE);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
public void testMonochromeAppIcon_adaptiveIconExists() throws Exception {
final String pkg = "testPackage";
final int monochromeIconResId = 1234;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 6aacfd706adc..94d24a91bbc9 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -37,6 +37,7 @@ import static android.app.Notification.FLAG_NO_CLEAR;
import static android.app.Notification.FLAG_ONGOING_EVENT;
import static android.app.Notification.FLAG_ONLY_ALERT_ONCE;
import static android.app.Notification.FLAG_USER_INITIATED_JOB;
+import static android.app.Notification.VISIBILITY_PRIVATE;
import static android.app.NotificationChannel.USER_LOCKED_ALLOW_BUBBLE;
import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED;
import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL;
@@ -2338,7 +2339,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
mService.updateAutobundledSummaryLocked(0, "pkg",
new NotificationAttributes(GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT,
- mock(Icon.class), 0), false);
+ mock(Icon.class), 0, VISIBILITY_PRIVATE), false);
waitForIdle();
assertTrue(summary.getSbn().isOngoing());
@@ -2357,7 +2358,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
mService.updateAutobundledSummaryLocked(0, "pkg",
new NotificationAttributes(GroupHelper.BASE_FLAGS,
- mock(Icon.class), 0), false);
+ mock(Icon.class), 0, VISIBILITY_PRIVATE), false);
waitForIdle();
assertFalse(summary.getSbn().isOngoing());
@@ -3479,7 +3480,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
when(mPermissionHelper.isPermissionFixed(PKG, temp.getUserId())).thenReturn(true);
NotificationRecord r = mService.createAutoGroupSummary(temp.getUserId(),
- temp.getSbn().getPackageName(), temp.getKey(), 0, mock(Icon.class), 0);
+ temp.getSbn().getPackageName(), temp.getKey(), 0, mock(Icon.class), 0,
+ VISIBILITY_PRIVATE);
assertThat(r.isImportanceFixed()).isTrue();
}
@@ -12215,9 +12217,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
// grouphelper is a mock here, so make the calls it would make
// add summary
- mService.addNotification(mService.createAutoGroupSummary(nr1.getUserId(),
- nr1.getSbn().getPackageName(), nr1.getKey(),
- GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT, mock(Icon.class), 0));
+ mService.addNotification(
+ mService.createAutoGroupSummary(nr1.getUserId(), nr1.getSbn().getPackageName(),
+ nr1.getKey(), GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT, mock(Icon.class), 0,
+ VISIBILITY_PRIVATE));
// cancel both children
mBinderService.cancelNotificationWithTag(PKG, PKG, nr0.getSbn().getTag(),
@@ -12246,7 +12249,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
mService.addNotification(nr1);
mService.addNotification(
mService.createAutoGroupSummary(nr1.getUserId(), nr1.getSbn().getPackageName(),
- nr1.getKey(), GroupHelper.BASE_FLAGS, mock(Icon.class), 0));
+ nr1.getKey(), GroupHelper.BASE_FLAGS, mock(Icon.class), 0, VISIBILITY_PRIVATE));
// add notifications + summary for USER_ALL
NotificationRecord nr0_all =
@@ -12259,7 +12262,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
mService.addNotification(
mService.createAutoGroupSummary(nr0_all.getUserId(),
nr0_all.getSbn().getPackageName(),
- nr0_all.getKey(), GroupHelper.BASE_FLAGS, mock(Icon.class), 0));
+ nr0_all.getKey(), GroupHelper.BASE_FLAGS, mock(Icon.class), 0, VISIBILITY_PRIVATE));
// cancel both children for USER_ALL
mBinderService.cancelNotificationWithTag(PKG, PKG, nr0_all.getSbn().getTag(),
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
index 20a0850db6b8..368a96b372fe 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
@@ -73,6 +73,8 @@ import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.SharedMemory;
+import android.service.voice.AlwaysOnHotwordDetector;
+import android.service.voice.HotwordAudioStream;
import android.service.voice.HotwordDetectedResult;
import android.service.voice.HotwordDetectionService;
import android.service.voice.HotwordDetectionServiceFailure;
@@ -81,6 +83,7 @@ import android.service.voice.HotwordRejectedResult;
import android.service.voice.IDspHotwordDetectionCallback;
import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
import android.service.voice.VisualQueryDetectionServiceFailure;
+import android.service.voice.VoiceInteractionManagerInternal.WearableHotwordDetectionCallback;
import android.text.TextUtils;
import android.util.Pair;
import android.util.Slog;
@@ -405,7 +408,83 @@ abstract class DetectorSession {
audioStream,
audioFormat,
options,
- callback);
+ callback,
+ /* shouldCloseAudioStreamWithDelayOnDetect= */ true);
+ }
+
+ void startListeningFromWearableLocked(
+ ParcelFileDescriptor audioStream,
+ AudioFormat audioFormat,
+ PersistableBundle options,
+ WearableHotwordDetectionCallback wearableCallback) {
+ if (DEBUG) {
+ Slog.d(TAG, "startListeningFromWearableLocked");
+ }
+ IMicrophoneHotwordDetectionVoiceInteractionCallback voiceInteractionCallback =
+ new IMicrophoneHotwordDetectionVoiceInteractionCallback() {
+ @Override
+ public void onDetected(
+ HotwordDetectedResult hotwordDetectedResult,
+ AudioFormat audioFormatFromCallback,
+ ParcelFileDescriptor audioStreamFromCallback) {
+ wearableCallback.onDetected();
+ try {
+ // This uses the DSP hotword code path to send the result to
+ // AlwaysOnHotwordDetector. DSP trigger and wearable trigger operates
+ // independently.
+ mCallback.onKeyphraseDetectedFromExternalSource(hotwordDetectedResult);
+ } catch (RemoteException ex) {
+ Slog.w(
+ TAG,
+ "RemoteException when sending HotwordDetectedResult to"
+ + " VoiceInteractionService.",
+ ex);
+ wearableCallback.onError(
+ "RemoteException when sending HotwordDetectedResult to"
+ + " VoiceInteractionService.");
+ notifyOnDetectorRemoteException();
+ }
+
+ // Close the local copies of the file descriptors after sending them to
+ // another process.
+ for (HotwordAudioStream resultAudioStream :
+ hotwordDetectedResult.getAudioStreams()) {
+ try {
+ resultAudioStream.getAudioStreamParcelFileDescriptor().close();
+ } catch (IOException ex) {
+ Slog.i(
+ TAG,
+ "Unable to close audio stream parcel file descriptor,",
+ ex);
+ }
+ }
+ }
+
+ @Override
+ public void onHotwordDetectionServiceFailure(
+ HotwordDetectionServiceFailure hotwordDetectionServiceFailure) {
+ wearableCallback.onError(
+ "onHotwordDetectionServiceFailure: "
+ + hotwordDetectionServiceFailure);
+ }
+
+ @Override
+ public void onRejected(HotwordRejectedResult hotwordRejectedResult) {
+ wearableCallback.onRejected();
+ }
+
+ @Override
+ public IBinder asBinder() {
+ // This callback will only be used locally within the same process.
+ return null;
+ }
+ };
+ handleExternalSourceHotwordDetectionLocked(
+ audioStream,
+ audioFormat,
+ options,
+ voiceInteractionCallback,
+ /* shouldCloseAudioStreamWithDelayOnDetect= */ false);
}
@SuppressWarnings("GuardedBy")
@@ -413,7 +492,8 @@ abstract class DetectorSession {
ParcelFileDescriptor audioStream,
AudioFormat audioFormat,
@Nullable PersistableBundle options,
- IMicrophoneHotwordDetectionVoiceInteractionCallback callback) {
+ IMicrophoneHotwordDetectionVoiceInteractionCallback callback,
+ boolean shouldCloseAudioStreamWithDelayOnDetect) {
if (DEBUG) {
Slog.d(TAG, "#handleExternalSourceHotwordDetectionLocked");
}
@@ -482,12 +562,22 @@ abstract class DetectorSession {
// TODO: what if we cancelled and started a new one?
mRemoteDetectionService.run(
service -> {
+ PersistableBundle optionsToSend = options;
+ if (android.app.wearable.Flags.enableHotwordWearableSensingApi()) {
+ if (optionsToSend == null) {
+ optionsToSend = new PersistableBundle();
+ }
+ optionsToSend.putBoolean(
+ HotwordDetectionService
+ .KEY_SYSTEM_WILL_CLOSE_AUDIO_STREAM_AFTER_CALLBACK,
+ shouldCloseAudioStreamWithDelayOnDetect);
+ }
service.detectFromMicrophoneSource(
serviceAudioSource,
// TODO: consider making a proxy callback + copy of audio format
AUDIO_SOURCE_EXTERNAL,
audioFormat,
- options,
+ optionsToSend,
new IDspHotwordDetectionCallback.Stub() {
@Override
public void onRejected(HotwordRejectedResult result)
@@ -530,18 +620,23 @@ abstract class DetectorSession {
getDetectorType(),
METRICS_EXTERNAL_SOURCE_DETECTED,
mVoiceInteractionServiceUid);
- mScheduledExecutorService.schedule(
- () -> {
- bestEffortClose(serviceAudioSink, audioSource);
- },
- EXTERNAL_HOTWORD_CLEANUP_MILLIS,
- TimeUnit.MILLISECONDS);
-
+ if (shouldCloseAudioStreamWithDelayOnDetect) {
+ mScheduledExecutorService.schedule(
+ () -> {
+ bestEffortClose(
+ serviceAudioSink, audioSource);
+ },
+ EXTERNAL_HOTWORD_CLEANUP_MILLIS,
+ TimeUnit.MILLISECONDS);
+ }
try {
enforcePermissionsForDataDelivery();
} catch (SecurityException e) {
- Slog.w(TAG, "Ignoring #onDetected due to a "
- + "SecurityException", e);
+ Slog.w(
+ TAG,
+ "Ignoring #onDetected due to a "
+ + "SecurityException",
+ e);
HotwordMetricsLogger.writeDetectorEvent(
getDetectorType(),
EXTERNAL_SOURCE_DETECT_SECURITY_EXCEPTION,
@@ -560,11 +655,16 @@ abstract class DetectorSession {
}
HotwordDetectedResult newResult;
try {
- newResult = mHotwordAudioStreamCopier
- .startCopyingAudioStreams(triggerResult);
+ newResult =
+ mHotwordAudioStreamCopier
+ .startCopyingAudioStreams(
+ triggerResult);
} catch (IOException e) {
- Slog.w(TAG, "Ignoring #onDetected due to a "
- + "IOException", e);
+ Slog.w(
+ TAG,
+ "Ignoring #onDetected due to a "
+ + "IOException",
+ e);
// TODO: Write event
try {
callback.onHotwordDetectionServiceFailure(
@@ -578,7 +678,12 @@ abstract class DetectorSession {
return;
}
try {
- callback.onDetected(newResult, /* audioFormat= */ null,
+ // The ParcelFileDescriptors in newResult might be
+ // closed after this call. Parcelling newResult can
+ // throw an exception
+ callback.onDetected(
+ newResult,
+ /* audioFormat= */ null,
/* audioStream= */ null);
} catch (RemoteException e) {
notifyOnDetectorRemoteException();
@@ -588,8 +693,7 @@ abstract class DetectorSession {
+ HotwordDetectedResult.getUsageSize(newResult)
+ " bits from hotword trusted process");
if (mDebugHotwordLogging) {
- Slog.i(TAG,
- "Egressed detected result: " + newResult);
+ Slog.i(TAG, "Egressed detected result: " + newResult);
}
}
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index cd390a17fc4d..f1f5458f161c 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -63,6 +63,7 @@ import android.service.voice.SoundTriggerFailure;
import android.service.voice.VisualQueryDetectionService;
import android.service.voice.VisualQueryDetectionServiceFailure;
import android.service.voice.VoiceInteractionManagerInternal.HotwordDetectionServiceIdentity;
+import android.service.voice.VoiceInteractionManagerInternal.WearableHotwordDetectionCallback;
import android.speech.IRecognitionServiceManager;
import android.util.Slog;
import android.util.SparseArray;
@@ -451,6 +452,25 @@ final class HotwordDetectionConnection {
session.startListeningFromExternalSourceLocked(audioStream, audioFormat, options, callback);
}
+ public void startListeningFromWearableLocked(
+ ParcelFileDescriptor audioStream,
+ AudioFormat audioFormat,
+ PersistableBundle options,
+ WearableHotwordDetectionCallback callback) {
+ if (DEBUG) {
+ Slog.d(TAG, "startListeningFromWearableLocked");
+ }
+ DetectorSession trustedSession = getDspTrustedHotwordDetectorSessionLocked();
+ if (trustedSession == null) {
+ callback.onError(
+ "Unable to start listening from wearable because the trusted hotword detection"
+ + " session is not available.");
+ return;
+ }
+ trustedSession.startListeningFromWearableLocked(
+ audioStream, audioFormat, options, callback);
+ }
+
/**
* This method is only used by SoftwareHotwordDetector.
*/
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 4cdec705bb6c..ecb0f9689ae7 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -16,6 +16,7 @@
package com.android.server.voiceinteraction;
+
import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
@@ -74,6 +75,7 @@ import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback
import android.service.voice.IVisualQueryDetectionVoiceInteractionCallback;
import android.service.voice.IVoiceInteractionSession;
import android.service.voice.VoiceInteractionManagerInternal;
+import android.service.voice.VoiceInteractionManagerInternal.WearableHotwordDetectionCallback;
import android.service.voice.VoiceInteractionService;
import android.service.voice.VoiceInteractionServiceInfo;
import android.service.voice.VoiceInteractionSession;
@@ -319,6 +321,46 @@ public class VoiceInteractionManagerService extends SystemService {
mServiceStub.mRoleObserver.onRoleHoldersChanged(RoleManager.ROLE_ASSISTANT,
UserHandle.of(userId));
}
+
+ @Override
+ public void startListeningFromWearable(
+ ParcelFileDescriptor audioStreamFromWearable,
+ AudioFormat audioFormatFromWearable,
+ PersistableBundle options,
+ ComponentName targetVisComponentName,
+ int userId,
+ WearableHotwordDetectionCallback callback) {
+ Slog.d(TAG, "#startListeningFromWearable");
+ VoiceInteractionManagerServiceImpl impl = mServiceStub.mImpl;
+ if (impl == null) {
+ callback.onError(
+ "Unable to start listening from wearable because the service impl is"
+ + " null.");
+ return;
+ }
+ if (targetVisComponentName != null && !targetVisComponentName.equals(impl.mComponent)) {
+ callback.onError(
+ TextUtils.formatSimple(
+ "Unable to start listening from wearable because the target"
+ + " VoiceInteractionService %s is different from the current"
+ + " VoiceInteractionService %s",
+ targetVisComponentName, impl.mComponent));
+ return;
+ }
+ if (userId != impl.mUser) {
+ callback.onError(
+ TextUtils.formatSimple(
+ "Unable to start listening from wearable because the target userId"
+ + " %s is different from the current"
+ + " VoiceInteractionManagerServiceImpl's userId %s",
+ userId, impl.mUser));
+ return;
+ }
+ synchronized (mServiceStub) {
+ impl.startListeningFromWearableLocked(
+ audioStreamFromWearable, audioFormatFromWearable, options, callback);
+ }
+ }
}
// implementation entry point and binder service
@@ -1706,7 +1748,10 @@ public class VoiceInteractionManagerService extends SystemService {
if (keyphrase.equals(phrase.getText())) {
ArraySet<Locale> locales = new ArraySet<>();
locales.add(phrase.getLocale());
- return new KeyphraseMetadata(phrase.getId(), phrase.getText(), locales,
+ return new KeyphraseMetadata(
+ phrase.getId(),
+ phrase.getText(),
+ locales,
phrase.getRecognitionModes());
}
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 7538142d094f..84b36d5948eb 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -65,6 +65,7 @@ import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback
import android.service.voice.IVisualQueryDetectionVoiceInteractionCallback;
import android.service.voice.IVoiceInteractionService;
import android.service.voice.IVoiceInteractionSession;
+import android.service.voice.VoiceInteractionManagerInternal.WearableHotwordDetectionCallback;
import android.service.voice.VoiceInteractionService;
import android.service.voice.VoiceInteractionServiceInfo;
import android.system.OsConstants;
@@ -857,6 +858,24 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne
options, token, callback);
}
+ public void startListeningFromWearableLocked(
+ ParcelFileDescriptor audioStream,
+ AudioFormat audioFormat,
+ PersistableBundle options,
+ WearableHotwordDetectionCallback callback) {
+ if (DEBUG) {
+ Slog.d(TAG, "startListeningFromWearable");
+ }
+ if (mHotwordDetectionConnection == null) {
+ callback.onError(
+ "Unable to start listening from wearable because the hotword detection"
+ + " connection is null.");
+ return;
+ }
+ mHotwordDetectionConnection.startListeningFromWearableLocked(
+ audioStream, audioFormat, options, callback);
+ }
+
public void stopListeningFromMicLocked() {
if (DEBUG) {
Slog.d(TAG, "stopListeningFromMicLocked");