diff options
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"); |