diff options
274 files changed, 10554 insertions, 4340 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 2f843f9d6164..8547ec164084 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -1809,12 +1809,25 @@ aconfig_declarations { name: "aconfig_settingslib_flags", package: "com.android.settingslib.flags", container: "system", + exportable: true, srcs: [ "packages/SettingsLib/aconfig/settingslib.aconfig", ], } java_aconfig_library { + name: "aconfig_settingslib_exported_flags_java_lib", + aconfig_declarations: "aconfig_settingslib_flags", + defaults: ["framework-minus-apex-aconfig-java-defaults"], + mode: "exported", + min_sdk_version: "30", + apex_available: [ + "//apex_available:platform", + "com.android.permission", + ], +} + +java_aconfig_library { name: "aconfig_settingslib_flags_java_lib", aconfig_declarations: "aconfig_settingslib_flags", defaults: ["framework-minus-apex-aconfig-java-defaults"], diff --git a/MEMORY_OWNERS b/MEMORY_OWNERS index 89ce5140d8ea..12aa2951bbc9 100644 --- a/MEMORY_OWNERS +++ b/MEMORY_OWNERS @@ -2,5 +2,4 @@ surenb@google.com tjmercier@google.com kaleshsingh@google.com jyescas@google.com -carlosgalo@google.com jji@google.com diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index abdfb53537f8..999db18a1229 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -485,6 +485,11 @@ public abstract class ActivityManagerInternal { */ public static final int OOM_ADJ_REASON_FOLLOW_UP = 23; + /** + * Oom Adj Reason: Update after oom adjuster configuration has changed. + */ + public static final int OOM_ADJ_REASON_RECONFIGURATION = 24; + @IntDef(prefix = {"OOM_ADJ_REASON_"}, value = { OOM_ADJ_REASON_NONE, OOM_ADJ_REASON_ACTIVITY, @@ -510,6 +515,7 @@ public abstract class ActivityManagerInternal { OOM_ADJ_REASON_RESTRICTION_CHANGE, OOM_ADJ_REASON_COMPONENT_DISABLED, OOM_ADJ_REASON_FOLLOW_UP, + OOM_ADJ_REASON_RECONFIGURATION, }) @Retention(RetentionPolicy.SOURCE) public @interface OomAdjReason {} diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 0d219a901b9d..4c4753872c03 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -152,7 +152,7 @@ flag { name: "cache_sdk_system_features" namespace: "system_performance" description: "Feature flag to enable optimized cache for SDK-defined system feature lookups." - bug: "375000483" + bug: "326623529" } flag { diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index 852f04793f15..9c6b71b72ec8 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -417,6 +417,7 @@ public abstract class CameraDevice implements AutoCloseable { * or if any of the output configurations sets a stream use * case different from {@link * android.hardware.camera2.CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT}. + * @throws UnsupportedOperationException if the camera has been opened in shared mode * @see CameraExtensionCharacteristics#getSupportedExtensions * @see CameraExtensionCharacteristics#getExtensionSupportedSizes */ @@ -1258,7 +1259,8 @@ public abstract class CameraDevice implements AutoCloseable { * configurations are empty; or the session configuration * executor is invalid; * or the output dynamic range combination is - * invalid/unsupported. + * invalid/unsupported; or the session type is not shared when + * camera has been opened in shared mode. * @throws CameraAccessException In case the camera device is no longer connected or has * encountered a fatal error. * @see #createCaptureSession(List, CameraCaptureSession.StateCallback, Handler) @@ -1292,6 +1294,8 @@ public abstract class CameraDevice implements AutoCloseable { * @throws CameraAccessException if the camera device is no longer connected or has * encountered a fatal error * @throws IllegalStateException if the camera device has been closed + * @throws UnsupportedOperationException if this is not a primary client of a camera opened in + * shared mode */ @NonNull public abstract CaptureRequest.Builder createCaptureRequest(@RequestTemplate int templateType) @@ -1328,6 +1332,8 @@ public abstract class CameraDevice implements AutoCloseable { * @throws CameraAccessException if the camera device is no longer connected or has * encountered a fatal error * @throws IllegalStateException if the camera device has been closed + * @throws UnsupportedOperationException if this is not a primary client of a camera opened in + * shared mode * * @see #TEMPLATE_PREVIEW * @see #TEMPLATE_RECORD @@ -1369,6 +1375,7 @@ public abstract class CameraDevice implements AutoCloseable { * @throws CameraAccessException if the camera device is no longer connected or has * encountered a fatal error * @throws IllegalStateException if the camera device has been closed + * @throws UnsupportedOperationException if the camera has been opened in shared mode * * @see CaptureRequest.Builder * @see TotalCaptureResult diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index aba2345f28d8..bfaff941939c 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -1375,6 +1375,9 @@ public final class CameraManager { * @throws SecurityException if the application does not have permission to * access the camera * + * @throws UnsupportedOperationException if {@link #isCameraDeviceSharingSupported} returns + * false for the given {@code cameraId}. + * * @see #getCameraIdList * @see android.app.admin.DevicePolicyManager#setCameraDisabled * @@ -1393,6 +1396,10 @@ public final class CameraManager { if (executor == null) { throw new IllegalArgumentException("executor was null"); } + if (!isCameraDeviceSharingSupported(cameraId)) { + throw new UnsupportedOperationException( + "CameraDevice sharing is not supported for Camera ID: " + cameraId); + } openCameraImpl(cameraId, callback, executor, /*oomScoreOffset*/0, getRotationOverride(mContext), /*sharedMode*/true); } diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 496d316eb028..1c65b0882e0f 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -299,6 +299,24 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> return mRequestType; } + /** + * Get the stream ids corresponding to the target surfaces. + * + * @hide + */ + public int[] getStreamIds() { + return mStreamIdxArray; + }; + + /** + * Get the surface ids corresponding to the target surfaces. + * + * @hide + */ + public int[] getSurfaceIds() { + return mSurfaceIdxArray; + }; + // If this request is part of constrained high speed request list that was created by // {@link android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession#createHighSpeedRequestList} private boolean mIsPartOfCHSRequestList = false; diff --git a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java index ce8661e90978..7e0456b22be8 100644 --- a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java @@ -340,6 +340,30 @@ public class CameraCaptureSessionImpl extends CameraCaptureSession } } + /** + * Shared Camera capture session API which can be used by the clients + * to start streaming. + * + * @hide + */ + public int startStreaming(List<Surface> surfaces, Executor executor, + CaptureCallback callback) throws CameraAccessException { + + synchronized (mDeviceImpl.mInterfaceLock) { + checkNotClosed(); + + executor = CameraDeviceImpl.checkExecutor(executor, callback); + + if (DEBUG) { + Log.v(TAG, mIdString + "startStreaming callback " + callback + " executor" + + " " + executor); + } + + return addPendingSequence(mDeviceImpl.startStreaming(surfaces, + createCaptureCallbackProxyWithExecutor(executor, callback), mDeviceExecutor)); + } + } + private void checkRepeatingRequest(CaptureRequest request) { if (request == null) { throw new IllegalArgumentException("request must not be null"); diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index 321f09bd4760..89a6b02b56c4 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -451,6 +451,16 @@ public class CameraDeviceImpl extends CameraDevice } } + /** + * When camera device is opened in shared mode, call to check if this is a primary client. + * + */ + public boolean isPrimaryClient() { + synchronized (mInterfaceLock) { + return mIsPrimaryClient; + } + } + private Map<String, CameraCharacteristics> getPhysicalIdToChars() { if (mPhysicalIdsToChars == null) { try { @@ -858,24 +868,19 @@ public class CameraDeviceImpl extends CameraDevice List<SharedOutputConfiguration> sharedConfigs = sharedSessionConfiguration.getOutputStreamsInformation(); for (SharedOutputConfiguration sharedConfig : sharedConfigs) { - if (outConfig.getConfiguredSize().equals(sharedConfig.getSize()) - && (outConfig.getConfiguredFormat() == sharedConfig.getFormat()) - && (outConfig.getSurfaceGroupId() == OutputConfiguration.SURFACE_GROUP_ID_NONE) - && (outConfig.getSurfaceType() == sharedConfig.getSurfaceType()) + if ((outConfig.getSurfaceGroupId() == OutputConfiguration.SURFACE_GROUP_ID_NONE) && (outConfig.getMirrorMode() == sharedConfig.getMirrorMode()) - && (outConfig.getUsage() == sharedConfig.getUsage()) && (outConfig.isReadoutTimestampEnabled() == sharedConfig.isReadoutTimestampEnabled()) && (outConfig.getTimestampBase() == sharedConfig.getTimestampBase()) && (outConfig.getStreamUseCase() == sharedConfig.getStreamUseCase()) - && (outConfig.getColorSpace().equals( - sharedSessionConfiguration.getColorSpace())) && (outConfig.getDynamicRangeProfile() == DynamicRangeProfiles.STANDARD) - && (outConfig.getConfiguredDataspace() == sharedConfig.getDataspace()) && (Objects.equals(outConfig.getPhysicalCameraId(), sharedConfig.getPhysicalCameraId())) && (outConfig.getSensorPixelModes().isEmpty()) + && (!outConfig.isMultiResolution()) + && (!outConfig.isDeferredConfiguration()) && (!outConfig.isShared())) { //Found valid config, return true return true; @@ -907,14 +912,6 @@ public class CameraDeviceImpl extends CameraDevice if (config.getExecutor() == null) { throw new IllegalArgumentException("Invalid executor"); } - if (mSharedMode) { - if (config.getSessionType() != SessionConfiguration.SESSION_SHARED) { - throw new IllegalArgumentException("Invalid session type"); - } - if (!checkSharedSessionConfiguration(outputConfigs)) { - throw new IllegalArgumentException("Invalid output configurations"); - } - } createCaptureSessionInternal(config.getInputConfiguration(), outputConfigs, config.getStateCallback(), config.getExecutor(), config.getSessionType(), config.getSessionParameters()); @@ -932,17 +929,26 @@ public class CameraDeviceImpl extends CameraDevice checkIfCameraClosedOrInError(); + boolean isSharedSession = (operatingMode == ICameraDeviceUser.SHARED_MODE); + if (Flags.cameraMultiClient() && mSharedMode) { + if (!isSharedSession) { + throw new IllegalArgumentException("Invalid session type"); + } + if (!checkSharedSessionConfiguration(outputConfigurations)) { + throw new IllegalArgumentException("Invalid output configurations"); + } + if (inputConfig != null) { + throw new IllegalArgumentException("Shared capture session doesn't support" + + " input configuration yet."); + } + } + boolean isConstrainedHighSpeed = (operatingMode == ICameraDeviceUser.CONSTRAINED_HIGH_SPEED_MODE); if (isConstrainedHighSpeed && inputConfig != null) { throw new IllegalArgumentException("Constrained high speed session doesn't support" + " input configuration yet."); } - boolean isSharedSession = (operatingMode == ICameraDeviceUser.SHARED_MODE); - if (isSharedSession && inputConfig != null) { - throw new IllegalArgumentException("Shared capture session doesn't support" - + " input configuration yet."); - } if (mCurrentExtensionSession != null) { mCurrentExtensionSession.commitStats(); @@ -1004,8 +1010,7 @@ public class CameraDeviceImpl extends CameraDevice mCharacteristics); } else if (isSharedSession) { newSession = new CameraSharedCaptureSessionImpl(mNextSessionId++, - callback, executor, this, mDeviceExecutor, configureSuccess, - mIsPrimaryClient); + callback, executor, this, mDeviceExecutor, configureSuccess); } else { newSession = new CameraCaptureSessionImpl(mNextSessionId++, input, callback, executor, this, mDeviceExecutor, configureSuccess); @@ -1074,6 +1079,11 @@ public class CameraDeviceImpl extends CameraDevice synchronized(mInterfaceLock) { checkIfCameraClosedOrInError(); + if (Flags.cameraMultiClient() && mSharedMode && !mIsPrimaryClient) { + throw new UnsupportedOperationException("In shared session mode," + + "only primary clients can create capture request."); + } + for (String physicalId : physicalCameraIdSet) { if (Objects.equals(physicalId, getId())) { throw new IllegalStateException("Physical id matches the logical id!"); @@ -1100,6 +1110,11 @@ public class CameraDeviceImpl extends CameraDevice synchronized(mInterfaceLock) { checkIfCameraClosedOrInError(); + if (Flags.cameraMultiClient() && mSharedMode && !mIsPrimaryClient) { + throw new UnsupportedOperationException("In shared session mode," + + "only primary clients can create capture request."); + } + CameraMetadataNative templatedRequest = null; templatedRequest = mRemoteDevice.createDefaultRequest(templateType); @@ -1119,6 +1134,10 @@ public class CameraDeviceImpl extends CameraDevice throws CameraAccessException { synchronized(mInterfaceLock) { checkIfCameraClosedOrInError(); + if (Flags.cameraMultiClient() && mSharedMode) { + throw new UnsupportedOperationException("In shared session mode," + + "reprocess capture requests are not supported."); + } CameraMetadataNative resultMetadata = new CameraMetadataNative(inputResult.getNativeCopy()); @@ -1572,6 +1591,74 @@ public class CameraDeviceImpl extends CameraDevice } } + public int startStreaming(List<Surface> surfaces, CaptureCallback callback, + Executor executor) throws CameraAccessException { + // Need a valid executor, or current thread needs to have a looper, if + // callback is valid + executor = checkExecutor(executor, callback); + synchronized (mInterfaceLock) { + checkIfCameraClosedOrInError(); + for (Surface surface : surfaces) { + if (surface == null) { + throw new IllegalArgumentException("Null Surface targets are not allowed"); + } + } + // In shared session mode, if there are other active clients streaming then + // stoprepeating does not actually send request to HAL to cancel the request. + // Cameraservice will use this call to remove this client surfaces provided in its + // previous streaming request. If this is the only client for the shared camera device + // then camerservice will ask HAL to cancel the previous repeating request + stopRepeating(); + + // StartStreaming API does not allow capture parameters to be provided through a capture + // request. If the primary client has an existing repeating request, the camera service + // will either attach the provided surfaces to that request or create a default capture + // request if no repeating request is active. A default capture request is created here + // for initial use. The capture callback will provide capture results that include the + // actual capture parameters used for the streaming. + CaptureRequest.Builder builder = createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + for (Surface surface : surfaces) { + builder.addTarget(surface); + } + CaptureRequest request = builder.build(); + request.convertSurfaceToStreamId(mConfiguredOutputs); + + SubmitInfo requestInfo; + requestInfo = mRemoteDevice.startStreaming(request.getStreamIds(), + request.getSurfaceIds()); + request.recoverStreamIdToSurface(); + List<CaptureRequest> requestList = new ArrayList<CaptureRequest>(); + requestList.add(request); + + if (callback != null) { + mCaptureCallbackMap.put(requestInfo.getRequestId(), + new CaptureCallbackHolder( + callback, requestList, executor, true, mNextSessionId - 1)); + } else { + if (DEBUG) { + Log.d(TAG, "Listen for request " + requestInfo.getRequestId() + " is null"); + } + } + + if (mRepeatingRequestId != REQUEST_ID_NONE) { + checkEarlyTriggerSequenceCompleteLocked(mRepeatingRequestId, + requestInfo.getLastFrameNumber(), mRepeatingRequestTypes); + } + + CaptureRequest[] requestArray = requestList.toArray( + new CaptureRequest[requestList.size()]); + mRepeatingRequestId = requestInfo.getRequestId(); + mRepeatingRequestTypes = getRequestTypes(requestArray); + + if (mIdle) { + mDeviceExecutor.execute(mCallOnActive); + } + mIdle = false; + + return requestInfo.getRequestId(); + } + } + public int setRepeatingRequest(CaptureRequest request, CaptureCallback callback, Executor executor) throws CameraAccessException { List<CaptureRequest> requestList = new ArrayList<CaptureRequest>(); @@ -2894,6 +2981,11 @@ public class CameraDeviceImpl extends CameraDevice @Override public void createExtensionSession(ExtensionSessionConfiguration extensionConfiguration) throws CameraAccessException { + if (Flags.cameraMultiClient() && mSharedMode) { + throw new UnsupportedOperationException("In shared session mode," + + "extension sessions are not supported."); + } + HashMap<String, CameraCharacteristics> characteristicsMap = new HashMap<>( getPhysicalIdToChars()); characteristicsMap.put(mCameraId, mCharacteristics); @@ -2929,4 +3021,4 @@ public class CameraDeviceImpl extends CameraDevice } } } -}
\ No newline at end of file +} diff --git a/core/java/android/hardware/camera2/impl/CameraSharedCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraSharedCaptureSessionImpl.java index a1f31c0ced5e..8c0dcfb2a28c 100644 --- a/core/java/android/hardware/camera2/impl/CameraSharedCaptureSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraSharedCaptureSessionImpl.java @@ -19,6 +19,8 @@ import android.annotation.FlaggedApi; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraOfflineSession; +import android.hardware.camera2.CameraOfflineSession.CameraOfflineSessionCallback; import android.hardware.camera2.CameraSharedCaptureSession; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.OutputConfiguration; @@ -28,6 +30,7 @@ import android.view.Surface; import com.android.internal.camera.flags.Flags; +import java.util.Collection; import java.util.List; import java.util.concurrent.Executor; @@ -46,7 +49,8 @@ public class CameraSharedCaptureSessionImpl private static final String TAG = "CameraSharedCaptureSessionImpl"; private final CameraCaptureSessionImpl mSessionImpl; private final ConditionVariable mInitialized = new ConditionVariable(); - private boolean mIsPrimary; + private final android.hardware.camera2.impl.CameraDeviceImpl mCameraDevice; + private final Executor mDeviceExecutor; /** * Create a new CameraCaptureSession. @@ -54,24 +58,32 @@ public class CameraSharedCaptureSessionImpl CameraSharedCaptureSessionImpl(int id, CameraCaptureSession.StateCallback callback, Executor stateExecutor, android.hardware.camera2.impl.CameraDeviceImpl deviceImpl, - Executor deviceStateExecutor, boolean configureSuccess, boolean isPrimary) { + Executor deviceStateExecutor, boolean configureSuccess) { CameraCaptureSession.StateCallback wrapperCallback = new WrapperCallback(callback); mSessionImpl = new CameraCaptureSessionImpl(id, /*input*/null, wrapperCallback, stateExecutor, deviceImpl, deviceStateExecutor, configureSuccess); - mIsPrimary = isPrimary; + mCameraDevice = deviceImpl; + mDeviceExecutor = deviceStateExecutor; mInitialized.open(); } @Override - public int startStreaming(List<Surface> surfaces, Executor executor, CaptureCallback listener) + public int startStreaming(List<Surface> surfaces, Executor executor, CaptureCallback callback) throws CameraAccessException { - // Todo: Need to add implementation. - return 0; + if (surfaces.isEmpty()) { + throw new IllegalArgumentException("No surfaces provided for streaming"); + } else if (executor == null) { + throw new IllegalArgumentException("executor must not be null"); + } else if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + + return mSessionImpl.startStreaming(surfaces, executor, callback); } @Override public void stopStreaming() throws CameraAccessException { - // Todo: Need to add implementation. + mSessionImpl.stopRepeating(); } @Override @@ -90,16 +102,24 @@ public class CameraSharedCaptureSessionImpl } @Override + public boolean supportsOfflineProcessing(Surface surface) { + return false; + } + + @Override public void abortCaptures() throws CameraAccessException { - if (mIsPrimary) { + if (mCameraDevice.isPrimaryClient()) { mSessionImpl.abortCaptures(); + return; } + throw new UnsupportedOperationException("Shared capture session only supports this method" + + " for primary clients"); } @Override public int setRepeatingRequest(CaptureRequest request, CaptureCallback listener, Handler handler) throws CameraAccessException { - if (mIsPrimary) { + if (mCameraDevice.isPrimaryClient()) { return mSessionImpl.setRepeatingRequest(request, listener, handler); } throw new UnsupportedOperationException("Shared capture session only supports this method" @@ -107,16 +127,30 @@ public class CameraSharedCaptureSessionImpl } @Override + public int setSingleRepeatingRequest(CaptureRequest request, Executor executor, + CaptureCallback listener) + throws CameraAccessException { + if (mCameraDevice.isPrimaryClient()) { + return mSessionImpl.setSingleRepeatingRequest(request, executor, listener); + } + throw new UnsupportedOperationException("Shared capture session only supports this method" + + " for primary clients"); + } + + @Override public void stopRepeating() throws CameraAccessException { - if (mIsPrimary) { + if (mCameraDevice.isPrimaryClient()) { mSessionImpl.stopRepeating(); + return; } + throw new UnsupportedOperationException("Shared capture session only supports this method" + + " for primary clients"); } @Override public int capture(CaptureRequest request, CaptureCallback listener, Handler handler) throws CameraAccessException { - if (mIsPrimary) { + if (mCameraDevice.isPrimaryClient()) { return mSessionImpl.capture(request, listener, handler); } throw new UnsupportedOperationException("Shared capture session only supports this method" @@ -124,6 +158,17 @@ public class CameraSharedCaptureSessionImpl } @Override + public int captureSingleRequest(CaptureRequest request, Executor executor, + CaptureCallback listener) + throws CameraAccessException { + if (mCameraDevice.isPrimaryClient()) { + return mSessionImpl.captureSingleRequest(request, executor, listener); + } + throw new UnsupportedOperationException("Shared capture session only supports this method" + + " for primary clients"); + } + + @Override public void tearDown(Surface surface) throws CameraAccessException { mSessionImpl.tearDown(surface); } @@ -149,48 +194,72 @@ public class CameraSharedCaptureSessionImpl } @Override + public CameraOfflineSession switchToOffline(Collection<Surface> offlineSurfaces, + Executor executor, CameraOfflineSessionCallback listener) + throws CameraAccessException { + throw new UnsupportedOperationException("Shared capture session do not support this method" + ); + } + + @Override public int setRepeatingBurst(List<CaptureRequest> requests, CaptureCallback listener, Handler handler) throws CameraAccessException { - throw new UnsupportedOperationException("Shared Capture session doesn't support" + throw new UnsupportedOperationException("Shared Capture session do not support" + + " this method"); + } + + @Override + public int setRepeatingBurstRequests(List<CaptureRequest> requests, + Executor executor, CaptureCallback listener) + throws CameraAccessException { + throw new UnsupportedOperationException("Shared Capture session do not support" + " this method"); } @Override public int captureBurst(List<CaptureRequest> requests, CaptureCallback listener, Handler handler) throws CameraAccessException { - throw new UnsupportedOperationException("Shared Capture session doesn't support" + throw new UnsupportedOperationException("Shared Capture session do not support" + + " this method"); + } + + @Override + public int captureBurstRequests(List<CaptureRequest> requests, + Executor executor, CaptureCallback listener) + throws CameraAccessException { + throw new UnsupportedOperationException("Shared Capture session do not support" + " this method"); } @Override public void updateOutputConfiguration(OutputConfiguration config) throws CameraAccessException { - throw new UnsupportedOperationException("Shared capture session doesn't support" + throw new UnsupportedOperationException("Shared capture session do not support" + " this method"); } @Override public void finalizeOutputConfigurations(List<OutputConfiguration> deferredOutputConfigs) throws CameraAccessException { - throw new UnsupportedOperationException("Shared capture session doesn't support" + throw new UnsupportedOperationException("Shared capture session do not support" + " this method"); } @Override public void prepare(Surface surface) throws CameraAccessException { - throw new UnsupportedOperationException("Shared capture session doesn't support" + throw new UnsupportedOperationException("Shared capture session do not support" + " this method"); } @Override public void prepare(int maxCount, Surface surface) throws CameraAccessException { - throw new UnsupportedOperationException("Shared capture session doesn't support" + throw new UnsupportedOperationException("Shared capture session do not support" + " this method"); } @Override public void closeWithoutDraining() { - throw new UnsupportedOperationException("Shared capture session doesn't support" + throw new UnsupportedOperationException("Shared capture session do not support" + " this method"); } diff --git a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java index a79e084b7f41..0b8e9c2687c3 100644 --- a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java +++ b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java @@ -65,6 +65,17 @@ public class ICameraDeviceUserWrapper { } } + public SubmitInfo startStreaming(int[] streamIdxArray, int[] surfaceIdxArray) + throws CameraAccessException { + try { + return mRemoteDevice.startStreaming(streamIdxArray, surfaceIdxArray); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); + } + } + public SubmitInfo submitRequest(CaptureRequest request, boolean streaming) throws CameraAccessException { try { @@ -325,4 +336,4 @@ public class ICameraDeviceUserWrapper { } } -}
\ No newline at end of file +} diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java index e12c46322d8c..d394154a2c0e 100644 --- a/core/java/android/hardware/camera2/params/OutputConfiguration.java +++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java @@ -1803,6 +1803,19 @@ public final class OutputConfiguration implements Parcelable { } /** + * Get the flag indicating if this {@link OutputConfiguration} is for a multi-resolution output + * with a MultiResolutionImageReader. + * + * @return true if this {@link OutputConfiguration} is for a multi-resolution output with a + * MultiResolutionImageReader. + * + * @hide + */ + public boolean isMultiResolution() { + return mIsMultiResolution; + } + + /** * Get the physical camera ID associated with this {@link OutputConfiguration}. * * <p>If this OutputConfiguration isn't targeting a physical camera of a logical diff --git a/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java b/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java index cdcc92ce4404..365f870ba22d 100644 --- a/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java +++ b/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java @@ -212,7 +212,7 @@ public final class SharedSessionConfiguration { } public @Nullable String getPhysicalCameraId() { - return mPhysicalCameraId; + return mPhysicalCameraId.isEmpty() ? null : mPhysicalCameraId; } } diff --git a/core/java/android/hardware/contexthub/HubEndpoint.java b/core/java/android/hardware/contexthub/HubEndpoint.java index 1e5bed519e1a..de88895ba55c 100644 --- a/core/java/android/hardware/contexthub/HubEndpoint.java +++ b/core/java/android/hardware/contexthub/HubEndpoint.java @@ -539,7 +539,10 @@ public class HubEndpoint { return this; } - /** Attach a callback interface for lifecycle events for this Endpoint */ + /** + * Attach a callback interface for lifecycle events for this Endpoint. Callback will be + * posted to the main thread. + */ @NonNull public Builder setLifecycleCallback( @NonNull HubEndpointLifecycleCallback lifecycleCallback) { @@ -560,7 +563,10 @@ public class HubEndpoint { return this; } - /** Attach a callback interface for message events for this Endpoint */ + /** + * Attach a callback interface for message events for this Endpoint. Callback will be posted + * to the main thread. + */ @NonNull public Builder setMessageCallback(@NonNull HubEndpointMessageCallback messageCallback) { mMessageCallback = messageCallback; diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 313bad50e88e..c4d11cd8aff7 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -204,3 +204,10 @@ flag { description: "Allows the user to disable input scrolling acceleration for mouse." bug: "383555305" } + +flag { + name: "remove_fallback_modifiers" + namespace: "input" + description: "Removes modifiers from the original key event that activated the fallback, ensuring that only the intended fallback event is sent." + bug: "382545048" +} diff --git a/core/java/android/os/BinderProxy.java b/core/java/android/os/BinderProxy.java index 01222cdd38b3..18d2afb8b1b5 100644 --- a/core/java/android/os/BinderProxy.java +++ b/core/java/android/os/BinderProxy.java @@ -687,12 +687,18 @@ public final class BinderProxy implements IBinder { return removeFrozenStateChangeCallbackNative(wrappedCallback); } + public static boolean isFrozenStateChangeCallbackSupported() { + return isFrozenStateChangeCallbackSupportedNative(); + } + private native void addFrozenStateChangeCallbackNative(FrozenStateChangeCallback callback) throws RemoteException; private native boolean removeFrozenStateChangeCallbackNative( FrozenStateChangeCallback callback); + private static native boolean isFrozenStateChangeCallbackSupportedNative(); + /** * Perform a dump on the remote object * diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 132805da7c94..507bcb8c2717 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -940,10 +940,10 @@ public class UserManager { /** * Specifies if a user is disallowed from resetting network settings - * from Settings. This can only be set by device owners and profile owners on the primary user. + * from Settings. This can only be set by device owners and profile owners on the main user. * The default value is <code>false</code>. - * <p>This restriction has no effect on secondary users and managed profiles since only the - * primary user can reset the network settings of the device. + * <p>This restriction has no effect on non-Admin users since they cannot reset the network + * settings of the device. * * <p>Holders of the permission * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MOBILE_NETWORK} @@ -1077,11 +1077,11 @@ public class UserManager { /** * Specifies if a user is disallowed from configuring cell broadcasts. * - * <p>This restriction can only be set by a device owner, a profile owner on the primary + * <p>This restriction can only be set by a device owner, a profile owner on the main * user or a profile owner of an organization-owned managed profile on the parent profile. * When it is set by a device owner, it applies globally. When it is set by a profile owner - * on the primary user or by a profile owner of an organization-owned managed profile on - * the parent profile, it disables the primary user from configuring cell broadcasts. + * on the main user or by a profile owner of an organization-owned managed profile on + * the parent profile, it disables the user from configuring cell broadcasts. * * <p>Holders of the permission * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MOBILE_NETWORK} @@ -1089,8 +1089,8 @@ public class UserManager { * * <p>The default value is <code>false</code>. * - * <p>This restriction has no effect on secondary users and managed profiles since only the - * primary user can configure cell broadcasts. + * <p>This restriction has no effect on non-Admin users since they cannot configure cell + * broadcasts. * * <p>Key for user restrictions. * <p>Type: Boolean @@ -1103,11 +1103,11 @@ public class UserManager { /** * Specifies if a user is disallowed from configuring mobile networks. * - * <p>This restriction can only be set by a device owner, a profile owner on the primary + * <p>This restriction can only be set by a device owner, a profile owner on the main * user or a profile owner of an organization-owned managed profile on the parent profile. * When it is set by a device owner, it applies globally. When it is set by a profile owner - * on the primary user or by a profile owner of an organization-owned managed profile on - * the parent profile, it disables the primary user from configuring mobile networks. + * on the main user or by a profile owner of an organization-owned managed profile on + * the parent profile, it disables the user from configuring mobile networks. * * <p>Holders of the permission * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MOBILE_NETWORK} @@ -1115,8 +1115,8 @@ public class UserManager { * * <p>The default value is <code>false</code>. * - * <p>This restriction has no effect on secondary users and managed profiles since only the - * primary user can configure mobile networks. + * <p>This restriction has no effect on non-Admin users since they cannot configure mobile + * networks. * * <p>Key for user restrictions. * <p>Type: Boolean diff --git a/core/java/android/service/quickaccesswallet/QuickAccessWalletService.java b/core/java/android/service/quickaccesswallet/QuickAccessWalletService.java index 90136ae00f6a..ffe8086ca4a1 100644 --- a/core/java/android/service/quickaccesswallet/QuickAccessWalletService.java +++ b/core/java/android/service/quickaccesswallet/QuickAccessWalletService.java @@ -93,6 +93,10 @@ import android.util.Log; * must do its own state management (keeping in mind that the service's process might be killed * by the Android System when unbound; for example, if the device is running low in memory). * + * <p> The service also provides pending intents to override the system's Quick Access activities + * via the {@link #getTargetActivityPendingIntent} and the + * {@link #getGestureTargetActivityPendingIntent} method. + * * <p> * <a name="ErrorHandling"></a> * <h3>Error handling</h3> @@ -384,6 +388,10 @@ public abstract class QuickAccessWalletService extends Service { * * <p>The pending intent will be sent when the user performs a gesture to open Wallet. * The pending intent should launch an activity. + * + * <p> If the gesture is performed and this method returns null, the system will launch the + * activity specified by the {@link #getTargetActivityPendingIntent} method. If that method + * also returns null, the system will launch the system-provided card switcher activity. */ @Nullable @FlaggedApi(Flags.FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) diff --git a/core/java/android/tracing/flags.aconfig b/core/java/android/tracing/flags.aconfig index fb1bd1703ce6..6116d599baa0 100644 --- a/core/java/android/tracing/flags.aconfig +++ b/core/java/android/tracing/flags.aconfig @@ -70,3 +70,11 @@ flag { is_fixed_read_only: true bug: "352538294" } + +flag { + name: "system_server_large_perfetto_shmem_buffer" + namespace: "windowing_tools" + description: "Large perfetto shmem buffer" + is_fixed_read_only: true + bug: "382369925" +} diff --git a/core/java/android/view/KeyCharacterMap.java b/core/java/android/view/KeyCharacterMap.java index a8d4e2d2c70a..48dfdd4a95f4 100644 --- a/core/java/android/view/KeyCharacterMap.java +++ b/core/java/android/view/KeyCharacterMap.java @@ -16,6 +16,9 @@ package android.view; + +import static com.android.hardware.input.Flags.removeFallbackModifiers; + import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; @@ -458,7 +461,15 @@ public class KeyCharacterMap implements Parcelable { FallbackAction action = FallbackAction.obtain(); metaState = KeyEvent.normalizeMetaState(metaState); if (nativeGetFallbackAction(mPtr, keyCode, metaState, action)) { - action.metaState = KeyEvent.normalizeMetaState(action.metaState); + if (removeFallbackModifiers()) { + // Strip all modifiers. This is safe to do since only exact keyCode + metaState + // modifiers will trigger a fallback. + // E.g. Ctrl + Space -> language_switch (fallback generated) + // Ctrl + Alt + Space -> Ctrl + Alt + Space (no fallback generated) + action.metaState = 0; + } else { + action.metaState = KeyEvent.normalizeMetaState(action.metaState); + } return action; } action.recycle(); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 8f8bfe2865a9..d88b6d642ee6 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -34199,7 +34199,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, && viewRootImpl.shouldCheckFrameRateCategory() && parent instanceof View && ((View) parent).getFrameContentVelocity() <= 0 - && !isInputMethodWindowType) { + && !isInputMethodWindowType + && viewRootImpl.getFrameRateCompatibility() != FRAME_RATE_COMPATIBILITY_AT_LEAST) { return FRAME_RATE_CATEGORY_HIGH_HINT | FRAME_RATE_CATEGORY_REASON_BOOST; } diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig index 0d04961569e6..a9674716eb40 100644 --- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig +++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig @@ -2,16 +2,6 @@ package: "com.android.window.flags" container: "system" flag { - name: "disable_thin_letterboxing_policy" - namespace: "large_screen_experiences_app_compat" - description: "Whether reachability is disabled in case of thin letterboxing" - bug: "341027847" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "allows_screen_size_decoupled_from_status_bar_and_cutout" namespace: "large_screen_experiences_app_compat" description: "When necessary, configuration decoupled from status bar and display cutout" diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 9d11d149b0ed..80f42e587c50 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -430,4 +430,15 @@ flag { description: "Support insets definition and calculation relative to task bounds." bug: "277292497" is_fixed_read_only: true +} + +flag { + name: "exclude_drawing_app_theme_snapshot_from_lock" + namespace: "windowing_frontend" + description: "Do not hold wm lock when drawing app theme snapshot." + is_fixed_read_only: true + bug: "373502791" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index c97d3ec787b3..abd93cfaf179 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -105,6 +105,13 @@ flag { flag { namespace: "windowing_sdk" + name: "activity_embedding_support_for_connected_displays" + description: "Enables activity embedding support for connected displays, including enabling AE optimization for Settings." + bug: "369438353" +} + +flag { + namespace: "windowing_sdk" name: "wlinfo_oncreate" description: "Makes WindowLayoutInfo accessible without racing in the Activity#onCreate()" bug: "337820752" diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 5acdf32a9f84..027113a75f6b 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -482,22 +482,11 @@ cc_library_shared_for_libandroid_runtime { "libbinder", "libhidlbase", // libhwbinder is in here ], - version_script: "platform/linux/libandroid_runtime_export.txt", - }, - darwin: { - host_ldlibs: [ - "-framework AppKit", - ], - dist: { - targets: ["layoutlib_jni"], - dir: "layoutlib_native/darwin", - }, - exported_symbols_list: "platform/darwin/libandroid_runtime_export.exp", }, linux_glibc_x86_64: { ldflags: ["-static-libgcc"], dist: { - targets: ["layoutlib_jni"], + targets: ["layoutlib"], dir: "layoutlib_native/linux", tag: "stripped_all", }, diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp index 8003bb7d442b..639f5bff7614 100644 --- a/core/jni/android_util_Binder.cpp +++ b/core/jni/android_util_Binder.cpp @@ -1706,6 +1706,10 @@ static jboolean android_os_BinderProxy_removeFrozenStateChangeCallback(JNIEnv* e return res; } +static jboolean android_os_BinderProxy_frozenStateChangeCallbackSupported(JNIEnv*, jclass*) { + return ProcessState::isDriverFeatureEnabled(ProcessState::DriverFeature::FREEZE_NOTIFICATION); +} + static void BinderProxy_destroy(void* rawNativeData) { BinderProxyNativeData * nativeData = (BinderProxyNativeData *) rawNativeData; @@ -1750,6 +1754,8 @@ static const JNINativeMethod gBinderProxyMethods[] = { "(Landroid/os/IBinder$FrozenStateChangeCallback;)V", (void*)android_os_BinderProxy_addFrozenStateChangeCallback}, {"removeFrozenStateChangeCallbackNative", "(Landroid/os/IBinder$FrozenStateChangeCallback;)Z", (void*)android_os_BinderProxy_removeFrozenStateChangeCallback}, + {"isFrozenStateChangeCallbackSupportedNative", + "()Z", (void*)android_os_BinderProxy_frozenStateChangeCallbackSupported}, {"getNativeFinalizer", "()J", (void*)android_os_BinderProxy_getNativeFinalizer}, {"getExtension", "()Landroid/os/IBinder;", (void*)android_os_BinderProxy_getExtension}, }; diff --git a/core/jni/platform/darwin/libandroid_runtime_export.exp b/core/jni/platform/darwin/libandroid_runtime_export.exp deleted file mode 100644 index 00a7585719ea..000000000000 --- a/core/jni/platform/darwin/libandroid_runtime_export.exp +++ /dev/null @@ -1,38 +0,0 @@ -# -# Copyright (C) 2024 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# symbols needed for the JNI operations -_JNI_OnLoad -_ANativeWindow* - -# symbols needed to link with layoutlib_jni -___android_log* -__ZNK7android7RefBase* -__ZN7android4base9SetLogger* -__ZN7android4base10SetAborter* -__ZN7android4base11GetProperty* -__ZN7android4Rect* -__ZN7android5Fence* -__ZN7android7RefBase* -__ZN7android7String* -__ZN7android10VectorImpl* -__ZN7android11BufferQueue* -__ZN7android14AndroidRuntime* -__ZN7android14sp_report_raceEv* -__ZN7android15KeyCharacterMap* -__ZN7android15InputDeviceInfo* -__ZN7android31android_view_InputDevice_create* -__ZN7android53android_view_Surface_createFromIGraphicBufferProducer* diff --git a/core/jni/platform/linux/libandroid_runtime_export.txt b/core/jni/platform/linux/libandroid_runtime_export.txt deleted file mode 100644 index 19e3478d5d38..000000000000 --- a/core/jni/platform/linux/libandroid_runtime_export.txt +++ /dev/null @@ -1,43 +0,0 @@ -# -# Copyright (C) 2024 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -{ - global: - # symbols needed for the JNI operations - JNI_OnLoad; - ANativeWindow*; - - # symbols needed to link with layoutlib_jni - __android_log*; - _ZNK7android7RefBase*; - _ZN7android4base9SetLogger*; - _ZN7android4base10SetAborter*; - _ZN7android4base11GetProperty*; - _ZN7android4Rect*; - _ZN7android5Fence*; - _ZN7android7RefBase*; - _ZN7android7String*; - _ZN7android10VectorImpl*; - _ZN7android11BufferQueue*; - _ZN7android14AndroidRuntime*; - _ZN7android14sp_report_raceEv*; - _ZN7android15KeyCharacterMap*; - _ZN7android15InputDeviceInfo*; - _ZN7android31android_view_InputDevice_create*; - _ZN7android53android_view_Surface_createFromIGraphicBufferProducer*; - local: - *; -}; diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml index 76c810bdb2c1..e91e1115ac1c 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_base.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml @@ -157,39 +157,27 @@ android:maxDrawableHeight="@dimen/notification_right_icon_size" /> - <LinearLayout - android:id="@+id/notification_buttons_column" + <FrameLayout + android:id="@+id/expand_button_touch_container" android:layout_width="wrap_content" android:layout_height="match_parent" - android:layout_alignParentEnd="true" - android:orientation="vertical" + android:minWidth="@dimen/notification_content_margin_end" > - <include layout="@layout/notification_close_button" - android:layout_width="@dimen/notification_close_button_size" - android:layout_height="@dimen/notification_close_button_size" - android:layout_gravity="end" - android:layout_marginEnd="20dp" - /> - - <FrameLayout - android:id="@+id/expand_button_touch_container" + <include layout="@layout/notification_expand_button" android:layout_width="wrap_content" - android:layout_height="0dp" - android:layout_weight="1" - android:minWidth="@dimen/notification_content_margin_end" - > - - <include layout="@layout/notification_expand_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical|end" - /> - - </FrameLayout> + android:layout_height="wrap_content" + android:layout_gravity="center_vertical|end" + /> - </LinearLayout> + </FrameLayout> </LinearLayout> + <include layout="@layout/notification_close_button" + android:id="@+id/close_button" + android:layout_width="@dimen/notification_close_button_size" + android:layout_height="@dimen/notification_close_button_size" + android:layout_gravity="top|end" /> + </FrameLayout> diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml index 2e0a7afc3cd1..2d367337bb6f 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_media.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml @@ -194,4 +194,11 @@ </FrameLayout> </LinearLayout> + + <include layout="@layout/notification_close_button" + android:id="@+id/close_button" + android:layout_width="@dimen/notification_close_button_size" + android:layout_height="@dimen/notification_close_button_size" + android:layout_gravity="top|end" /> + </com.android.internal.widget.MediaNotificationView> diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml index f644adefda9d..fbecb8c30b9c 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml @@ -199,6 +199,12 @@ </LinearLayout> + <include layout="@layout/notification_close_button" + android:id="@+id/close_button" + android:layout_width="@dimen/notification_close_button_size" + android:layout_height="@dimen/notification_close_button_size" + android:layout_gravity="top|end" /> + </com.android.internal.widget.NotificationMaxHeightFrameLayout> <LinearLayout diff --git a/core/res/res/layout/notification_2025_template_header.xml b/core/res/res/layout/notification_2025_template_header.xml index fc727e1c72f5..2d30d8a8bbb6 100644 --- a/core/res/res/layout/notification_2025_template_header.xml +++ b/core/res/res/layout/notification_2025_template_header.xml @@ -60,7 +60,7 @@ android:layout_height="match_parent" android:layout_alignParentStart="true" android:layout_centerVertical="true" - android:layout_toStartOf="@id/notification_buttons_column" + android:layout_toStartOf="@id/expand_button" android:layout_alignWithParentIfMissing="true" android:clipChildren="false" android:gravity="center_vertical" @@ -81,28 +81,17 @@ android:focusable="false" /> - <LinearLayout - android:id="@+id/notification_buttons_column" + <include layout="@layout/notification_expand_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentEnd="true" - android:orientation="vertical" - > - - <include layout="@layout/notification_close_button" - android:layout_width="@dimen/notification_close_button_size" - android:layout_height="@dimen/notification_close_button_size" - android:layout_gravity="end" - android:layout_marginEnd="20dp" - /> - - <include layout="@layout/notification_expand_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentEnd="true" - android:layout_centerVertical="true" - /> + android:layout_centerVertical="true" + android:layout_alignParentEnd="true" /> - </LinearLayout> + <include layout="@layout/notification_close_button" + android:id="@+id/close_button" + android:layout_width="@dimen/notification_close_button_size" + android:layout_height="@dimen/notification_close_button_size" + android:layout_alignParentTop="true" + android:layout_alignParentEnd="true" /> </NotificationHeaderView> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 565e28e0cc87..45a5d85a097d 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2165,6 +2165,17 @@ config_enableGeofenceOverlay is false. --> <string name="config_geofenceProviderPackageName" translatable="false">@null</string> + <!-- Whether to enable GNSS assistance overlay which allows GnssAssistanceProvider to be + replaced by an app at run-time. When disabled, only the + config_gnssAssistanceProviderPackageName package will be searched for + GnssAssistanceProvider, otherwise any system package is eligible. Anyone who wants to + disable the overlay mechanism can set it to false. + --> + <bool name="config_enableGnssAssistanceOverlay" translatable="false">true</bool> + <!-- Package name providing GNSS assistance API support. Used only when + config_enableGnssAssistanceOverlay is false. --> + <string name="config_gnssAssistanceProviderPackageName" translatable="false">@null</string> + <!-- Whether to enable Hardware Activity-Recognition overlay which allows Hardware Activity-Recognition to be replaced by an app at run-time. When disabled, only the config_activityRecognitionHardwarePackageName package will be searched for diff --git a/core/res/res/values/config_watch.xml b/core/res/res/values/config_watch.xml index 629a343f1280..bcb1e0941b5a 100644 --- a/core/res/res/values/config_watch.xml +++ b/core/res/res/values/config_watch.xml @@ -15,8 +15,7 @@ --> <resources> - <!-- TODO(b/382103556): use predefined Material3 token --> <!-- For Wear Material3 --> - <dimen name="config_wearMaterial3_buttonCornerRadius">26dp</dimen> - <dimen name="config_wearMaterial3_bottomDialogCornerRadius">18dp</dimen> + <dimen name="config_wearMaterial3_buttonCornerRadius">@dimen/config_shapeCornerRadiusLarge</dimen> + <dimen name="config_wearMaterial3_bottomDialogCornerRadius">@dimen/config_shapeCornerRadiusMedium</dimen> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 73e06f6a2520..8195d38993c8 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2040,6 +2040,7 @@ <java-symbol type="bool" name="config_useGnssHardwareProvider" /> <java-symbol type="bool" name="config_enableGeocoderOverlay" /> <java-symbol type="bool" name="config_enableGeofenceOverlay" /> + <java-symbol type="bool" name="config_enableGnssAssistanceOverlay" /> <java-symbol type="bool" name="config_enableNetworkLocationOverlay" /> <java-symbol type="bool" name="config_sf_limitedAlpha" /> <java-symbol type="bool" name="config_unplugTurnsOnScreen" /> @@ -2223,6 +2224,7 @@ <java-symbol type="string" name="config_gnssLocationProviderPackageName" /> <java-symbol type="string" name="config_geocoderProviderPackageName" /> <java-symbol type="string" name="config_geofenceProviderPackageName" /> + <java-symbol type="string" name="config_gnssAssistanceProviderPackageName" /> <java-symbol type="string" name="config_networkLocationProviderPackageName" /> <java-symbol type="string" name="config_wimaxManagerClassname" /> <java-symbol type="string" name="config_wimaxNativeLibLocation" /> diff --git a/core/tests/coretests/res/color/color_with_lstar.xml b/core/tests/coretests/res/color/color_with_lstar.xml index dcc3d6db1b0a..7762fc069ed5 100644 --- a/core/tests/coretests/res/color/color_with_lstar.xml +++ b/core/tests/coretests/res/color/color_with_lstar.xml @@ -16,5 +16,5 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:color="#ff0000" android:lStar="50" /> + <item android:color="@color/testcolor2" android:lStar="50" /> </selector> diff --git a/core/tests/coretests/res/values/colors.xml b/core/tests/coretests/res/values/colors.xml index 029aa0dd8eb6..f01af8421515 100644 --- a/core/tests/coretests/res/values/colors.xml +++ b/core/tests/coretests/res/values/colors.xml @@ -25,6 +25,5 @@ <drawable name="yellow">#ffffff00</drawable> <color name="testcolor1">#ff00ff00</color> <color name="testcolor2">#ffff0000</color> - <color name="testcolor3">#fff00000</color> <color name="failColor">#ff0000ff</color> </resources> diff --git a/core/tests/coretests/src/android/graphics/BitmapFactoryTest.java b/core/tests/coretests/src/android/graphics/BitmapFactoryTest.java index 564460e18294..84bdbe03df13 100644 --- a/core/tests/coretests/src/android/graphics/BitmapFactoryTest.java +++ b/core/tests/coretests/src/android/graphics/BitmapFactoryTest.java @@ -16,19 +16,27 @@ package android.graphics; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + import android.os.ParcelFileDescriptor; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; import java.io.ByteArrayOutputStream; import java.io.FileDescriptor; -public class BitmapFactoryTest extends TestCase { +@RunWith(AndroidJUnit4.class) +public class BitmapFactoryTest { // tests that we can decode bitmaps from MemoryFiles @SmallTest + @Test public void testBitmapParcelFileDescriptor() throws Exception { Bitmap bitmap1 = Bitmap.createBitmap( new int[] { Color.BLUE }, 1, 1, Bitmap.Config.RGB_565); diff --git a/core/tests/coretests/src/android/graphics/BitmapTest.java b/core/tests/coretests/src/android/graphics/BitmapTest.java index 2280cf1cccfa..0126d367eb20 100644 --- a/core/tests/coretests/src/android/graphics/BitmapTest.java +++ b/core/tests/coretests/src/android/graphics/BitmapTest.java @@ -16,19 +16,28 @@ package android.graphics; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.hardware.HardwareBuffer; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.nio.ShortBuffer; -public class BitmapTest extends TestCase { +@SmallTest +@RunWith(AndroidJUnit4.class) +public class BitmapTest { - @SmallTest + @Test public void testBasic() throws Exception { Bitmap bm1 = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888); Bitmap bm2 = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565); @@ -63,7 +72,7 @@ public class BitmapTest extends TestCase { assertTrue("getConfig", bm3.getConfig() == Bitmap.Config.ARGB_8888); } - @SmallTest + @Test public void testMutability() throws Exception { Bitmap bm1 = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888); Bitmap bm2 = Bitmap.createBitmap(new int[100 * 200], 100, 200, @@ -82,7 +91,7 @@ public class BitmapTest extends TestCase { } } - @SmallTest + @Test public void testGetPixelsWithAlpha() throws Exception { int[] colors = new int[100]; for (int i = 0; i < 100; i++) { @@ -108,7 +117,7 @@ public class BitmapTest extends TestCase { } - @SmallTest + @Test public void testGetPixelsWithoutAlpha() throws Exception { int[] colors = new int[100]; for (int i = 0; i < 100; i++) { @@ -125,7 +134,7 @@ public class BitmapTest extends TestCase { } } - @SmallTest + @Test public void testSetPixelsWithAlpha() throws Exception { int[] colors = new int[100]; for (int i = 0; i < 100; i++) { @@ -151,7 +160,7 @@ public class BitmapTest extends TestCase { } } - @SmallTest + @Test public void testSetPixelsWithoutAlpha() throws Exception { int[] colors = new int[100]; for (int i = 0; i < 100; i++) { @@ -181,7 +190,7 @@ public class BitmapTest extends TestCase { return unpre; } - @SmallTest + @Test public void testSetPixelsWithNonOpaqueAlpha() throws Exception { int[] colors = new int[256]; for (int i = 0; i < 256; i++) { @@ -238,10 +247,13 @@ public class BitmapTest extends TestCase { } } - @SmallTest + private static final int GRAPHICS_USAGE = + GraphicBuffer.USAGE_HW_TEXTURE | GraphicBuffer.USAGE_SW_READ_OFTEN + | GraphicBuffer.USAGE_SW_WRITE_OFTEN; + + @Test public void testWrapHardwareBufferWithSrgbColorSpace() { - GraphicBuffer buffer = GraphicBuffer.create(10, 10, PixelFormat.RGBA_8888, - GraphicBuffer.USAGE_HW_TEXTURE | GraphicBuffer.USAGE_SOFTWARE_MASK); + GraphicBuffer buffer = GraphicBuffer.create(10, 10, PixelFormat.RGBA_8888, GRAPHICS_USAGE); Canvas canvas = buffer.lockCanvas(); canvas.drawColor(Color.YELLOW); buffer.unlockCanvasAndPost(canvas); @@ -252,10 +264,9 @@ public class BitmapTest extends TestCase { assertEquals(ColorSpace.get(ColorSpace.Named.SRGB), hardwareBitmap.getColorSpace()); } - @SmallTest + @Test public void testWrapHardwareBufferWithDisplayP3ColorSpace() { - GraphicBuffer buffer = GraphicBuffer.create(10, 10, PixelFormat.RGBA_8888, - GraphicBuffer.USAGE_HW_TEXTURE | GraphicBuffer.USAGE_SOFTWARE_MASK); + GraphicBuffer buffer = GraphicBuffer.create(10, 10, PixelFormat.RGBA_8888, GRAPHICS_USAGE); Canvas canvas = buffer.lockCanvas(); canvas.drawColor(Color.YELLOW); buffer.unlockCanvasAndPost(canvas); @@ -267,7 +278,7 @@ public class BitmapTest extends TestCase { assertEquals(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), hardwareBitmap.getColorSpace()); } - @SmallTest + @Test public void testCopyWithDirectByteBuffer() { // Initialize Bitmap final int width = 2; @@ -305,7 +316,7 @@ public class BitmapTest extends TestCase { assertTrue(bm2.sameAs(bm1)); } - @SmallTest + @Test public void testCopyWithDirectShortBuffer() { // Initialize Bitmap final int width = 2; @@ -344,7 +355,7 @@ public class BitmapTest extends TestCase { assertTrue(bm2.sameAs(bm1)); } - @SmallTest + @Test public void testCopyWithDirectIntBuffer() { // Initialize Bitmap final int width = 2; @@ -383,7 +394,7 @@ public class BitmapTest extends TestCase { assertTrue(bm2.sameAs(bm1)); } - @SmallTest + @Test public void testCopyWithHeapByteBuffer() { // Initialize Bitmap final int width = 2; @@ -420,7 +431,7 @@ public class BitmapTest extends TestCase { assertTrue(bm2.sameAs(bm1)); } - @SmallTest + @Test public void testCopyWithHeapShortBuffer() { // Initialize Bitmap final int width = 2; @@ -457,7 +468,7 @@ public class BitmapTest extends TestCase { assertTrue(bm2.sameAs(bm1)); } - @SmallTest + @Test public void testCopyWithHeapIntBuffer() { // Initialize Bitmap final int width = 2; diff --git a/core/tests/coretests/src/android/graphics/ColorStateListTest.java b/core/tests/coretests/src/android/graphics/ColorStateListTest.java index ab41bd07ac6d..5cc915e45a6f 100644 --- a/core/tests/coretests/src/android/graphics/ColorStateListTest.java +++ b/core/tests/coretests/src/android/graphics/ColorStateListTest.java @@ -16,33 +16,41 @@ package android.graphics; +import static org.junit.Assert.assertEquals; + import android.content.res.ColorStateList; import android.content.res.Resources; -import android.test.AndroidTestCase; import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.frameworks.coretests.R; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + /** - * Tests of {@link android.graphics.ColorStateList} + * Tests of {@link ColorStateList} */ -public class ColorStateListTest extends AndroidTestCase { +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ColorStateListTest { private Resources mResources; private int mFailureColor; - @Override - protected void setUp() throws Exception { - super.setUp(); - mResources = mContext.getResources(); + @Before + public void setUp() throws Exception { + mResources = InstrumentationRegistry.getInstrumentation().getContext().getResources(); mFailureColor = mResources.getColor(R.color.failColor); } - @SmallTest + @Test public void testStateIsInList() throws Exception { ColorStateList colorStateList = mResources.getColorStateList(R.color.color1); int[] focusedState = {android.R.attr.state_focused}; @@ -50,7 +58,7 @@ public class ColorStateListTest extends AndroidTestCase { assertEquals(mResources.getColor(R.color.testcolor1), focusColor); } - @SmallTest + @Test public void testStateIsInList_proto() throws Exception { ColorStateList colorStateList = recreateFromProto( mResources.getColorStateList(R.color.color1)); @@ -59,7 +67,7 @@ public class ColorStateListTest extends AndroidTestCase { assertEquals(mResources.getColor(R.color.testcolor1), focusColor); } - @SmallTest + @Test public void testEmptyState() throws Exception { ColorStateList colorStateList = mResources.getColorStateList(R.color.color1); int[] emptyState = {}; @@ -67,7 +75,7 @@ public class ColorStateListTest extends AndroidTestCase { assertEquals(mResources.getColor(R.color.testcolor2), defaultColor); } - @SmallTest + @Test public void testEmptyState_proto() throws Exception { ColorStateList colorStateList = recreateFromProto( mResources.getColorStateList(R.color.color1)); @@ -76,22 +84,23 @@ public class ColorStateListTest extends AndroidTestCase { assertEquals(mResources.getColor(R.color.testcolor2), defaultColor); } - @SmallTest + @Test public void testGetColor() throws Exception { int defaultColor = mResources.getColor(R.color.color1); assertEquals(mResources.getColor(R.color.testcolor2), defaultColor); } - @SmallTest + @Test public void testGetColorWhenListHasNoDefault() throws Exception { int defaultColor = mResources.getColor(R.color.color_no_default); assertEquals(mResources.getColor(R.color.testcolor1), defaultColor); } - @SmallTest + @Test public void testLstar() throws Exception { + var cl = ColorStateList.valueOf(mResources.getColor(R.color.testcolor2)).withLStar(50.0f); int defaultColor = mResources.getColor(R.color.color_with_lstar); - assertEquals(mResources.getColor(R.color.testcolor3), defaultColor); + assertEquals(cl.getDefaultColor(), defaultColor); } private ColorStateList recreateFromProto(ColorStateList colorStateList) throws Exception { diff --git a/core/tests/coretests/src/android/graphics/FontFileUtilTest.java b/core/tests/coretests/src/android/graphics/FontFileUtilTest.java index 52cc4cac4816..063bdf52fbd2 100644 --- a/core/tests/coretests/src/android/graphics/FontFileUtilTest.java +++ b/core/tests/coretests/src/android/graphics/FontFileUtilTest.java @@ -30,9 +30,11 @@ import android.util.Log; import android.util.Pair; import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import org.junit.Test; +import org.junit.runner.RunWith; import java.io.File; import java.io.FileInputStream; @@ -43,6 +45,7 @@ import java.nio.ByteBuffer; import java.nio.channels.FileChannel; @SmallTest +@RunWith(AndroidJUnit4.class) public class FontFileUtilTest { private static final String TAG = "FontFileUtilTest"; private static final String CACHE_FILE_PREFIX = ".font"; diff --git a/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java b/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java index 8a54e5b998e7..816bde603d36 100644 --- a/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java +++ b/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java @@ -21,10 +21,9 @@ import static com.google.common.truth.Truth.assertThat; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; -import android.test.InstrumentationTestCase; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.text.flags.Flags; @@ -37,7 +36,7 @@ import org.junit.runner.RunWith; */ @SmallTest @RunWith(AndroidJUnit4.class) -public class PaintFontVariationTest extends InstrumentationTestCase { +public class PaintFontVariationTest { @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); diff --git a/core/tests/coretests/src/android/graphics/PaintTest.java b/core/tests/coretests/src/android/graphics/PaintTest.java index 878ba703c8fe..56760d77e28b 100644 --- a/core/tests/coretests/src/android/graphics/PaintTest.java +++ b/core/tests/coretests/src/android/graphics/PaintTest.java @@ -18,19 +18,26 @@ package android.graphics; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; -import android.test.InstrumentationTestCase; import android.text.TextUtils; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.text.flags.Flags; import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; import java.util.Arrays; import java.util.HashSet; @@ -38,13 +45,14 @@ import java.util.HashSet; /** * PaintTest tests {@link Paint}. */ -public class PaintTest extends InstrumentationTestCase { +@RunWith(AndroidJUnit4.class) +public class PaintTest { @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private static final String FONT_PATH = "fonts/HintedAdvanceWidthTest-Regular.ttf"; - static void assertEquals(String message, float[] expected, float[] actual) { + static void assertFloatArrayEquals(String message, float[] expected, float[] actual) { if (expected.length != actual.length) { fail(message + " expected array length:<" + expected.length + "> but was:<" + actual.length + ">"); @@ -88,9 +96,10 @@ public class PaintTest extends InstrumentationTestCase { }; @SmallTest + @Test public void testHintingWidth() { final Typeface fontTypeface = Typeface.createFromAsset( - getInstrumentation().getContext().getAssets(), FONT_PATH); + InstrumentationRegistry.getInstrumentation().getContext().getAssets(), FONT_PATH); Paint paint = new Paint(); paint.setTypeface(fontTypeface); @@ -103,12 +112,14 @@ public class PaintTest extends InstrumentationTestCase { paint.setHinting(Paint.HINTING_OFF); paint.getTextWidths(String.valueOf(testCase.mText), widths); - assertEquals("Text width of '" + testCase.mText + "' without hinting is not expected.", + assertFloatArrayEquals( + "Text width of '" + testCase.mText + "' without hinting is not expected.", testCase.mWidthWithoutHinting, widths); paint.setHinting(Paint.HINTING_ON); paint.getTextWidths(String.valueOf(testCase.mText), widths); - assertEquals("Text width of '" + testCase.mText + "' with hinting is not expected.", + assertFloatArrayEquals( + "Text width of '" + testCase.mText + "' with hinting is not expected.", testCase.mWidthWithHinting, widths); } } @@ -131,9 +142,11 @@ public class PaintTest extends InstrumentationTestCase { return sb.toString(); } + @Test public void testHasGlyph_variationSelectors() { final Typeface fontTypeface = Typeface.createFromAsset( - getInstrumentation().getContext().getAssets(), "fonts/hasGlyphTestFont.ttf"); + InstrumentationRegistry.getInstrumentation().getContext().getAssets(), + "fonts/hasGlyphTestFont.ttf"); Paint p = new Paint(); p.setTypeface(fontTypeface); @@ -175,6 +188,7 @@ public class PaintTest extends InstrumentationTestCase { } } + @Test public void testGetTextRunAdvances() { { // LTR @@ -231,6 +245,7 @@ public class PaintTest extends InstrumentationTestCase { } } + @Test public void testGetTextRunAdvances_invalid() { Paint p = new Paint(); char[] text = "test".toCharArray(); @@ -284,6 +299,7 @@ public class PaintTest extends InstrumentationTestCase { } } + @Test public void testMeasureTextBidi() { Paint p = new Paint(); { @@ -340,18 +356,21 @@ public class PaintTest extends InstrumentationTestCase { } } + @Test public void testSetGetWordSpacing() { Paint p = new Paint(); - assertEquals(0.0f, p.getWordSpacing()); // The default value should be 0. + assertEquals(0.0f, p.getWordSpacing(), 0.0f); // The default value should be 0. p.setWordSpacing(1.0f); - assertEquals(1.0f, p.getWordSpacing()); + assertEquals(1.0f, p.getWordSpacing(), 0.0f); p.setWordSpacing(-2.0f); - assertEquals(-2.0f, p.getWordSpacing()); + assertEquals(-2.0f, p.getWordSpacing(), 0.0f); } + @Test public void testGetUnderlinePositionAndThickness() { final Typeface fontTypeface = Typeface.createFromAsset( - getInstrumentation().getContext().getAssets(), "fonts/underlineTestFont.ttf"); + InstrumentationRegistry.getInstrumentation().getContext().getAssets(), + "fonts/underlineTestFont.ttf"); final Paint p = new Paint(); final int textSize = 100; p.setTextSize(textSize); @@ -391,6 +410,7 @@ public class PaintTest extends InstrumentationTestCase { return ccByChars; } + @Test public void testCluster() { final Paint p = new Paint(); p.setTextSize(100); @@ -417,6 +437,7 @@ public class PaintTest extends InstrumentationTestCase { } @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS) + @Test public void testDerivedFromSameTypeface() { final Paint p = new Paint(); @@ -432,6 +453,7 @@ public class PaintTest extends InstrumentationTestCase { } @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS) + @Test public void testDerivedFromChained() { final Paint p = new Paint(); diff --git a/core/tests/coretests/src/android/graphics/ThreadBitmapTest.java b/core/tests/coretests/src/android/graphics/ThreadBitmapTest.java index e1ca7dfb7cc2..fbaf502596f7 100644 --- a/core/tests/coretests/src/android/graphics/ThreadBitmapTest.java +++ b/core/tests/coretests/src/android/graphics/ThreadBitmapTest.java @@ -16,17 +16,17 @@ package android.graphics; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; -public class ThreadBitmapTest extends TestCase { - - @Override - protected void setUp() throws Exception { - } +@RunWith(AndroidJUnit4.class) +public class ThreadBitmapTest { @LargeTest + @Test public void testCreation() { for (int i = 0; i < 200; i++) { @@ -44,4 +44,3 @@ public class ThreadBitmapTest extends TestCase { public void run() {} } } - diff --git a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java index 14292725506e..2b6eda8f0988 100644 --- a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java +++ b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java @@ -39,6 +39,8 @@ import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.text.flags.Flags; + import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -514,9 +516,14 @@ public class TypefaceSystemFallbackTest { assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); paint.setElegantTextHeight(false); - assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); - assertEquals(GLYPH_3EM_WIDTH, paint.measureText("b"), 0.0f); - assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + if (Flags.deprecateElegantTextHeightApi()) { + // Calling setElegantTextHeight is no-op. + assertTrue(paint.isElegantTextHeight()); + } else { + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + } } @Test @@ -553,9 +560,14 @@ public class TypefaceSystemFallbackTest { assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); paint.setElegantTextHeight(false); - assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); - assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); - assertEquals(GLYPH_3EM_WIDTH, paint.measureText("c"), 0.0f); + if (Flags.deprecateElegantTextHeightApi()) { + // Calling setElegantTextHeight is no-op. + assertTrue(paint.isElegantTextHeight()); + } else { + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("c"), 0.0f); + } testTypeface = fontMap.get("sans-serif"); assertNotNull(testTypeface); @@ -566,9 +578,14 @@ public class TypefaceSystemFallbackTest { assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); paint.setElegantTextHeight(false); - assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); - assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); - assertEquals(GLYPH_3EM_WIDTH, paint.measureText("c"), 0.0f); + if (Flags.deprecateElegantTextHeightApi()) { + // Calling setElegantTextHeight is no-op. + assertTrue(paint.isElegantTextHeight()); + } else { + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("c"), 0.0f); + } } @Test diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java index fb1efa86c236..8b4f714fbf65 100644 --- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java +++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java @@ -1171,6 +1171,88 @@ public class ViewFrameRateTest { waitForAfterDraw(); } + @Test + public void ignoreHeuristicWhenFling() throws Throwable { + if (!ViewProperties.vrr_enabled().orElse(true)) { + return; + } + + waitForFrameRateCategoryToSettle(); + FrameLayout host = new FrameLayout(mActivity); + View childView = new View(mActivity); + float velocity = 1000; + + TranslateAnimation translateAnimation = new TranslateAnimation( + Animation.RELATIVE_TO_PARENT, 0f, // fromXDelta + Animation.RELATIVE_TO_PARENT, 0f, // toXDelta + Animation.RELATIVE_TO_PARENT, 1f, // fromYDelta (100%p) + Animation.RELATIVE_TO_PARENT, 0f // toYDelta + ); + translateAnimation.setDuration(100); + + mActivityRule.runOnUiThread(() -> { + ViewGroup.LayoutParams fullSize = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mActivity.setContentView(host, fullSize); + host.setFrameContentVelocity(velocity); + ViewGroupOverlay overlay = host.getOverlay(); + overlay.add(childView); + assertEquals(velocity, host.getFrameContentVelocity()); + assertEquals(host.getFrameContentVelocity(), + ((View) childView.getParent()).getFrameContentVelocity()); + + mMovingView.startAnimation(translateAnimation); + + // The frame rate should be "Normal" during fling gestures, + // even if there's a moving View. + assertEquals(FRAME_RATE_CATEGORY_NORMAL, + mViewRoot.getLastPreferredFrameRateCategory()); + }); + waitForAfterDraw(); + } + + @Test + public void ignoreHeuristicWhenFlingMovementFirst() throws Throwable { + if (!ViewProperties.vrr_enabled().orElse(true)) { + return; + } + + waitForFrameRateCategoryToSettle(); + FrameLayout host = new FrameLayout(mActivity); + View childView = new View(mActivity); + float velocity = 1000; + + TranslateAnimation translateAnimation = new TranslateAnimation( + Animation.RELATIVE_TO_PARENT, 0f, // fromXDelta + Animation.RELATIVE_TO_PARENT, 0f, // toXDelta + Animation.RELATIVE_TO_PARENT, 1f, // fromYDelta (100%p) + Animation.RELATIVE_TO_PARENT, 0f // toYDelta + ); + translateAnimation.setDuration(100); + + mActivityRule.runOnUiThread(() -> { + mMovingView.startAnimation(translateAnimation); + + ViewGroup.LayoutParams fullSize = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mActivity.setContentView(host, fullSize); + host.setFrameContentVelocity(velocity); + ViewGroupOverlay overlay = host.getOverlay(); + overlay.add(childView); + assertEquals(velocity, host.getFrameContentVelocity()); + assertEquals(host.getFrameContentVelocity(), + ((View) childView.getParent()).getFrameContentVelocity()); + + // The frame rate should be "Normal" during fling gestures, + // even if there's a moving View. + assertEquals(FRAME_RATE_CATEGORY_NORMAL, + mViewRoot.getLastPreferredFrameRateCategory()); + }); + waitForAfterDraw(); + } + private void runAfterDraw(@NonNull Runnable runnable) { Handler handler = new Handler(Looper.getMainLooper()); mAfterDrawLatch = new CountDownLatch(1); diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 5f1fb4b44613..500548500927 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -171,3 +171,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "task_view_repository" + namespace: "multitasking" + description: "Factor task-view state tracking out of taskviewtransitions" + bug: "384976265" +} diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp index 41d1b5c15369..eecf199a3ec2 100644 --- a/libs/WindowManager/Shell/multivalentTests/Android.bp +++ b/libs/WindowManager/Shell/multivalentTests/Android.bp @@ -55,6 +55,7 @@ android_robolectric_test { "truth", "flag-junit-base", "flag-junit", + "testables", ], auto_gen_config: true, } @@ -77,6 +78,7 @@ android_test { "truth", "platform-test-annotations", "platform-test-rules", + "testables", ], libs: [ "android.test.base.stubs.system", diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt index 0d8f80935f5a..3e01256fd67c 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles.bar +import android.animation.AnimatorTestRule +import android.app.ActivityManager import android.content.Context import android.graphics.Insets import android.graphics.Rect @@ -23,7 +25,6 @@ import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.widget.FrameLayout -import androidx.core.animation.AnimatorTestRule import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -36,27 +37,34 @@ import com.android.wm.shell.bubbles.BubbleExpandedViewManager import com.android.wm.shell.bubbles.BubbleLogger import com.android.wm.shell.bubbles.BubbleOverflow import com.android.wm.shell.bubbles.BubblePositioner +import com.android.wm.shell.bubbles.BubbleTaskView import com.android.wm.shell.bubbles.DeviceConfig import com.android.wm.shell.bubbles.FakeBubbleExpandedViewManager import com.android.wm.shell.bubbles.FakeBubbleFactory -import com.android.wm.shell.bubbles.FakeBubbleTaskViewFactory +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewTaskController import com.google.common.truth.Truth.assertThat import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import org.junit.After import org.junit.Before -import org.junit.ClassRule +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever /** Tests for [BubbleBarAnimationHelper] */ @SmallTest @RunWith(AndroidJUnit4::class) class BubbleBarAnimationHelperTest { - companion object { - @JvmField @ClassRule val animatorTestRule: AnimatorTestRule = AnimatorTestRule() + @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this) + companion object { const val SCREEN_WIDTH = 2000 const val SCREEN_HEIGHT = 1000 } @@ -148,6 +156,26 @@ class BubbleBarAnimationHelperTest { } @Test + fun animateSwitch_bubbleToBubble_updateTaskBounds() { + val fromBubble = createBubble("from").initialize(container) + val toBubbleTaskController = mock<TaskViewTaskController>() + val toBubble = createBubble("to", toBubbleTaskController).initialize(container) + + getInstrumentation().runOnMainSync { + animationHelper.animateSwitch(fromBubble, toBubble) {} + // Start the animation, but don't finish + animatorTestRule.advanceTimeBy(100) + } + getInstrumentation().waitForIdleSync() + // Clear invocations to ensure that bounds update happens after animation ends + clearInvocations(toBubbleTaskController) + getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(900) } + getInstrumentation().waitForIdleSync() + + verify(toBubbleTaskController).setWindowBounds(any()) + } + + @Test fun animateSwitch_bubbleToOverflow_oldHiddenNewShown() { val fromBubble = createBubble(key = "from").initialize(container) val overflow = createOverflow().initialize(container) @@ -193,13 +221,43 @@ class BubbleBarAnimationHelperTest { assertThat(toBubble.bubbleBarExpandedView?.isSurfaceZOrderedOnTop).isFalse() } - private fun createBubble(key: String): Bubble { + @Test + fun animateToRestPosition_updateTaskBounds() { + val taskController = mock<TaskViewTaskController>() + val bubble = createBubble("key", taskController).initialize(container) + + getInstrumentation().runOnMainSync { + animationHelper.animateExpansion(bubble) {} + animatorTestRule.advanceTimeBy(1000) + } + getInstrumentation().waitForIdleSync() + getInstrumentation().runOnMainSync { + animationHelper.animateToRestPosition() + animatorTestRule.advanceTimeBy(100) + } + // Clear invocations to ensure that bounds update happens after animation ends + clearInvocations(taskController) + getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(900) } + getInstrumentation().waitForIdleSync() + + verify(taskController).setWindowBounds(any()) + } + + private fun createBubble( + key: String, + taskViewTaskController: TaskViewTaskController = mock<TaskViewTaskController>(), + ): Bubble { + val taskView = TaskView(context, taskViewTaskController) + val taskInfo = mock<ActivityManager.RunningTaskInfo>() + whenever(taskViewTaskController.taskInfo).thenReturn(taskInfo) + val bubbleTaskView = BubbleTaskView(taskView, mainExecutor) + val bubbleBarExpandedView = FakeBubbleFactory.createExpandedView( context, bubblePositioner, expandedViewManager, - FakeBubbleTaskViewFactory(context, mainExecutor).create(), + bubbleTaskView, mainExecutor, bgExecutor, bubbleLogger, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java index 3e8a9b64dac6..3188e5b9c6d2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java @@ -463,6 +463,7 @@ public class BubbleBarAnimationHelper { super.onAnimationEnd(animation); bbev.resetPivot(); bbev.setDragging(false); + updateExpandedView(bbev); } }); startNewAnimator(animatorSet); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index c74bf53268f9..9ebb7f5aa270 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -643,7 +643,9 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged t.setPosition(animatingLeash, x, endY); t.setAlpha(animatingLeash, 1.f); } - dispatchEndPositioning(mDisplayId, mCancelled, t); + if (!android.view.inputmethod.Flags.refactorInsetsController()) { + dispatchEndPositioning(mDisplayId, mCancelled, t); + } if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) { ImeTracker.forLogging().onProgress(mStatsToken, ImeTracker.PHASE_WM_ANIMATION_RUNNING); @@ -659,6 +661,14 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged ImeTracker.forLogging().onCancelled(mStatsToken, ImeTracker.PHASE_WM_ANIMATION_RUNNING); } + if (android.view.inputmethod.Flags.refactorInsetsController()) { + // In split screen, we also set {@link + // WindowContainer#mExcludeInsetsTypes} but this should only happen after + // the IME client visibility was set. Otherwise the insets will we + // dispatched too early, and we get a flicker. Thus, only dispatching it + // after reporting that the IME is hidden to system server. + dispatchEndPositioning(mDisplayId, mCancelled, t); + } if (DEBUG_IME_VISIBILITY) { EventLog.writeEvent(IMF_IME_REMOTE_ANIM_END, mStatsToken != null ? mStatsToken.getTag() : ImeTracker.TOKEN_NONE, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 7928e5ed4188..a7a5f09c88f8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -935,10 +935,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // back to the decoration using // {@link DesktopModeWindowDecoration#setOnMaximizeOrRestoreClickListener}, which // should shared with the maximize menu's maximize/restore actions. + final DesktopRepository desktopRepository = mDesktopUserRepositories.getProfile( + decoration.mTaskInfo.userId); if (Flags.enableFullyImmersiveInDesktop() - && TaskInfoKt.getRequestingImmersive(decoration.mTaskInfo)) { - // Task is requesting immersive, so it should either enter or exit immersive, - // depending on immersive state. + && desktopRepository.isTaskInFullImmersiveState( + decoration.mTaskInfo.taskId)) { + // Task is in immersive and should exit. onEnterOrExitImmersive(decoration.mTaskInfo); } else { // Full immersive is disabled or task doesn't request/support it, so just diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index aead0a7afb53..ffe8e7135513 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -1054,26 +1054,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun testImmersiveButtonClick_entersImmersiveMode() { - val onClickListenerCaptor = forClass(View.OnClickListener::class.java) - as ArgumentCaptor<View.OnClickListener> - val decor = createOpenTaskDecoration( - windowingMode = WINDOWING_MODE_FREEFORM, - onCaptionButtonClickListener = onClickListenerCaptor, - requestingImmersive = true, - ) - val view = mock(View::class.java) - whenever(view.id).thenReturn(R.id.maximize_window) - whenever(mockDesktopRepository.isTaskInFullImmersiveState(decor.mTaskInfo.taskId)) - .thenReturn(false) - - onClickListenerCaptor.value.onClick(view) - - verify(mockDesktopImmersiveController).moveTaskToImmersive(decor.mTaskInfo) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun testImmersiveRestoreButtonClick_exitsImmersiveMode() { val onClickListenerCaptor = forClass(View.OnClickListener::class.java) as ArgumentCaptor<View.OnClickListener> diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index 1bc15d72bacc..cc4a29b31996 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -199,6 +199,7 @@ cc_test { // This is to suppress warnings/errors from gtest "-Wno-unnamed-type-template-args", ], + require_root: true, srcs: [ // Helpers/infra for testing. "tests/CommonHelpers.cpp", diff --git a/libs/androidfw/TypeWrappers.cpp b/libs/androidfw/TypeWrappers.cpp index 70d14a11830e..970463492c1a 100644 --- a/libs/androidfw/TypeWrappers.cpp +++ b/libs/androidfw/TypeWrappers.cpp @@ -16,8 +16,6 @@ #include <androidfw/TypeWrappers.h> -#include <algorithm> - namespace android { TypeVariant::TypeVariant(const ResTable_type* data) : data(data), mLength(dtohl(data->entryCount)) { @@ -31,30 +29,44 @@ TypeVariant::TypeVariant(const ResTable_type* data) : data(data), mLength(dtohl( ALOGE("Type's entry indices extend beyond its boundaries"); mLength = 0; } else { - mLength = ResTable_sparseTypeEntry{entryIndices[entryCount - 1]}.idx + 1; + mLength = dtohs(ResTable_sparseTypeEntry{entryIndices[entryCount - 1]}.idx) + 1; } } } TypeVariant::iterator& TypeVariant::iterator::operator++() { - mIndex++; + ++mIndex; if (mIndex > mTypeVariant->mLength) { mIndex = mTypeVariant->mLength; } - return *this; -} -static bool keyCompare(uint32_t entry, uint16_t index) { - return dtohs(ResTable_sparseTypeEntry{entry}.idx) < index; + const ResTable_type* type = mTypeVariant->data; + if ((type->flags & ResTable_type::FLAG_SPARSE) == 0) { + return *this; + } + + // Need to adjust |mSparseIndex| as well if we've passed its current element. + const uint32_t entryCount = dtohl(type->entryCount); + const auto entryIndices = reinterpret_cast<const uint32_t*>( + reinterpret_cast<uintptr_t>(type) + dtohs(type->header.headerSize)); + if (mSparseIndex >= entryCount) { + return *this; // done + } + const auto element = (const ResTable_sparseTypeEntry*)(entryIndices + mSparseIndex); + if (mIndex > dtohs(element->idx)) { + ++mSparseIndex; + } + + return *this; } const ResTable_entry* TypeVariant::iterator::operator*() const { - const ResTable_type* type = mTypeVariant->data; if (mIndex >= mTypeVariant->mLength) { - return NULL; + return nullptr; } - const uint32_t entryCount = dtohl(mTypeVariant->data->entryCount); + const ResTable_type* type = mTypeVariant->data; + const uint32_t entryCount = dtohl(type->entryCount); const uintptr_t containerEnd = reinterpret_cast<uintptr_t>(type) + dtohl(type->header.size); const uint32_t* const entryIndices = reinterpret_cast<const uint32_t*>( @@ -63,18 +75,19 @@ const ResTable_entry* TypeVariant::iterator::operator*() const { sizeof(uint16_t) : sizeof(uint32_t); if (reinterpret_cast<uintptr_t>(entryIndices) + (indexSize * entryCount) > containerEnd) { ALOGE("Type's entry indices extend beyond its boundaries"); - return NULL; + return nullptr; } uint32_t entryOffset; if (type->flags & ResTable_type::FLAG_SPARSE) { - auto iter = std::lower_bound(entryIndices, entryIndices + entryCount, mIndex, keyCompare); - if (iter == entryIndices + entryCount - || dtohs(ResTable_sparseTypeEntry{*iter}.idx) != mIndex) { - return NULL; + if (mSparseIndex >= entryCount) { + return nullptr; } - - entryOffset = static_cast<uint32_t>(dtohs(ResTable_sparseTypeEntry{*iter}.offset)) * 4u; + const auto element = (const ResTable_sparseTypeEntry*)(entryIndices + mSparseIndex); + if (dtohs(element->idx) != mIndex) { + return nullptr; + } + entryOffset = static_cast<uint32_t>(dtohs(element->offset)) * 4u; } else if (type->flags & ResTable_type::FLAG_OFFSET16) { auto entryIndices16 = reinterpret_cast<const uint16_t*>(entryIndices); entryOffset = offset_from16(entryIndices16[mIndex]); @@ -83,25 +96,25 @@ const ResTable_entry* TypeVariant::iterator::operator*() const { } if (entryOffset == ResTable_type::NO_ENTRY) { - return NULL; + return nullptr; } if ((entryOffset & 0x3) != 0) { ALOGE("Index %u points to entry with unaligned offset 0x%08x", mIndex, entryOffset); - return NULL; + return nullptr; } const ResTable_entry* entry = reinterpret_cast<const ResTable_entry*>( reinterpret_cast<uintptr_t>(type) + dtohl(type->entriesStart) + entryOffset); if (reinterpret_cast<uintptr_t>(entry) > containerEnd - sizeof(*entry)) { ALOGE("Entry offset at index %u points outside the Type's boundaries", mIndex); - return NULL; + return nullptr; } else if (reinterpret_cast<uintptr_t>(entry) + entry->size() > containerEnd) { ALOGE("Entry at index %u extends beyond Type's boundaries", mIndex); - return NULL; + return nullptr; } else if (entry->size() < sizeof(*entry)) { ALOGE("Entry at index %u is too small (%zu)", mIndex, entry->size()); - return NULL; + return nullptr; } return entry; } diff --git a/libs/androidfw/include/androidfw/TypeWrappers.h b/libs/androidfw/include/androidfw/TypeWrappers.h index fb2fad619011..db641b78a4e4 100644 --- a/libs/androidfw/include/androidfw/TypeWrappers.h +++ b/libs/androidfw/include/androidfw/TypeWrappers.h @@ -27,24 +27,14 @@ struct TypeVariant { class iterator { public: - iterator& operator=(const iterator& rhs) { - mTypeVariant = rhs.mTypeVariant; - mIndex = rhs.mIndex; - return *this; - } - bool operator==(const iterator& rhs) const { return mTypeVariant == rhs.mTypeVariant && mIndex == rhs.mIndex; } - bool operator!=(const iterator& rhs) const { - return mTypeVariant != rhs.mTypeVariant || mIndex != rhs.mIndex; - } - iterator operator++(int) { - uint32_t prevIndex = mIndex; + iterator prev = *this; operator++(); - return iterator(mTypeVariant, prevIndex); + return prev; } const ResTable_entry* operator->() const { @@ -60,18 +50,26 @@ struct TypeVariant { private: friend struct TypeVariant; - iterator(const TypeVariant* tv, uint32_t index) - : mTypeVariant(tv), mIndex(index) {} + + enum class Kind { Begin, End }; + iterator(const TypeVariant* tv, Kind kind) + : mTypeVariant(tv) { + mSparseIndex = mIndex = kind == Kind::Begin ? 0 : tv->mLength; + // mSparseIndex here is technically past the number of sparse entries, but it is still + // ok as it is enough to infer that this is the end iterator. + } + const TypeVariant* mTypeVariant; uint32_t mIndex; + uint32_t mSparseIndex; }; iterator beginEntries() const { - return iterator(this, 0); + return iterator(this, iterator::Kind::Begin); } iterator endEntries() const { - return iterator(this, mLength); + return iterator(this, iterator::Kind::End); } const ResTable_type* data; diff --git a/libs/androidfw/tests/TypeWrappers_test.cpp b/libs/androidfw/tests/TypeWrappers_test.cpp index ed30904ec179..d66e05805484 100644 --- a/libs/androidfw/tests/TypeWrappers_test.cpp +++ b/libs/androidfw/tests/TypeWrappers_test.cpp @@ -14,28 +14,42 @@ * limitations under the License. */ -#include <algorithm> #include <androidfw/ResourceTypes.h> #include <androidfw/TypeWrappers.h> -#include <utils/String8.h> +#include <androidfw/Util.h> + +#include <optional> +#include <vector> #include <gtest/gtest.h> namespace android { -// create a ResTable_type in memory with a vector of Res_value* -static ResTable_type* createTypeTable(std::vector<Res_value*>& values, - bool compact_entry = false, - bool short_offsets = false) +using ResValueVector = std::vector<std::optional<Res_value>>; + +// create a ResTable_type in memory +static util::unique_cptr<ResTable_type> createTypeTable( + const ResValueVector& in_values, bool compact_entry, bool short_offsets, bool sparse) { + ResValueVector sparse_values; + if (sparse) { + std::ranges::copy_if(in_values, std::back_inserter(sparse_values), + [](auto&& v) { return v.has_value(); }); + } + const ResValueVector& values = sparse ? sparse_values : in_values; + ResTable_type t{}; t.header.type = RES_TABLE_TYPE_TYPE; t.header.headerSize = sizeof(t); t.header.size = sizeof(t); t.id = 1; - t.flags = short_offsets ? ResTable_type::FLAG_OFFSET16 : 0; + t.flags = sparse + ? ResTable_type::FLAG_SPARSE + : short_offsets ? ResTable_type::FLAG_OFFSET16 : 0; - t.header.size += values.size() * (short_offsets ? sizeof(uint16_t) : sizeof(uint32_t)); + t.header.size += values.size() * + (sparse ? sizeof(ResTable_sparseTypeEntry) : + short_offsets ? sizeof(uint16_t) : sizeof(uint32_t)); t.entriesStart = t.header.size; t.entryCount = values.size(); @@ -53,9 +67,18 @@ static ResTable_type* createTypeTable(std::vector<Res_value*>& values, memcpy(p_header, &t, sizeof(t)); size_t i = 0, entry_offset = 0; - uint32_t k = 0; - for (auto const& v : values) { - if (short_offsets) { + uint32_t sparse_index = 0; + + for (auto const& v : in_values) { + if (sparse) { + if (!v) { + ++i; + continue; + } + const auto p = reinterpret_cast<ResTable_sparseTypeEntry*>(p_offsets) + sparse_index++; + p->idx = i; + p->offset = (entry_offset >> 2) & 0xffffu; + } else if (short_offsets) { uint16_t *p = reinterpret_cast<uint16_t *>(p_offsets) + i; *p = v ? (entry_offset >> 2) & 0xffffu : 0xffffu; } else { @@ -83,62 +106,92 @@ static ResTable_type* createTypeTable(std::vector<Res_value*>& values, } i++; } - return reinterpret_cast<ResTable_type*>(data); + return util::unique_cptr<ResTable_type>{reinterpret_cast<ResTable_type*>(data)}; } TEST(TypeVariantIteratorTest, shouldIterateOverTypeWithoutErrors) { - std::vector<Res_value *> values; - - Res_value *v1 = new Res_value{}; - values.push_back(v1); - - values.push_back(nullptr); - - Res_value *v2 = new Res_value{}; - values.push_back(v2); - - Res_value *v3 = new Res_value{ sizeof(Res_value), 0, Res_value::TYPE_STRING, 0x12345678}; - values.push_back(v3); + ResValueVector values; + + values.push_back(std::nullopt); + values.push_back(Res_value{}); + values.push_back(std::nullopt); + values.push_back(Res_value{}); + values.push_back(Res_value{ sizeof(Res_value), 0, Res_value::TYPE_STRING, 0x12345678}); + values.push_back(std::nullopt); + values.push_back(std::nullopt); + values.push_back(std::nullopt); + values.push_back(Res_value{ sizeof(Res_value), 0, Res_value::TYPE_STRING, 0x87654321}); // test for combinations of compact_entry and short_offsets - for (size_t i = 0; i < 4; i++) { - bool compact_entry = i & 0x1, short_offsets = i & 0x2; - ResTable_type* data = createTypeTable(values, compact_entry, short_offsets); - TypeVariant v(data); + for (size_t i = 0; i < 8; i++) { + bool compact_entry = i & 0x1, short_offsets = i & 0x2, sparse = i & 0x4; + auto data = createTypeTable(values, compact_entry, short_offsets, sparse); + TypeVariant v(data.get()); TypeVariant::iterator iter = v.beginEntries(); ASSERT_EQ(uint32_t(0), iter.index()); - ASSERT_TRUE(NULL != *iter); - ASSERT_EQ(uint32_t(0), iter->key()); + ASSERT_TRUE(NULL == *iter); ASSERT_NE(v.endEntries(), iter); - iter++; + ++iter; ASSERT_EQ(uint32_t(1), iter.index()); - ASSERT_TRUE(NULL == *iter); + ASSERT_TRUE(NULL != *iter); + ASSERT_EQ(uint32_t(1), iter->key()); ASSERT_NE(v.endEntries(), iter); iter++; ASSERT_EQ(uint32_t(2), iter.index()); + ASSERT_TRUE(NULL == *iter); + ASSERT_NE(v.endEntries(), iter); + + ++iter; + + ASSERT_EQ(uint32_t(3), iter.index()); ASSERT_TRUE(NULL != *iter); - ASSERT_EQ(uint32_t(2), iter->key()); + ASSERT_EQ(uint32_t(3), iter->key()); ASSERT_NE(v.endEntries(), iter); iter++; - ASSERT_EQ(uint32_t(3), iter.index()); + ASSERT_EQ(uint32_t(4), iter.index()); ASSERT_TRUE(NULL != *iter); ASSERT_EQ(iter->is_compact(), compact_entry); - ASSERT_EQ(uint32_t(3), iter->key()); + ASSERT_EQ(uint32_t(4), iter->key()); ASSERT_EQ(uint32_t(0x12345678), iter->value().data); ASSERT_EQ(Res_value::TYPE_STRING, iter->value().dataType); + ++iter; + + ASSERT_EQ(uint32_t(5), iter.index()); + ASSERT_TRUE(NULL == *iter); + ASSERT_NE(v.endEntries(), iter); + + ++iter; + + ASSERT_EQ(uint32_t(6), iter.index()); + ASSERT_TRUE(NULL == *iter); + ASSERT_NE(v.endEntries(), iter); + + ++iter; + + ASSERT_EQ(uint32_t(7), iter.index()); + ASSERT_TRUE(NULL == *iter); + ASSERT_NE(v.endEntries(), iter); + iter++; - ASSERT_EQ(v.endEntries(), iter); + ASSERT_EQ(uint32_t(8), iter.index()); + ASSERT_TRUE(NULL != *iter); + ASSERT_EQ(iter->is_compact(), compact_entry); + ASSERT_EQ(uint32_t(8), iter->key()); + ASSERT_EQ(uint32_t(0x87654321), iter->value().data); + ASSERT_EQ(Res_value::TYPE_STRING, iter->value().dataType); - free(data); + ++iter; + + ASSERT_EQ(v.endEntries(), iter); } } diff --git a/libs/protoutil/Android.bp b/libs/protoutil/Android.bp index 8af4b7e8f4c8..4fecf4de0312 100644 --- a/libs/protoutil/Android.bp +++ b/libs/protoutil/Android.bp @@ -59,7 +59,6 @@ cc_library { apex_available: [ "//apex_available:platform", "com.android.os.statsd", - "test_com.android.os.statsd", "com.android.uprobestats", ], } diff --git a/location/api/system-current.txt b/location/api/system-current.txt index 0c2f3adc2838..023bad26e4f5 100644 --- a/location/api/system-current.txt +++ b/location/api/system-current.txt @@ -1459,6 +1459,13 @@ package android.location.provider { field public static final String ACTION_GEOCODE_PROVIDER = "com.android.location.service.GeocodeProvider"; } + @FlaggedApi("android.location.flags.gnss_assistance_interface") public abstract class GnssAssistanceProviderBase { + ctor public GnssAssistanceProviderBase(@NonNull android.content.Context, @NonNull String); + method @NonNull public final android.os.IBinder getBinder(); + method public abstract void onRequest(@NonNull android.os.OutcomeReceiver<android.location.GnssAssistance,java.lang.Throwable>); + field public static final String ACTION_GNSS_ASSISTANCE_PROVIDER = "android.location.provider.action.GNSS_ASSISTANCE_PROVIDER"; + } + public abstract class LocationProviderBase { ctor public LocationProviderBase(@NonNull android.content.Context, @NonNull String, @NonNull android.location.provider.ProviderProperties); method @Nullable public final android.os.IBinder getBinder(); diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig index c02cc808d60c..1b38982f48c1 100644 --- a/location/java/android/location/flags/location.aconfig +++ b/location/java/android/location/flags/location.aconfig @@ -167,4 +167,4 @@ flag { namespace: "location" description: "Flag for GNSS assistance interface" bug: "209078566" -}
\ No newline at end of file +} diff --git a/location/java/android/location/provider/GnssAssistanceProviderBase.java b/location/java/android/location/provider/GnssAssistanceProviderBase.java new file mode 100644 index 000000000000..f4b26d5033a5 --- /dev/null +++ b/location/java/android/location/provider/GnssAssistanceProviderBase.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.location.provider; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Context; +import android.content.Intent; +import android.location.GnssAssistance; +import android.location.flags.Flags; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.OutcomeReceiver; +import android.os.RemoteException; +import android.util.Log; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + + +/** + * Base class for GNSS assistance providers outside the system server. + * + * <p>GNSS assistance providers should be wrapped in a non-exported service which returns the result + * of {@link #getBinder()} from the service's {@link android.app.Service#onBind(Intent)} method. The + * service should not be exported so that components other than the system server cannot bind to it. + * Alternatively, the service may be guarded by a permission that only system server can obtain. The + * service may specify metadata on its capabilities: + * + * <ul> + * <li>"serviceVersion": An integer version code to help tie break if multiple services are + * capable of implementing the geocode provider. All else equal, the service with the highest + * version code will be chosen. Assumed to be 0 if not specified. + * <li>"serviceIsMultiuser": A boolean property, indicating if the service wishes to take + * responsibility for handling changes to the current user on the device. If true, the service + * will always be bound from the system user. If false, the service will always be bound from + * the current user. If the current user changes, the old binding will be released, and a new + * binding established under the new user. Assumed to be false if not specified. + * </ul> + * + * <p>The service should have an intent filter in place for the GNSS assistance provider as + * specified by the constant in this class. + * + * <p>GNSS assistance providers are identified by their UID / package name / attribution tag. Based + * on this identity, geocode providers may be given some special privileges. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_GNSS_ASSISTANCE_INTERFACE) +@SystemApi +public abstract class GnssAssistanceProviderBase { + + /** + * The action the wrapping service should have in its intent filter to implement the GNSS + * Assistance provider. + */ + public static final String ACTION_GNSS_ASSISTANCE_PROVIDER = + "android.location.provider.action.GNSS_ASSISTANCE_PROVIDER"; + + final String mTag; + @Nullable + final String mAttributionTag; + final IBinder mBinder; + + /** + * Subclasses should pass in a context and an arbitrary tag that may be used for logcat logging + * of errors, and thus should uniquely identify the class. + */ + public GnssAssistanceProviderBase(@NonNull Context context, @NonNull String tag) { + mTag = tag; + mAttributionTag = context.getAttributionTag(); + mBinder = new GnssAssistanceProviderBase.Service(); + } + + /** + * Returns the IBinder instance that should be returned from the {@link + * android.app.Service#onBind(Intent)} method of the wrapping service. + */ + @NonNull + public final IBinder getBinder() { + return mBinder; + } + + /** + * Requests GNSS assistance data of the given arguments. The given callback must be invoked + * once. + */ + public abstract void onRequest( + @NonNull OutcomeReceiver<GnssAssistance, Throwable> callback); + + private class Service extends IGnssAssistanceProvider.Stub { + @Override + public void request(IGnssAssistanceCallback callback) { + try { + onRequest(new GnssAssistanceProviderBase.SingleUseCallback(callback)); + } catch (RuntimeException e) { + // exceptions on one-way binder threads are dropped - move to a different thread + Log.w(mTag, e); + new Handler(Looper.getMainLooper()) + .post( + () -> { + throw new AssertionError(e); + }); + } + } + } + + private static class SingleUseCallback implements + OutcomeReceiver<GnssAssistance, Throwable> { + + private final AtomicReference<IGnssAssistanceCallback> mCallback; + + SingleUseCallback(IGnssAssistanceCallback callback) { + mCallback = new AtomicReference<>(callback); + } + + @Override + public void onError(Throwable e) { + try { + Objects.requireNonNull(mCallback.getAndSet(null)).onError(); + } catch (RemoteException r) { + throw r.rethrowFromSystemServer(); + } + } + + @Override + public void onResult(GnssAssistance result) { + try { + Objects.requireNonNull(mCallback.getAndSet(null)).onResult(result); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } +} diff --git a/core/tests/coretests/src/android/graphics/GraphicsTests.java b/location/java/android/location/provider/IGnssAssistanceCallback.aidl index 70f5976843bc..ea38d08df6c2 100644 --- a/core/tests/coretests/src/android/graphics/GraphicsTests.java +++ b/location/java/android/location/provider/IGnssAssistanceCallback.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,15 +14,15 @@ * limitations under the License. */ -package android.graphics; +package android.location.provider; -import junit.framework.TestSuite; +import android.location.GnssAssistance; -public class GraphicsTests { - public static TestSuite suite() { - TestSuite suite = new TestSuite(GraphicsTests.class.getName()); - - suite.addTestSuite(BitmapTest.class); - return suite; - } +/** + * Binder interface for GNSS assistance callbacks. + * @hide + */ +oneway interface IGnssAssistanceCallback { + void onError(); + void onResult(in GnssAssistance result); } diff --git a/location/java/android/location/provider/IGnssAssistanceProvider.aidl b/location/java/android/location/provider/IGnssAssistanceProvider.aidl new file mode 100644 index 000000000000..1796e9edb347 --- /dev/null +++ b/location/java/android/location/provider/IGnssAssistanceProvider.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.location.provider; + +import android.location.provider.IGnssAssistanceCallback; + +/** + * Binder interface for services that implement GNSS assistance providers. Do not implement this + * directly, extend {@link GnssAssistanceProviderBase} instead. + * @hide + */ +oneway interface IGnssAssistanceProvider { + void request(in IGnssAssistanceCallback callback); +} diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java index 302969f58ba8..19d39234d1c6 100644 --- a/media/java/android/media/MediaCodecInfo.java +++ b/media/java/android/media/MediaCodecInfo.java @@ -245,12 +245,7 @@ public final class MediaCodecInfo { * {@link MediaCodecInfo#getCapabilitiesForType getCapabilitiesForType()}, passing a MIME type. */ public static final class CodecCapabilities { - public CodecCapabilities() { - } - - // CLASSIFICATION - private String mMime; - private int mMaxSupportedInstances; + private static final String TAG = "CodecCapabilities"; // LEGACY FIELDS @@ -628,12 +623,6 @@ public final class MediaCodecInfo { */ public int[] colorFormats; // NOTE this array is modifiable by user - // FEATURES - - private int mFlagsSupported; - private int mFlagsRequired; - private int mFlagsVerified; - /** * <b>video decoder only</b>: codec supports seamless resolution changes. */ @@ -823,122 +812,680 @@ public final class MediaCodecInfo { @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 - * include optional features that can be turned on, as well as - * features that are always on. - */ - public final boolean isFeatureSupported(String name) { - return checkFeature(name, mFlagsSupported); - } + /** package private */ interface CodecCapsIntf { + public CodecCapsIntf dup(); - /** - * Query codec feature requirements. - * <p> - * These features are required to be used by the codec, and as such, - * they are always turned on. - */ - public final boolean isFeatureRequired(String name) { - return checkFeature(name, mFlagsRequired); + public boolean isFeatureSupported(String name); + + public boolean isFeatureRequired(String name); + + public boolean isFormatSupported(MediaFormat format); + + public MediaFormat getDefaultFormat(); + + public String getMimeType(); + + public int getMaxSupportedInstances(); + + public AudioCapabilities getAudioCapabilities(); + + public VideoCapabilities getVideoCapabilities(); + + public EncoderCapabilities getEncoderCapabilities(); + + public boolean isRegular(); + + public CodecProfileLevel[] getProfileLevels(); + + public int[] getColorFormats(); } - // Flags are used for feature list creation so separate this into a private - // static class to delay reading the flags only when constructing the list. - private static class FeatureList { - private static Feature[] getDecoderFeatures() { - ArrayList<Feature> features = new ArrayList(); - features.add(new Feature(FEATURE_AdaptivePlayback, (1 << 0), true)); - features.add(new Feature(FEATURE_SecurePlayback, (1 << 1), false)); - features.add(new Feature(FEATURE_TunneledPlayback, (1 << 2), false)); - features.add(new Feature(FEATURE_PartialFrame, (1 << 3), false)); - features.add(new Feature(FEATURE_FrameParsing, (1 << 4), false)); - features.add(new Feature(FEATURE_MultipleFrames, (1 << 5), false)); - features.add(new Feature(FEATURE_DynamicTimestamp, (1 << 6), false)); - features.add(new Feature(FEATURE_LowLatency, (1 << 7), true)); - if (GetFlag(() -> android.media.codec.Flags.dynamicColorAspects())) { - features.add(new Feature(FEATURE_DynamicColorAspects, (1 << 8), true)); + /* package private */ static final class CodecCapsLegacyImpl implements CodecCapsIntf { + // errors while reading profile levels - accessed from sister capabilities + int mError; + + private CodecProfileLevel[] mProfileLevels; + private int[] mColorFormats; + + // CLASSIFICATION + private String mMime; + private int mMaxSupportedInstances; + + // FEATURES + private int mFlagsSupported; + private int mFlagsRequired; + private int mFlagsVerified; + + // NEW-STYLE CAPABILITIES + private AudioCapabilities mAudioCaps; + private VideoCapabilities mVideoCaps; + private EncoderCapabilities mEncoderCaps; + private MediaFormat mDefaultFormat; + + private MediaFormat mCapabilitiesInfo; + + public CodecProfileLevel[] getProfileLevels() { + return mProfileLevels; + } + + public int[] getColorFormats() { + return mColorFormats; + } + + public CodecCapsLegacyImpl() {} + + public CodecCapsLegacyImpl dup() { + CodecCapsLegacyImpl caps = new CodecCapsLegacyImpl(); + + caps.mProfileLevels = Arrays.copyOf(mProfileLevels, mProfileLevels.length); + caps.mColorFormats = Arrays.copyOf(mColorFormats, mColorFormats.length); + + caps.mMime = mMime; + caps.mMaxSupportedInstances = mMaxSupportedInstances; + caps.mFlagsRequired = mFlagsRequired; + caps.mFlagsSupported = mFlagsSupported; + caps.mFlagsVerified = mFlagsVerified; + caps.mAudioCaps = mAudioCaps; + caps.mVideoCaps = mVideoCaps; + caps.mEncoderCaps = mEncoderCaps; + caps.mDefaultFormat = mDefaultFormat; + caps.mCapabilitiesInfo = mCapabilitiesInfo; + + return caps; + } + + public final boolean isFeatureSupported(String name) { + return checkFeature(name, mFlagsSupported); + } + + public final boolean isFeatureRequired(String name) { + return checkFeature(name, mFlagsRequired); + } + + // Flags are used for feature list creation so separate this into a private + // static class to delay reading the flags only when constructing the list. + private static class FeatureList { + private static Feature[] getDecoderFeatures() { + ArrayList<Feature> features = new ArrayList(); + features.add(new Feature(FEATURE_AdaptivePlayback, (1 << 0), true)); + features.add(new Feature(FEATURE_SecurePlayback, (1 << 1), false)); + features.add(new Feature(FEATURE_TunneledPlayback, (1 << 2), false)); + features.add(new Feature(FEATURE_PartialFrame, (1 << 3), false)); + features.add(new Feature(FEATURE_FrameParsing, (1 << 4), false)); + features.add(new Feature(FEATURE_MultipleFrames, (1 << 5), false)); + features.add(new Feature(FEATURE_DynamicTimestamp, (1 << 6), false)); + features.add(new Feature(FEATURE_LowLatency, (1 << 7), true)); + if (GetFlag(() -> android.media.codec.Flags.dynamicColorAspects())) { + features.add(new Feature(FEATURE_DynamicColorAspects, (1 << 8), true)); + } + if (GetFlag(() -> 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)); + + return features.toArray(new Feature[0]); + }; + + private static Feature[] decoderFeatures = getDecoderFeatures(); + + private static Feature[] getEncoderFeatures() { + ArrayList<Feature> features = new ArrayList(); + + features.add(new Feature(FEATURE_IntraRefresh, (1 << 0), false)); + features.add(new Feature(FEATURE_MultipleFrames, (1 << 1), false)); + features.add(new Feature(FEATURE_DynamicTimestamp, (1 << 2), false)); + features.add(new Feature(FEATURE_QpBounds, (1 << 3), false)); + features.add(new Feature(FEATURE_EncodingStatistics, (1 << 4), false)); + features.add(new Feature(FEATURE_HdrEditing, (1 << 5), false)); + if (GetFlag(() -> android.media.codec.Flags.hlgEditing())) { + features.add(new Feature(FEATURE_HlgEditing, (1 << 6), true)); + } + if (GetFlag(() -> android.media.codec.Flags.regionOfInterest())) { + features.add(new Feature(FEATURE_Roi, (1 << 7), true)); + } + + // feature to exclude codec from REGULAR codec list + features.add(new Feature(FEATURE_SpecialCodec, (1 << 30), false, true)); + + return features.toArray(new Feature[0]); + }; + + private static Feature[] encoderFeatures = getEncoderFeatures(); + + public static Feature[] getFeatures(boolean isEncoder) { + if (isEncoder) { + return encoderFeatures; + } else { + return decoderFeatures; + } } - if (GetFlag(() -> android.media.codec.Flags.nullOutputSurface())) { - features.add(new Feature(FEATURE_DetachedSurface, (1 << 9), true)); + } + + /** @hide */ + public String[] validFeatures() { + Feature[] features = getValidFeatures(); + String[] res = new String[features.length]; + for (int i = 0; i < res.length; i++) { + if (!features[i].mInternal) { + res[i] = features[i].mName; + } } + return res; + } - // feature to exclude codec from REGULAR codec list - features.add(new Feature(FEATURE_SpecialCodec, (1 << 30), false, true)); + private Feature[] getValidFeatures() { + return FeatureList.getFeatures(isEncoder()); + } - return features.toArray(new Feature[0]); - }; + private boolean checkFeature(String name, int flags) { + for (Feature feat: getValidFeatures()) { + if (feat.mName.equals(name)) { + return (flags & feat.mValue) != 0; + } + } + return false; + } - private static Feature[] decoderFeatures = getDecoderFeatures(); + public boolean isRegular() { + // regular codecs only require default features + for (Feature feat: getValidFeatures()) { + if (!feat.mDefault && isFeatureRequired(feat.mName)) { + return false; + } + } + return true; + } - private static Feature[] getEncoderFeatures() { - ArrayList<Feature> features = new ArrayList(); + public final boolean isFormatSupported(MediaFormat format) { + final Map<String, Object> map = format.getMap(); + final String mime = (String) map.get(MediaFormat.KEY_MIME); - features.add(new Feature(FEATURE_IntraRefresh, (1 << 0), false)); - features.add(new Feature(FEATURE_MultipleFrames, (1 << 1), false)); - features.add(new Feature(FEATURE_DynamicTimestamp, (1 << 2), false)); - features.add(new Feature(FEATURE_QpBounds, (1 << 3), false)); - features.add(new Feature(FEATURE_EncodingStatistics, (1 << 4), false)); - features.add(new Feature(FEATURE_HdrEditing, (1 << 5), false)); - if (GetFlag(() -> android.media.codec.Flags.hlgEditing())) { - features.add(new Feature(FEATURE_HlgEditing, (1 << 6), true)); + // mime must match if present + if (mime != null && !mMime.equalsIgnoreCase(mime)) { + return false; } - if (GetFlag(() -> android.media.codec.Flags.regionOfInterest())) { - features.add(new Feature(FEATURE_Roi, (1 << 7), true)); + + // check feature support + for (Feature feat: getValidFeatures()) { + if (feat.mInternal) { + continue; + } + + Integer yesNo = (Integer) map.get(MediaFormat.KEY_FEATURE_ + feat.mName); + if (yesNo == null) { + continue; + } + if ((yesNo == 1 && !isFeatureSupported(feat.mName)) + || (yesNo == 0 && isFeatureRequired(feat.mName))) { + return false; + } } - // feature to exclude codec from REGULAR codec list - features.add(new Feature(FEATURE_SpecialCodec, (1 << 30), false, true)); + Integer profile = (Integer) map.get(MediaFormat.KEY_PROFILE); + Integer level = (Integer) map.get(MediaFormat.KEY_LEVEL); - return features.toArray(new Feature[0]); - }; + if (profile != null) { + if (!supportsProfileLevel(profile, level)) { + return false; + } - private static Feature[] encoderFeatures = getEncoderFeatures(); + // If we recognize this profile, check that this format is supported by the + // highest level supported by the codec for that profile. (Ignore specified + // level beyond the above profile/level check as level is only used as a + // guidance. E.g. AVC Level 1 CIF format is supported if codec supports + // level 1.1 even though max size for Level 1 is QCIF. However, MPEG2 Simple + // Profile 1080p format is not supported even if codec supports Main Profile + // Level High, as Simple Profile does not support 1080p. + CodecCapsLegacyImpl levelCaps = null; + int maxLevel = 0; + for (CodecProfileLevel pl : mProfileLevels) { + if (pl.profile == profile && pl.level > maxLevel) { + // H.263 levels are not completely ordered: + // Level45 support only implies Level10 support + if (!mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263) + || pl.level != CodecProfileLevel.H263Level45 + || maxLevel == CodecProfileLevel.H263Level10) { + maxLevel = pl.level; + } + } + } + levelCaps = createFromProfileLevel(mMime, profile, maxLevel); + // We must remove the profile from this format otherwise + // levelCaps.isFormatSupported will get into this same condition and loop + // forever. Furthermore, since levelCaps does not contain features and bitrate + // specific keys, keep only keys relevant for a level check. + Map<String, Object> levelCriticalFormatMap = new HashMap<>(map); + final Set<String> criticalKeys = isVideo() + ? VideoCapabilities.VideoCapsLegacyImpl.VIDEO_LEVEL_CRITICAL_FORMAT_KEYS + : isAudio() + ? AudioCapabilities.AudioCapsLegacyImpl.AUDIO_LEVEL_CRITICAL_FORMAT_KEYS + : null; + + // critical keys will always contain KEY_MIME, but should also contain others + // to be meaningful + if (criticalKeys != null && criticalKeys.size() > 1 && levelCaps != null) { + levelCriticalFormatMap.keySet().retainAll(criticalKeys); + + MediaFormat levelCriticalFormat = new MediaFormat(levelCriticalFormatMap); + if (!levelCaps.isFormatSupported(levelCriticalFormat)) { + return false; + } + } + } + if (mAudioCaps != null && !mAudioCaps.supportsFormat(format)) { + return false; + } + if (mVideoCaps != null && !mVideoCaps.supportsFormat(format)) { + return false; + } + if (mEncoderCaps != null && !mEncoderCaps.supportsFormat(format)) { + return false; + } + return true; + } - public static Feature[] getFeatures(boolean isEncoder) { - if (isEncoder) { - return encoderFeatures; - } else { - return decoderFeatures; + private static boolean supportsBitrate( + Range<Integer> bitrateRange, MediaFormat format) { + Map<String, Object> map = format.getMap(); + + // consider max bitrate over average bitrate for support + Integer maxBitrate = (Integer)map.get(MediaFormat.KEY_MAX_BIT_RATE); + Integer bitrate = (Integer)map.get(MediaFormat.KEY_BIT_RATE); + if (bitrate == null) { + bitrate = maxBitrate; + } else if (maxBitrate != null) { + bitrate = Math.max(bitrate, maxBitrate); + } + + if (bitrate != null && bitrate > 0) { + return bitrateRange.contains(bitrate); } + + return true; } - } - /** @hide */ - public String[] validFeatures() { - Feature[] features = getValidFeatures(); - String[] res = new String[features.length]; - for (int i = 0; i < res.length; i++) { - if (!features[i].mInternal) { - res[i] = features[i].mName; + private boolean supportsProfileLevel(int profile, Integer level) { + for (CodecProfileLevel pl: mProfileLevels) { + if (pl.profile != profile) { + continue; + } + + // No specific level requested + if (level == null) { + return true; + } + + // AAC doesn't use levels + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AAC)) { + return true; + } + + // DTS doesn't use levels + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS) + || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_HD) + || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_UHD)) { + return true; + } + + // H.263 levels are not completely ordered: + // Level45 support only implies Level10 support + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263)) { + if (pl.level != level && pl.level == CodecProfileLevel.H263Level45 + && level > CodecProfileLevel.H263Level10) { + continue; + } + } + + // MPEG4 levels are not completely ordered: + // Level1 support only implies Level0 (and not Level0b) support + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG4)) { + if (pl.level != level && pl.level == CodecProfileLevel.MPEG4Level1 + && level > CodecProfileLevel.MPEG4Level0) { + continue; + } + } + + // HEVC levels incorporate both tiers and levels. Verify tier support. + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { + boolean supportsHighTier = + (pl.level & CodecProfileLevel.HEVCHighTierLevels) != 0; + boolean checkingHighTier + = (level & CodecProfileLevel.HEVCHighTierLevels) != 0; + // high tier levels are only supported by other high tier levels + if (checkingHighTier && !supportsHighTier) { + continue; + } + } + + if (pl.level >= level) { + // if we recognize the listed profile/level, we must also recognize the + // profile/level arguments. + if (createFromProfileLevel(mMime, profile, pl.level) != null) { + return createFromProfileLevel(mMime, profile, level) != null; + } + return true; + } } + return false; + } + + public MediaFormat getDefaultFormat() { + return mDefaultFormat; + } + + public String getMimeType() { + return mMime; + } + + public int getMaxSupportedInstances() { + return mMaxSupportedInstances; } - return res; - } - private Feature[] getValidFeatures() { - return FeatureList.getFeatures(isEncoder()); + private boolean isAudio() { + return mAudioCaps != null; + } + + public AudioCapabilities getAudioCapabilities() { + return mAudioCaps; + } + + private boolean isEncoder() { + return mEncoderCaps != null; + } + + public EncoderCapabilities getEncoderCapabilities() { + return mEncoderCaps; + } + + private boolean isVideo() { + return mVideoCaps != null; + } + + public VideoCapabilities getVideoCapabilities() { + return mVideoCaps; + } + + public static CodecCapsLegacyImpl createFromProfileLevel( + String mime, int profile, int level) { + CodecProfileLevel pl = new CodecProfileLevel(); + pl.profile = profile; + pl.level = level; + MediaFormat defaultFormat = new MediaFormat(); + defaultFormat.setString(MediaFormat.KEY_MIME, mime); + + CodecCapsLegacyImpl ret = new CodecCapsLegacyImpl( + new CodecProfileLevel[] { pl }, new int[0], true /* encoder */, + defaultFormat, new MediaFormat() /* info */); + if (ret.mError != 0) { + return null; + } + return ret; + } + + /* package private */ CodecCapsLegacyImpl( + CodecProfileLevel[] profLevs, int[] colFmts, + boolean encoder, + Map<String, Object>defaultFormatMap, + Map<String, Object>capabilitiesMap) { + this(profLevs, colFmts, encoder, + new MediaFormat(defaultFormatMap), + new MediaFormat(capabilitiesMap)); + } + + /* package private */ CodecCapsLegacyImpl( + CodecProfileLevel[] profLevs, int[] colFmts, boolean encoder, + MediaFormat defaultFormat, MediaFormat info) { + final Map<String, Object> map = info.getMap(); + mColorFormats = colFmts; + mFlagsVerified = 0; // TODO: remove as it is unused + mDefaultFormat = defaultFormat; + mCapabilitiesInfo = info; + mMime = mDefaultFormat.getString(MediaFormat.KEY_MIME); + + /* VP9 introduced profiles around 2016, so some VP9 codecs may not advertise any + supported profiles. Determine the level for them using the info they provide. */ + if (profLevs.length == 0 + && mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP9)) { + CodecProfileLevel profLev = new CodecProfileLevel(); + profLev.profile = CodecProfileLevel.VP9Profile0; + profLev.level = VideoCapabilities.VideoCapsLegacyImpl.equivalentVP9Level(info); + profLevs = new CodecProfileLevel[] { profLev }; + } + mProfileLevels = profLevs; + + if (mMime.toLowerCase().startsWith("audio/")) { + mAudioCaps = AudioCapabilities.create(info, this); + mAudioCaps.getDefaultFormat(mDefaultFormat); + } else if (mMime.toLowerCase().startsWith("video/") + || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC)) { + mVideoCaps = VideoCapabilities.create(info, this); + } + if (encoder) { + mEncoderCaps = EncoderCapabilities.create(info, this); + mEncoderCaps.getDefaultFormat(mDefaultFormat); + } + + final Map<String, Object> global = MediaCodecList.getGlobalSettings(); + mMaxSupportedInstances = Utils.parseIntSafely( + global.get("max-concurrent-instances"), DEFAULT_MAX_SUPPORTED_INSTANCES); + + int maxInstances = Utils.parseIntSafely( + map.get("max-concurrent-instances"), mMaxSupportedInstances); + mMaxSupportedInstances = + Range.create(1, MAX_SUPPORTED_INSTANCES_LIMIT).clamp(maxInstances); + + for (Feature feat: getValidFeatures()) { + String key = MediaFormat.KEY_FEATURE_ + feat.mName; + Integer yesNo = (Integer)map.get(key); + if (yesNo == null) { + continue; + } + if (yesNo > 0) { + mFlagsRequired |= feat.mValue; + } + mFlagsSupported |= feat.mValue; + if (!feat.mInternal) { + mDefaultFormat.setInteger(key, 1); + } + // TODO restrict features by mFlagsVerified once all codecs reliably verify them + } + } } - private boolean checkFeature(String name, int flags) { - for (Feature feat: getValidFeatures()) { - if (feat.mName.equals(name)) { - return (flags & feat.mValue) != 0; + /* package private */ static final class CodecCapsNativeImpl implements CodecCapsIntf { + private long mNativeContext; // accessed by native methods + + private CodecProfileLevel[] mProfileLevels; + private int[] mColorFormats; + + private MediaFormat mDefaultFormat; + private AudioCapabilities mAudioCaps; + private VideoCapabilities mVideoCaps; + private EncoderCapabilities mEncoderCaps; + + public static CodecCapsNativeImpl createFromProfileLevel( + String mime, int profile, int level) { + return native_createFromProfileLevel(mime, profile, level); + } + + /** + * Constructor used by JNI. + * + * The Java CodecCapabilities object keeps these subobjects to avoid recontructing. + */ + /* package private */ CodecCapsNativeImpl(CodecProfileLevel[] profLevs, int[] colFmts, + MediaFormat defaultFormat, AudioCapabilities audioCaps, + VideoCapabilities videoCaps, EncoderCapabilities encoderCaps) { + mProfileLevels = profLevs; + mColorFormats = colFmts; + mDefaultFormat = defaultFormat; + mAudioCaps = audioCaps; + mVideoCaps = videoCaps; + mEncoderCaps = encoderCaps; + } + + public CodecCapsNativeImpl dup() { + CodecCapsNativeImpl impl = native_dup(); + return impl; + } + + @Override + protected void finalize() { + native_finalize(); + } + + public CodecProfileLevel[] getProfileLevels() { + return mProfileLevels; + } + + public int[] getColorFormats() { + return mColorFormats; + } + + public final boolean isFeatureSupported(String name) { + return native_isFeatureSupported(name); + } + + public final boolean isFeatureRequired(String name) { + return native_isFeatureRequired(name); + } + + public boolean isRegular() { + return native_isRegular(); + } + + public final boolean isFormatSupported(MediaFormat format) { + if (format == null) { + throw new NullPointerException(); + } + + Map<String, Object> formatMap = format.getMap(); + String[] keys = new String[formatMap.size()]; + Object[] values = new Object[formatMap.size()]; + + int i = 0; + for (Map.Entry<String, Object> entry: formatMap.entrySet()) { + keys[i] = entry.getKey(); + values[i] = entry.getValue(); + ++i; } + + return native_isFormatSupported(keys, values); } - return false; + + public MediaFormat getDefaultFormat() { + return mDefaultFormat; + } + + public String getMimeType() { + return native_getMimeType(); + } + + public int getMaxSupportedInstances() { + return native_getMaxSupportedInstances(); + } + + public AudioCapabilities getAudioCapabilities() { + return mAudioCaps; + } + + public EncoderCapabilities getEncoderCapabilities() { + return mEncoderCaps; + } + + public VideoCapabilities getVideoCapabilities() { + return mVideoCaps; + } + + private static native void native_init(); + private static native CodecCapsNativeImpl native_createFromProfileLevel( + String mime, int profile, int level); + private native CodecCapsNativeImpl native_dup(); + private native void native_finalize(); + private native int native_getMaxSupportedInstances(); + private native String native_getMimeType(); + private native final boolean native_isFeatureRequired(String name); + private native final boolean native_isFeatureSupported(String name); + private native final boolean native_isFormatSupported(@Nullable String[] keys, + @Nullable Object[] values); + private native boolean native_isRegular(); + + static { + System.loadLibrary("media_jni"); + native_init(); + } + } + + private CodecCapsIntf mImpl; + + /** + * Retrieve the codec capabilities for a certain {@code mime type}, {@code + * profile} and {@code level}. If the type, or profile-level combination + * is not understood by the framework, it returns null. + * <p class=note> In {@link android.os.Build.VERSION_CODES#M}, calling this + * method without calling any method of the {@link MediaCodecList} class beforehand + * results in a {@link NullPointerException}.</p> + */ + public static CodecCapabilities createFromProfileLevel( + String mime, int profile, int level) { + CodecCapsIntf impl; + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + impl = CodecCapsNativeImpl.createFromProfileLevel(mime, profile, level); + } else { + impl = CodecCapsLegacyImpl.createFromProfileLevel(mime, profile, level); + } + return new CodecCapabilities(impl); + } + + public CodecCapabilities() { + mImpl = new CodecCapsLegacyImpl(); + } + + /** package private */ CodecCapabilities(CodecCapsIntf impl) { + mImpl = impl; + profileLevels = mImpl.getProfileLevels(); + colorFormats = mImpl.getColorFormats(); + } + + /** @hide */ + public CodecCapabilities dup() { + CodecCapabilities caps = new CodecCapabilities(); + + // profileLevels and colorFormats may be modified by client. + caps.profileLevels = Arrays.copyOf(profileLevels, profileLevels.length); + caps.colorFormats = Arrays.copyOf(colorFormats, colorFormats.length); + + caps.mImpl = mImpl.dup(); + + return caps; + } + + /** + * Query codec feature capabilities. + * <p> + * These features are supported to be used by the codec. These + * include optional features that can be turned on, as well as + * features that are always on. + */ + public final boolean isFeatureSupported(String name) { + return mImpl.isFeatureSupported(name); + } + + /** + * Query codec feature requirements. + * <p> + * These features are required to be used by the codec, and as such, + * they are always turned on. + */ + public final boolean isFeatureRequired(String name) { + return mImpl.isFeatureRequired(name); } /** @hide */ public boolean isRegular() { - // regular codecs only require default features - for (Feature feat: getValidFeatures()) { - if (!feat.mDefault && isFeatureRequired(feat.mName)) { - return false; - } - } - return true; + return mImpl.isRegular(); } /** @@ -1047,384 +1594,573 @@ public final class MediaCodecInfo { * and feature requests. */ public final boolean isFormatSupported(MediaFormat format) { - final Map<String, Object> map = format.getMap(); - final String mime = (String)map.get(MediaFormat.KEY_MIME); + return mImpl.isFormatSupported(format); + } - // mime must match if present - if (mime != null && !mMime.equalsIgnoreCase(mime)) { - return false; + /** + * Returns a MediaFormat object with default values for configurations that have + * defaults. + */ + public MediaFormat getDefaultFormat() { + return mImpl.getDefaultFormat(); + } + + /** + * Returns the mime type for which this codec-capability object was created. + */ + public String getMimeType() { + return mImpl.getMimeType(); + } + + /** + * Returns the max number of the supported concurrent codec instances. + * <p> + * This is a hint for an upper bound. Applications should not expect to successfully + * operate more instances than the returned value, but the actual number of + * concurrently operable instances may be less as it depends on the available + * resources at time of use. + */ + public int getMaxSupportedInstances() { + return mImpl.getMaxSupportedInstances(); + } + + /** + * Returns the audio capabilities or {@code null} if this is not an audio codec. + */ + public AudioCapabilities getAudioCapabilities() { + return mImpl.getAudioCapabilities(); + } + + /** + * Returns the encoding capabilities or {@code null} if this is not an encoder. + */ + public EncoderCapabilities getEncoderCapabilities() { + return mImpl.getEncoderCapabilities(); + } + + /** + * Returns the video capabilities or {@code null} if this is not a video codec. + */ + public VideoCapabilities getVideoCapabilities() { + return mImpl.getVideoCapabilities(); + } + } + + /** + * A class that supports querying the audio capabilities of a codec. + */ + public static final class AudioCapabilities { + private static final String TAG = "AudioCapabilities"; + + /* package private */ interface AudioCapsIntf { + public Range<Integer> getBitrateRange(); + + public int[] getSupportedSampleRates(); + + public Range<Integer>[] getSupportedSampleRateRanges(); + + public int getMaxInputChannelCount(); + + public int getMinInputChannelCount(); + + public Range<Integer>[] getInputChannelCountRanges(); + + public boolean isSampleRateSupported(int sampleRate); + + public void getDefaultFormat(MediaFormat format); + + public boolean supportsFormat(MediaFormat format); + } + + /* package private */ static final class AudioCapsLegacyImpl implements AudioCapsIntf { + private CodecCapabilities.CodecCapsLegacyImpl mParent; + private Range<Integer> mBitrateRange; + + private int[] mSampleRates; + private Range<Integer>[] mSampleRateRanges; + private Range<Integer>[] mInputChannelRanges; + + private static final int MAX_INPUT_CHANNEL_COUNT = 30; + + public Range<Integer> getBitrateRange() { + return mBitrateRange; } - // check feature support - for (Feature feat: getValidFeatures()) { - if (feat.mInternal) { - continue; - } + public int[] getSupportedSampleRates() { + return mSampleRates != null ? Arrays.copyOf(mSampleRates, mSampleRates.length) + : null; + } + + public Range<Integer>[] getSupportedSampleRateRanges() { + return Arrays.copyOf(mSampleRateRanges, mSampleRateRanges.length); + } - Integer yesNo = (Integer)map.get(MediaFormat.KEY_FEATURE_ + feat.mName); - if (yesNo == null) { - continue; + public int getMaxInputChannelCount() { + int overall_max = 0; + for (int i = mInputChannelRanges.length - 1; i >= 0; i--) { + int lmax = mInputChannelRanges[i].getUpper(); + if (lmax > overall_max) { + overall_max = lmax; + } } - if ((yesNo == 1 && !isFeatureSupported(feat.mName)) || - (yesNo == 0 && isFeatureRequired(feat.mName))) { - return false; + return overall_max; + } + + public int getMinInputChannelCount() { + int overall_min = MAX_INPUT_CHANNEL_COUNT; + for (int i = mInputChannelRanges.length - 1; i >= 0; i--) { + int lmin = mInputChannelRanges[i].getLower(); + if (lmin < overall_min) { + overall_min = lmin; + } } + return overall_min; } - Integer profile = (Integer)map.get(MediaFormat.KEY_PROFILE); - Integer level = (Integer)map.get(MediaFormat.KEY_LEVEL); + public Range<Integer>[] getInputChannelCountRanges() { + return Arrays.copyOf(mInputChannelRanges, mInputChannelRanges.length); + } - if (profile != null) { - if (!supportsProfileLevel(profile, level)) { - return false; + /* no public constructor */ + private AudioCapsLegacyImpl() { } + + public static AudioCapsLegacyImpl create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + Log.d(TAG, "Legacy implementation is called while native flag is on."); } - // If we recognize this profile, check that this format is supported by the - // highest level supported by the codec for that profile. (Ignore specified - // level beyond the above profile/level check as level is only used as a - // guidance. E.g. AVC Level 1 CIF format is supported if codec supports level 1.1 - // even though max size for Level 1 is QCIF. However, MPEG2 Simple Profile - // 1080p format is not supported even if codec supports Main Profile Level High, - // as Simple Profile does not support 1080p. - CodecCapabilities levelCaps = null; - int maxLevel = 0; - for (CodecProfileLevel pl : profileLevels) { - if (pl.profile == profile && pl.level > maxLevel) { - // H.263 levels are not completely ordered: - // Level45 support only implies Level10 support - if (!mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263) - || pl.level != CodecProfileLevel.H263Level45 - || maxLevel == CodecProfileLevel.H263Level10) { - maxLevel = pl.level; - } + AudioCapsLegacyImpl caps = new AudioCapsLegacyImpl(); + caps.init(info, parent); + return caps; + } + + private void init(MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + mParent = parent; + initWithPlatformLimits(); + applyLevelLimits(); + parseFromInfo(info); + } + + private void initWithPlatformLimits() { + mBitrateRange = Range.create(0, Integer.MAX_VALUE); + mInputChannelRanges = new Range[] {Range.create(1, MAX_INPUT_CHANNEL_COUNT)}; + // mBitrateRange = Range.create(1, 320000); + final int minSampleRate = SystemProperties. + getInt("ro.mediacodec.min_sample_rate", 7350); + final int maxSampleRate = SystemProperties. + getInt("ro.mediacodec.max_sample_rate", 192000); + mSampleRateRanges = new Range[] { Range.create(minSampleRate, maxSampleRate) }; + mSampleRates = null; + } + + private boolean supports(Integer sampleRate, Integer inputChannels) { + // channels and sample rates are checked orthogonally + if (inputChannels != null) { + int ix = Utils.binarySearchDistinctRanges( + mInputChannelRanges, inputChannels); + if (ix < 0) { + return false; } } - levelCaps = createFromProfileLevel(mMime, profile, maxLevel); - // We must remove the profile from this format otherwise levelCaps.isFormatSupported - // will get into this same condition and loop forever. Furthermore, since levelCaps - // does not contain features and bitrate specific keys, keep only keys relevant for - // a level check. - Map<String, Object> levelCriticalFormatMap = new HashMap<>(map); - final Set<String> criticalKeys = - isVideo() ? VideoCapabilities.VIDEO_LEVEL_CRITICAL_FORMAT_KEYS : - isAudio() ? AudioCapabilities.AUDIO_LEVEL_CRITICAL_FORMAT_KEYS : - null; - - // critical keys will always contain KEY_MIME, but should also contain others to be - // meaningful - if (criticalKeys != null && criticalKeys.size() > 1 && levelCaps != null) { - levelCriticalFormatMap.keySet().retainAll(criticalKeys); - - MediaFormat levelCriticalFormat = new MediaFormat(levelCriticalFormatMap); - if (!levelCaps.isFormatSupported(levelCriticalFormat)) { + if (sampleRate != null) { + int ix = Utils.binarySearchDistinctRanges( + mSampleRateRanges, sampleRate); + if (ix < 0) { return false; } } + return true; } - if (mAudioCaps != null && !mAudioCaps.supportsFormat(format)) { - return false; - } - if (mVideoCaps != null && !mVideoCaps.supportsFormat(format)) { - return false; - } - if (mEncoderCaps != null && !mEncoderCaps.supportsFormat(format)) { - return false; - } - return true; - } - private static boolean supportsBitrate( - Range<Integer> bitrateRange, MediaFormat format) { - Map<String, Object> map = format.getMap(); + public boolean isSampleRateSupported(int sampleRate) { + return supports(sampleRate, null); + } - // consider max bitrate over average bitrate for support - Integer maxBitrate = (Integer)map.get(MediaFormat.KEY_MAX_BIT_RATE); - Integer bitrate = (Integer)map.get(MediaFormat.KEY_BIT_RATE); - if (bitrate == null) { - bitrate = maxBitrate; - } else if (maxBitrate != null) { - bitrate = Math.max(bitrate, maxBitrate); + /** modifies rates */ + private void limitSampleRates(int[] rates) { + Arrays.sort(rates); + ArrayList<Range<Integer>> ranges = new ArrayList<Range<Integer>>(); + for (int rate: rates) { + if (supports(rate, null /* channels */)) { + ranges.add(Range.create(rate, rate)); + } + } + mSampleRateRanges = ranges.toArray(new Range[ranges.size()]); + createDiscreteSampleRates(); } - if (bitrate != null && bitrate > 0) { - return bitrateRange.contains(bitrate); + private void createDiscreteSampleRates() { + mSampleRates = new int[mSampleRateRanges.length]; + for (int i = 0; i < mSampleRateRanges.length; i++) { + mSampleRates[i] = mSampleRateRanges[i].getLower(); + } } - return true; - } + /** modifies rateRanges */ + private void limitSampleRates(Range<Integer>[] rateRanges) { + sortDistinctRanges(rateRanges); + mSampleRateRanges = intersectSortedDistinctRanges(mSampleRateRanges, rateRanges); - private boolean supportsProfileLevel(int profile, Integer level) { - for (CodecProfileLevel pl: profileLevels) { - if (pl.profile != profile) { - continue; + // check if all values are discrete + for (Range<Integer> range: mSampleRateRanges) { + if (!range.getLower().equals(range.getUpper())) { + mSampleRates = null; + return; + } } + createDiscreteSampleRates(); + } - // No specific level requested - if (level == null) { - return true; + private void applyLevelLimits() { + int[] sampleRates = null; + Range<Integer> sampleRateRange = null, bitRates = null; + int maxChannels = MAX_INPUT_CHANNEL_COUNT; + CodecProfileLevel[] profileLevels = mParent.getProfileLevels(); + String mime = mParent.getMimeType(); + + if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MPEG)) { + sampleRates = new int[] { + 8000, 11025, 12000, + 16000, 22050, 24000, + 32000, 44100, 48000 }; + bitRates = Range.create(8000, 320000); + maxChannels = 2; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_NB)) { + sampleRates = new int[] { 8000 }; + bitRates = Range.create(4750, 12200); + maxChannels = 1; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_WB)) { + sampleRates = new int[] { 16000 }; + bitRates = Range.create(6600, 23850); + maxChannels = 1; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AAC)) { + sampleRates = new int[] { + 7350, 8000, + 11025, 12000, 16000, + 22050, 24000, 32000, + 44100, 48000, 64000, + 88200, 96000 }; + bitRates = Range.create(8000, 510000); + maxChannels = 48; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_VORBIS)) { + bitRates = Range.create(32000, 500000); + sampleRateRange = Range.create(8000, 192000); + maxChannels = 255; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_OPUS)) { + bitRates = Range.create(6000, 510000); + sampleRates = new int[] { 8000, 12000, 16000, 24000, 48000 }; + maxChannels = 255; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_RAW)) { + sampleRateRange = Range.create(1, 192000); + bitRates = Range.create(1, 10000000); + maxChannels = AudioSystem.OUT_CHANNEL_COUNT_MAX; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_FLAC)) { + sampleRateRange = Range.create(1, 655350); + // lossless codec, so bitrate is ignored + maxChannels = 255; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_ALAW) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_MLAW)) { + sampleRates = new int[] { 8000 }; + bitRates = Range.create(64000, 64000); + // platform allows multiple channels for this format + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MSGSM)) { + sampleRates = new int[] { 8000 }; + bitRates = Range.create(13000, 13000); + maxChannels = 1; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC3)) { + maxChannels = 6; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3)) { + maxChannels = 16; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3_JOC)) { + sampleRates = new int[] { 48000 }; + bitRates = Range.create(32000, 6144000); + maxChannels = 16; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC4)) { + sampleRates = new int[] { 44100, 48000, 96000, 192000 }; + bitRates = Range.create(16000, 2688000); + maxChannels = 24; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS)) { + sampleRates = new int[] { 44100, 48000 }; + bitRates = Range.create(96000, 1524000); + maxChannels = 6; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_HD)) { + for (CodecProfileLevel profileLevel: profileLevels) { + switch (profileLevel.profile) { + case CodecProfileLevel.DTS_HDProfileLBR: + sampleRates = new int[]{ 22050, 24000, 44100, 48000 }; + bitRates = Range.create(32000, 768000); + break; + case CodecProfileLevel.DTS_HDProfileHRA: + case CodecProfileLevel.DTS_HDProfileMA: + sampleRates + = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; + bitRates = Range.create(96000, 24500000); + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + mParent.mError |= ERROR_UNRECOGNIZED; + sampleRates + = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; + bitRates = Range.create(96000, 24500000); + } + } + maxChannels = 8; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_UHD)) { + for (CodecProfileLevel profileLevel: profileLevels) { + switch (profileLevel.profile) { + case CodecProfileLevel.DTS_UHDProfileP2: + sampleRates = new int[]{ 48000 }; + bitRates = Range.create(96000, 768000); + maxChannels = 10; + break; + case CodecProfileLevel.DTS_UHDProfileP1: + sampleRates + = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; + bitRates = Range.create(96000, 24500000); + maxChannels = 32; + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + mParent.mError |= ERROR_UNRECOGNIZED; + sampleRates + = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; + bitRates = Range.create(96000, 24500000); + maxChannels = 32; + } + } + } else { + Log.w(TAG, "Unsupported mime " + mime); + mParent.mError |= ERROR_UNSUPPORTED; } - // AAC doesn't use levels - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AAC)) { - return true; + // restrict ranges + if (sampleRates != null) { + limitSampleRates(sampleRates); + } else if (sampleRateRange != null) { + limitSampleRates(new Range[] { sampleRateRange }); } - // DTS doesn't use levels - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS) - || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_HD) - || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_UHD)) { - return true; + Range<Integer> channelRange = Range.create(1, maxChannels); + + applyLimits(new Range[] { channelRange }, bitRates); + } + + private void applyLimits(Range<Integer>[] inputChannels, Range<Integer> bitRates) { + + // clamp & make a local copy + Range<Integer>[] myInputChannels = new Range[inputChannels.length]; + for (int i = 0; i < inputChannels.length; i++) { + int lower = inputChannels[i].clamp(1); + int upper = inputChannels[i].clamp(MAX_INPUT_CHANNEL_COUNT); + myInputChannels[i] = Range.create(lower, upper); } - // H.263 levels are not completely ordered: - // Level45 support only implies Level10 support - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263)) { - if (pl.level != level && pl.level == CodecProfileLevel.H263Level45 - && level > CodecProfileLevel.H263Level10) { - continue; - } + // sort, intersect with existing, & save channel list + sortDistinctRanges(myInputChannels); + Range<Integer>[] joinedChannelList = + intersectSortedDistinctRanges(myInputChannels, mInputChannelRanges); + mInputChannelRanges = joinedChannelList; + + if (bitRates != null) { + mBitrateRange = mBitrateRange.intersect(bitRates); } + } - // MPEG4 levels are not completely ordered: - // Level1 support only implies Level0 (and not Level0b) support - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG4)) { - if (pl.level != level && pl.level == CodecProfileLevel.MPEG4Level1 - && level > CodecProfileLevel.MPEG4Level0) { - continue; + private void parseFromInfo(MediaFormat info) { + int maxInputChannels = MAX_INPUT_CHANNEL_COUNT; + Range<Integer>[] channels = new Range[] { Range.create(1, maxInputChannels)}; + Range<Integer> bitRates = POSITIVE_INTEGERS; + + if (info.containsKey("sample-rate-ranges")) { + String[] rateStrings = info.getString("sample-rate-ranges").split(","); + Range<Integer>[] rateRanges = new Range[rateStrings.length]; + for (int i = 0; i < rateStrings.length; i++) { + rateRanges[i] = Utils.parseIntRange(rateStrings[i], null); } + limitSampleRates(rateRanges); } - // HEVC levels incorporate both tiers and levels. Verify tier support. - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { - boolean supportsHighTier = - (pl.level & CodecProfileLevel.HEVCHighTierLevels) != 0; - boolean checkingHighTier = (level & CodecProfileLevel.HEVCHighTierLevels) != 0; - // high tier levels are only supported by other high tier levels - if (checkingHighTier && !supportsHighTier) { - continue; + // we will prefer channel-ranges over max-channel-count + if (info.containsKey("channel-ranges")) { + String[] channelStrings = info.getString("channel-ranges").split(","); + Range<Integer>[] channelRanges = new Range[channelStrings.length]; + for (int i = 0; i < channelStrings.length; i++) { + channelRanges[i] = Utils.parseIntRange(channelStrings[i], null); + } + channels = channelRanges; + } else if (info.containsKey("channel-range")) { + Range<Integer> oneRange = Utils.parseIntRange(info.getString("channel-range"), + null); + channels = new Range[] { oneRange }; + } else if (info.containsKey("max-channel-count")) { + maxInputChannels = Utils.parseIntSafely( + info.getString("max-channel-count"), maxInputChannels); + if (maxInputChannels == 0) { + channels = new Range[] {Range.create(0, 0)}; + } else { + channels = new Range[] {Range.create(1, maxInputChannels)}; } + } else if ((mParent.mError & ERROR_UNSUPPORTED) != 0) { + maxInputChannels = 0; + channels = new Range[] {Range.create(0, 0)}; } - if (pl.level >= level) { - // if we recognize the listed profile/level, we must also recognize the - // profile/level arguments. - if (createFromProfileLevel(mMime, profile, pl.level) != null) { - return createFromProfileLevel(mMime, profile, level) != null; - } - return true; + if (info.containsKey("bitrate-range")) { + bitRates = bitRates.intersect( + Utils.parseIntRange(info.getString("bitrate-range"), bitRates)); } + + applyLimits(channels, bitRates); } - return false; - } - // errors while reading profile levels - accessed from sister capabilities - int mError; + /** @hide */ + public void getDefaultFormat(MediaFormat format) { + // report settings that have only a single choice + if (mBitrateRange.getLower().equals(mBitrateRange.getUpper())) { + format.setInteger(MediaFormat.KEY_BIT_RATE, mBitrateRange.getLower()); + } + if (getMaxInputChannelCount() == 1) { + // mono-only format + format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); + } + if (mSampleRates != null && mSampleRates.length == 1) { + format.setInteger(MediaFormat.KEY_SAMPLE_RATE, mSampleRates[0]); + } + } - private static final String TAG = "CodecCapabilities"; + /* package private */ + // must not contain KEY_PROFILE + static final Set<String> AUDIO_LEVEL_CRITICAL_FORMAT_KEYS = Set.of( + // We don't set level-specific limits for audio codecs today. Key candidates + // would be sample rate, bit rate or channel count. + // MediaFormat.KEY_SAMPLE_RATE, + // MediaFormat.KEY_CHANNEL_COUNT, + // MediaFormat.KEY_BIT_RATE, + MediaFormat.KEY_MIME); + + /** @hide */ + public boolean supportsFormat(MediaFormat format) { + Map<String, Object> map = format.getMap(); + Integer sampleRate = (Integer)map.get(MediaFormat.KEY_SAMPLE_RATE); + Integer channels = (Integer)map.get(MediaFormat.KEY_CHANNEL_COUNT); + + if (!supports(sampleRate, channels)) { + return false; + } - // NEW-STYLE CAPABILITIES - private AudioCapabilities mAudioCaps; - private VideoCapabilities mVideoCaps; - private EncoderCapabilities mEncoderCaps; - private MediaFormat mDefaultFormat; + if (!CodecCapabilities.CodecCapsLegacyImpl.supportsBitrate(mBitrateRange, format)) { + return false; + } - /** - * Returns a MediaFormat object with default values for configurations that have - * defaults. - */ - public MediaFormat getDefaultFormat() { - return mDefaultFormat; + // nothing to do for: + // KEY_CHANNEL_MASK: codecs don't get this + // KEY_IS_ADTS: required feature for all AAC decoders + return true; + } } - /** - * Returns the mime type for which this codec-capability object was created. - */ - public String getMimeType() { - return mMime; - } + /* package private */ static final class AudioCapsNativeImpl implements AudioCapsIntf { + private long mNativeContext; // accessed by native methods - /** - * Returns the max number of the supported concurrent codec instances. - * <p> - * This is a hint for an upper bound. Applications should not expect to successfully - * operate more instances than the returned value, but the actual number of - * concurrently operable instances may be less as it depends on the available - * resources at time of use. - */ - public int getMaxSupportedInstances() { - return mMaxSupportedInstances; - } + private Range<Integer> mBitrateRange; + private int[] mSampleRates; + private Range<Integer>[] mSampleRateRanges; + private Range<Integer>[] mInputChannelRanges; - private boolean isAudio() { - return mAudioCaps != null; - } + /** + * Constructor used by JNI. + * + * The Java AudioCapabilities object keeps these subobjects to avoid recontruction. + */ + /* package private */ AudioCapsNativeImpl(Range<Integer> bitrateRange, + int[] sampleRates, Range<Integer>[] sampleRateRanges, + Range<Integer>[] inputChannelRanges) { + mBitrateRange = bitrateRange; + mSampleRates = sampleRates; + mSampleRateRanges = sampleRateRanges; + mInputChannelRanges = inputChannelRanges; + } - /** - * Returns the audio capabilities or {@code null} if this is not an audio codec. - */ - public AudioCapabilities getAudioCapabilities() { - return mAudioCaps; - } + /* no public constructor */ + private AudioCapsNativeImpl() { } - private boolean isEncoder() { - return mEncoderCaps != null; - } + public Range<Integer> getBitrateRange() { + return mBitrateRange; + } - /** - * Returns the encoding capabilities or {@code null} if this is not an encoder. - */ - public EncoderCapabilities getEncoderCapabilities() { - return mEncoderCaps; - } + public int[] getSupportedSampleRates() { + return mSampleRates != null ? Arrays.copyOf(mSampleRates, mSampleRates.length) + : null; + } - private boolean isVideo() { - return mVideoCaps != null; - } + public Range<Integer>[] getSupportedSampleRateRanges() { + return Arrays.copyOf(mSampleRateRanges, mSampleRateRanges.length); + } - /** - * Returns the video capabilities or {@code null} if this is not a video codec. - */ - public VideoCapabilities getVideoCapabilities() { - return mVideoCaps; - } + public Range<Integer>[] getInputChannelCountRanges() { + return Arrays.copyOf(mInputChannelRanges, mInputChannelRanges.length); + } - /** @hide */ - public CodecCapabilities dup() { - CodecCapabilities caps = new CodecCapabilities(); + public int getMaxInputChannelCount() { + return native_getMaxInputChannelCount(); + } - // profileLevels and colorFormats may be modified by client. - caps.profileLevels = Arrays.copyOf(profileLevels, profileLevels.length); - caps.colorFormats = Arrays.copyOf(colorFormats, colorFormats.length); + public int getMinInputChannelCount() { + return native_getMinInputChannelCount(); + } - caps.mMime = mMime; - caps.mMaxSupportedInstances = mMaxSupportedInstances; - caps.mFlagsRequired = mFlagsRequired; - caps.mFlagsSupported = mFlagsSupported; - caps.mFlagsVerified = mFlagsVerified; - caps.mAudioCaps = mAudioCaps; - caps.mVideoCaps = mVideoCaps; - caps.mEncoderCaps = mEncoderCaps; - caps.mDefaultFormat = mDefaultFormat; - caps.mCapabilitiesInfo = mCapabilitiesInfo; + public boolean isSampleRateSupported(int sampleRate) { + return native_isSampleRateSupported(sampleRate); + } - return caps; - } + // This API is for internal Java implementation only. Should not be called. + public void getDefaultFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); + } - /** - * Retrieve the codec capabilities for a certain {@code mime type}, {@code - * profile} and {@code level}. If the type, or profile-level combination - * is not understood by the framework, it returns null. - * <p class=note> In {@link android.os.Build.VERSION_CODES#M}, calling this - * method without calling any method of the {@link MediaCodecList} class beforehand - * results in a {@link NullPointerException}.</p> - */ - public static CodecCapabilities createFromProfileLevel( - String mime, int profile, int level) { - CodecProfileLevel pl = new CodecProfileLevel(); - pl.profile = profile; - pl.level = level; - MediaFormat defaultFormat = new MediaFormat(); - defaultFormat.setString(MediaFormat.KEY_MIME, mime); - - CodecCapabilities ret = new CodecCapabilities( - new CodecProfileLevel[] { pl }, new int[0], true /* encoder */, - defaultFormat, new MediaFormat() /* info */); - if (ret.mError != 0) { - return null; + // This API is for internal Java implementation only. Should not be called. + public boolean supportsFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); } - return ret; - } - /* package private */ CodecCapabilities( - CodecProfileLevel[] profLevs, int[] colFmts, - boolean encoder, - Map<String, Object>defaultFormatMap, - Map<String, Object>capabilitiesMap) { - this(profLevs, colFmts, encoder, - new MediaFormat(defaultFormatMap), - new MediaFormat(capabilitiesMap)); - } + private native int native_getMaxInputChannelCount(); + private native int native_getMinInputChannelCount(); + private native boolean native_isSampleRateSupported(int sampleRate); + private static native void native_init(); - private MediaFormat mCapabilitiesInfo; - - /* package private */ CodecCapabilities( - CodecProfileLevel[] profLevs, int[] colFmts, boolean encoder, - MediaFormat defaultFormat, MediaFormat info) { - final Map<String, Object> map = info.getMap(); - colorFormats = colFmts; - mFlagsVerified = 0; // TODO: remove as it is unused - mDefaultFormat = defaultFormat; - mCapabilitiesInfo = info; - mMime = mDefaultFormat.getString(MediaFormat.KEY_MIME); - - /* VP9 introduced profiles around 2016, so some VP9 codecs may not advertise any - supported profiles. Determine the level for them using the info they provide. */ - if (profLevs.length == 0 && mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP9)) { - CodecProfileLevel profLev = new CodecProfileLevel(); - profLev.profile = CodecProfileLevel.VP9Profile0; - profLev.level = VideoCapabilities.equivalentVP9Level(info); - profLevs = new CodecProfileLevel[] { profLev }; - } - profileLevels = profLevs; - - if (mMime.toLowerCase().startsWith("audio/")) { - mAudioCaps = AudioCapabilities.create(info, this); - mAudioCaps.getDefaultFormat(mDefaultFormat); - } else if (mMime.toLowerCase().startsWith("video/") - || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC)) { - mVideoCaps = VideoCapabilities.create(info, this); - } - if (encoder) { - mEncoderCaps = EncoderCapabilities.create(info, this); - mEncoderCaps.getDefaultFormat(mDefaultFormat); - } - - final Map<String, Object> global = MediaCodecList.getGlobalSettings(); - mMaxSupportedInstances = Utils.parseIntSafely( - global.get("max-concurrent-instances"), DEFAULT_MAX_SUPPORTED_INSTANCES); - - int maxInstances = Utils.parseIntSafely( - map.get("max-concurrent-instances"), mMaxSupportedInstances); - mMaxSupportedInstances = - Range.create(1, MAX_SUPPORTED_INSTANCES_LIMIT).clamp(maxInstances); - - for (Feature feat: getValidFeatures()) { - String key = MediaFormat.KEY_FEATURE_ + feat.mName; - Integer yesNo = (Integer)map.get(key); - if (yesNo == null) { - continue; - } - if (yesNo > 0) { - mFlagsRequired |= feat.mValue; - } - mFlagsSupported |= feat.mValue; - if (!feat.mInternal) { - mDefaultFormat.setInteger(key, 1); - } - // TODO restrict features by mFlagsVerified once all codecs reliably verify them + static { + System.loadLibrary("media_jni"); + native_init(); } } - } - /** - * A class that supports querying the audio capabilities of a codec. - */ - public static final class AudioCapabilities { - private static final String TAG = "AudioCapabilities"; - private CodecCapabilities mParent; - private Range<Integer> mBitrateRange; + private AudioCapsIntf mImpl; + + /** @hide */ + public static AudioCapabilities create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + AudioCapsLegacyImpl impl = AudioCapsLegacyImpl.create(info, parent); + AudioCapabilities caps = new AudioCapabilities(impl); + return caps; + } - private int[] mSampleRates; - private Range<Integer>[] mSampleRateRanges; - private Range<Integer>[] mInputChannelRanges; + /* package private */ AudioCapabilities(AudioCapsIntf impl) { + mImpl = impl; + } - private static final int MAX_INPUT_CHANNEL_COUNT = 30; + /* no public constructor */ + private AudioCapabilities() { } /** * Returns the range of supported bitrates in bits/second. */ public Range<Integer> getBitrateRange() { - return mBitrateRange; + return mImpl.getBitrateRange(); } /** @@ -1433,7 +2169,7 @@ public final class MediaCodecInfo { * {@code null}. The array is sorted in ascending order. */ public int[] getSupportedSampleRates() { - return mSampleRates != null ? Arrays.copyOf(mSampleRates, mSampleRates.length) : null; + return mImpl.getSupportedSampleRates(); } /** @@ -1442,7 +2178,21 @@ public final class MediaCodecInfo { * distinct. */ public Range<Integer>[] getSupportedSampleRateRanges() { - return Arrays.copyOf(mSampleRateRanges, mSampleRateRanges.length); + return mImpl.getSupportedSampleRateRanges(); + } + + /* + * Returns an array of ranges representing the number of input channels supported. + * The codec supports any number of input channels within this range. + * + * This supersedes the {@link #getMaxInputChannelCount} method. + * + * For many codecs, this will be a single range [1..N], for some N. + */ + @SuppressLint("ArrayReturn") + @NonNull + public Range<Integer>[] getInputChannelCountRanges() { + return mImpl.getInputChannelCountRanges(); } /** @@ -1462,14 +2212,7 @@ public final class MediaCodecInfo { */ @IntRange(from = 1, to = 255) public int getMaxInputChannelCount() { - int overall_max = 0; - for (int i = mInputChannelRanges.length - 1; i >= 0; i--) { - int lmax = mInputChannelRanges[i].getUpper(); - if (lmax > overall_max) { - overall_max = lmax; - } - } - return overall_max; + return mImpl.getMaxInputChannelCount(); } /** @@ -1481,364 +2224,24 @@ public final class MediaCodecInfo { */ @IntRange(from = 1, to = 255) public int getMinInputChannelCount() { - int overall_min = MAX_INPUT_CHANNEL_COUNT; - for (int i = mInputChannelRanges.length - 1; i >= 0; i--) { - int lmin = mInputChannelRanges[i].getLower(); - if (lmin < overall_min) { - overall_min = lmin; - } - } - return overall_min; - } - - /* - * Returns an array of ranges representing the number of input channels supported. - * The codec supports any number of input channels within this range. - * - * This supersedes the {@link #getMaxInputChannelCount} method. - * - * For many codecs, this will be a single range [1..N], for some N. - */ - @SuppressLint("ArrayReturn") - @NonNull - public Range<Integer>[] getInputChannelCountRanges() { - return Arrays.copyOf(mInputChannelRanges, mInputChannelRanges.length); - } - - /* no public constructor */ - private AudioCapabilities() { } - - /** @hide */ - public static AudioCapabilities create( - MediaFormat info, CodecCapabilities parent) { - AudioCapabilities caps = new AudioCapabilities(); - caps.init(info, parent); - return caps; - } - - private void init(MediaFormat info, CodecCapabilities parent) { - mParent = parent; - initWithPlatformLimits(); - applyLevelLimits(); - parseFromInfo(info); - } - - private void initWithPlatformLimits() { - mBitrateRange = Range.create(0, Integer.MAX_VALUE); - mInputChannelRanges = new Range[] {Range.create(1, MAX_INPUT_CHANNEL_COUNT)}; - // mBitrateRange = Range.create(1, 320000); - final int minSampleRate = SystemProperties. - getInt("ro.mediacodec.min_sample_rate", 7350); - final int maxSampleRate = SystemProperties. - getInt("ro.mediacodec.max_sample_rate", 192000); - mSampleRateRanges = new Range[] { Range.create(minSampleRate, maxSampleRate) }; - mSampleRates = null; - } - - private boolean supports(Integer sampleRate, Integer inputChannels) { - // channels and sample rates are checked orthogonally - if (inputChannels != null) { - int ix = Utils.binarySearchDistinctRanges( - mInputChannelRanges, inputChannels); - if (ix < 0) { - return false; - } - } - if (sampleRate != null) { - int ix = Utils.binarySearchDistinctRanges( - mSampleRateRanges, sampleRate); - if (ix < 0) { - return false; - } - } - return true; + return mImpl.getMinInputChannelCount(); } /** * Query whether the sample rate is supported by the codec. */ public boolean isSampleRateSupported(int sampleRate) { - return supports(sampleRate, null); - } - - /** modifies rates */ - private void limitSampleRates(int[] rates) { - Arrays.sort(rates); - ArrayList<Range<Integer>> ranges = new ArrayList<Range<Integer>>(); - for (int rate: rates) { - if (supports(rate, null /* channels */)) { - ranges.add(Range.create(rate, rate)); - } - } - mSampleRateRanges = ranges.toArray(new Range[ranges.size()]); - createDiscreteSampleRates(); - } - - private void createDiscreteSampleRates() { - mSampleRates = new int[mSampleRateRanges.length]; - for (int i = 0; i < mSampleRateRanges.length; i++) { - mSampleRates[i] = mSampleRateRanges[i].getLower(); - } - } - - /** modifies rateRanges */ - private void limitSampleRates(Range<Integer>[] rateRanges) { - sortDistinctRanges(rateRanges); - mSampleRateRanges = intersectSortedDistinctRanges(mSampleRateRanges, rateRanges); - - // check if all values are discrete - for (Range<Integer> range: mSampleRateRanges) { - if (!range.getLower().equals(range.getUpper())) { - mSampleRates = null; - return; - } - } - createDiscreteSampleRates(); - } - - private void applyLevelLimits() { - int[] sampleRates = null; - Range<Integer> sampleRateRange = null, bitRates = null; - int maxChannels = MAX_INPUT_CHANNEL_COUNT; - CodecProfileLevel[] profileLevels = mParent.profileLevels; - String mime = mParent.getMimeType(); - - if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MPEG)) { - sampleRates = new int[] { - 8000, 11025, 12000, - 16000, 22050, 24000, - 32000, 44100, 48000 }; - bitRates = Range.create(8000, 320000); - maxChannels = 2; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_NB)) { - sampleRates = new int[] { 8000 }; - bitRates = Range.create(4750, 12200); - maxChannels = 1; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_WB)) { - sampleRates = new int[] { 16000 }; - bitRates = Range.create(6600, 23850); - maxChannels = 1; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AAC)) { - sampleRates = new int[] { - 7350, 8000, - 11025, 12000, 16000, - 22050, 24000, 32000, - 44100, 48000, 64000, - 88200, 96000 }; - bitRates = Range.create(8000, 510000); - maxChannels = 48; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_VORBIS)) { - bitRates = Range.create(32000, 500000); - sampleRateRange = Range.create(8000, 192000); - maxChannels = 255; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_OPUS)) { - bitRates = Range.create(6000, 510000); - sampleRates = new int[] { 8000, 12000, 16000, 24000, 48000 }; - maxChannels = 255; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_RAW)) { - sampleRateRange = Range.create(1, 192000); - bitRates = Range.create(1, 10000000); - maxChannels = AudioSystem.OUT_CHANNEL_COUNT_MAX; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_FLAC)) { - sampleRateRange = Range.create(1, 655350); - // lossless codec, so bitrate is ignored - maxChannels = 255; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_ALAW) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_MLAW)) { - sampleRates = new int[] { 8000 }; - bitRates = Range.create(64000, 64000); - // platform allows multiple channels for this format - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MSGSM)) { - sampleRates = new int[] { 8000 }; - bitRates = Range.create(13000, 13000); - maxChannels = 1; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC3)) { - maxChannels = 6; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3)) { - maxChannels = 16; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3_JOC)) { - sampleRates = new int[] { 48000 }; - bitRates = Range.create(32000, 6144000); - maxChannels = 16; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC4)) { - sampleRates = new int[] { 44100, 48000, 96000, 192000 }; - bitRates = Range.create(16000, 2688000); - maxChannels = 24; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS)) { - sampleRates = new int[] { 44100, 48000 }; - bitRates = Range.create(96000, 1524000); - maxChannels = 6; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_HD)) { - for (CodecProfileLevel profileLevel: profileLevels) { - switch (profileLevel.profile) { - case CodecProfileLevel.DTS_HDProfileLBR: - sampleRates = new int[]{ 22050, 24000, 44100, 48000 }; - bitRates = Range.create(32000, 768000); - break; - case CodecProfileLevel.DTS_HDProfileHRA: - case CodecProfileLevel.DTS_HDProfileMA: - sampleRates = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; - bitRates = Range.create(96000, 24500000); - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - mParent.mError |= ERROR_UNRECOGNIZED; - sampleRates = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; - bitRates = Range.create(96000, 24500000); - } - } - maxChannels = 8; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_UHD)) { - for (CodecProfileLevel profileLevel: profileLevels) { - switch (profileLevel.profile) { - case CodecProfileLevel.DTS_UHDProfileP2: - sampleRates = new int[]{ 48000 }; - bitRates = Range.create(96000, 768000); - maxChannels = 10; - break; - case CodecProfileLevel.DTS_UHDProfileP1: - sampleRates = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; - bitRates = Range.create(96000, 24500000); - maxChannels = 32; - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - mParent.mError |= ERROR_UNRECOGNIZED; - sampleRates = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; - bitRates = Range.create(96000, 24500000); - maxChannels = 32; - } - } - } else { - Log.w(TAG, "Unsupported mime " + mime); - mParent.mError |= ERROR_UNSUPPORTED; - } - - // restrict ranges - if (sampleRates != null) { - limitSampleRates(sampleRates); - } else if (sampleRateRange != null) { - limitSampleRates(new Range[] { sampleRateRange }); - } - - Range<Integer> channelRange = Range.create(1, maxChannels); - - applyLimits(new Range[] { channelRange }, bitRates); - } - - private void applyLimits(Range<Integer>[] inputChannels, Range<Integer> bitRates) { - - // clamp & make a local copy - Range<Integer>[] myInputChannels = new Range[inputChannels.length]; - for (int i = 0; i < inputChannels.length; i++) { - int lower = inputChannels[i].clamp(1); - int upper = inputChannels[i].clamp(MAX_INPUT_CHANNEL_COUNT); - myInputChannels[i] = Range.create(lower, upper); - } - - // sort, intersect with existing, & save channel list - sortDistinctRanges(myInputChannels); - Range<Integer>[] joinedChannelList = - intersectSortedDistinctRanges(myInputChannels, mInputChannelRanges); - mInputChannelRanges = joinedChannelList; - - if (bitRates != null) { - mBitrateRange = mBitrateRange.intersect(bitRates); - } - } - - private void parseFromInfo(MediaFormat info) { - int maxInputChannels = MAX_INPUT_CHANNEL_COUNT; - Range<Integer>[] channels = new Range[] { Range.create(1, maxInputChannels)}; - Range<Integer> bitRates = POSITIVE_INTEGERS; - - if (info.containsKey("sample-rate-ranges")) { - String[] rateStrings = info.getString("sample-rate-ranges").split(","); - Range<Integer>[] rateRanges = new Range[rateStrings.length]; - for (int i = 0; i < rateStrings.length; i++) { - rateRanges[i] = Utils.parseIntRange(rateStrings[i], null); - } - limitSampleRates(rateRanges); - } - - // we will prefer channel-ranges over max-channel-count - if (info.containsKey("channel-ranges")) { - String[] channelStrings = info.getString("channel-ranges").split(","); - Range<Integer>[] channelRanges = new Range[channelStrings.length]; - for (int i = 0; i < channelStrings.length; i++) { - channelRanges[i] = Utils.parseIntRange(channelStrings[i], null); - } - channels = channelRanges; - } else if (info.containsKey("channel-range")) { - Range<Integer> oneRange = Utils.parseIntRange(info.getString("channel-range"), - null); - channels = new Range[] { oneRange }; - } else if (info.containsKey("max-channel-count")) { - maxInputChannels = Utils.parseIntSafely( - info.getString("max-channel-count"), maxInputChannels); - if (maxInputChannels == 0) { - channels = new Range[] {Range.create(0, 0)}; - } else { - channels = new Range[] {Range.create(1, maxInputChannels)}; - } - } else if ((mParent.mError & ERROR_UNSUPPORTED) != 0) { - maxInputChannels = 0; - channels = new Range[] {Range.create(0, 0)}; - } - - if (info.containsKey("bitrate-range")) { - bitRates = bitRates.intersect( - Utils.parseIntRange(info.getString("bitrate-range"), bitRates)); - } - - applyLimits(channels, bitRates); + return mImpl.isSampleRateSupported(sampleRate); } /** @hide */ public void getDefaultFormat(MediaFormat format) { - // report settings that have only a single choice - if (mBitrateRange.getLower().equals(mBitrateRange.getUpper())) { - format.setInteger(MediaFormat.KEY_BIT_RATE, mBitrateRange.getLower()); - } - if (getMaxInputChannelCount() == 1) { - // mono-only format - format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); - } - if (mSampleRates != null && mSampleRates.length == 1) { - format.setInteger(MediaFormat.KEY_SAMPLE_RATE, mSampleRates[0]); - } + mImpl.getDefaultFormat(format); } - /* package private */ - // must not contain KEY_PROFILE - static final Set<String> AUDIO_LEVEL_CRITICAL_FORMAT_KEYS = Set.of( - // We don't set level-specific limits for audio codecs today. Key candidates would - // be sample rate, bit rate or channel count. - // MediaFormat.KEY_SAMPLE_RATE, - // MediaFormat.KEY_CHANNEL_COUNT, - // MediaFormat.KEY_BIT_RATE, - MediaFormat.KEY_MIME); - /** @hide */ public boolean supportsFormat(MediaFormat format) { - Map<String, Object> map = format.getMap(); - Integer sampleRate = (Integer)map.get(MediaFormat.KEY_SAMPLE_RATE); - Integer channels = (Integer)map.get(MediaFormat.KEY_CHANNEL_COUNT); - - if (!supports(sampleRate, channels)) { - return false; - } - - if (!CodecCapabilities.supportsBitrate(mBitrateRange, format)) { - return false; - } - - // nothing to do for: - // KEY_CHANNEL_MASK: codecs don't get this - // KEY_IS_ADTS: required feature for all AAC decoders - return true; + return mImpl.supportsFormat(format); } } @@ -1898,304 +2301,6 @@ public final class MediaCodecInfo { */ public static final class VideoCapabilities { private static final String TAG = "VideoCapabilities"; - private CodecCapabilities mParent; - private Range<Integer> mBitrateRange; - - private Range<Integer> mHeightRange; - private Range<Integer> mWidthRange; - private Range<Integer> mBlockCountRange; - private Range<Integer> mHorizontalBlockRange; - private Range<Integer> mVerticalBlockRange; - private Range<Rational> mAspectRatioRange; - private Range<Rational> mBlockAspectRatioRange; - private Range<Long> mBlocksPerSecondRange; - private Map<Size, Range<Long>> mMeasuredFrameRates; - private List<PerformancePoint> mPerformancePoints; - private Range<Integer> mFrameRateRange; - - private int mBlockWidth; - private int mBlockHeight; - private int mWidthAlignment; - private int mHeightAlignment; - private int mSmallerDimensionUpperLimit; - - private boolean mAllowMbOverride; // allow XML to override calculated limits - - /** - * Returns the range of supported bitrates in bits/second. - */ - public Range<Integer> getBitrateRange() { - return mBitrateRange; - } - - /** - * Returns the range of supported video widths. - * <p class=note> - * 32-bit processes will not support resolutions larger than 4096x4096 due to - * the limited address space. - */ - public Range<Integer> getSupportedWidths() { - return mWidthRange; - } - - /** - * Returns the range of supported video heights. - * <p class=note> - * 32-bit processes will not support resolutions larger than 4096x4096 due to - * the limited address space. - */ - public Range<Integer> getSupportedHeights() { - return mHeightRange; - } - - /** - * Returns the alignment requirement for video width (in pixels). - * - * This is a power-of-2 value that video width must be a - * multiple of. - */ - public int getWidthAlignment() { - return mWidthAlignment; - } - - /** - * Returns the alignment requirement for video height (in pixels). - * - * This is a power-of-2 value that video height must be a - * multiple of. - */ - public int getHeightAlignment() { - return mHeightAlignment; - } - - /** - * Return the upper limit on the smaller dimension of width or height. - * <p></p> - * Some codecs have a limit on the smaller dimension, whether it be - * the width or the height. E.g. a codec may only be able to handle - * up to 1920x1080 both in landscape and portrait mode (1080x1920). - * In this case the maximum width and height are both 1920, but the - * smaller dimension limit will be 1080. For other codecs, this is - * {@code Math.min(getSupportedWidths().getUpper(), - * getSupportedHeights().getUpper())}. - * - * @hide - */ - public int getSmallerDimensionUpperLimit() { - return mSmallerDimensionUpperLimit; - } - - /** - * Returns the range of supported frame rates. - * <p> - * This is not a performance indicator. Rather, it expresses the - * limits specified in the coding standard, based on the complexities - * of encoding material for later playback at a certain frame rate, - * or the decoding of such material in non-realtime. - */ - public Range<Integer> getSupportedFrameRates() { - return mFrameRateRange; - } - - /** - * Returns the range of supported video widths for a video height. - * @param height the height of the video - */ - public Range<Integer> getSupportedWidthsFor(int height) { - try { - Range<Integer> range = mWidthRange; - if (!mHeightRange.contains(height) - || (height % mHeightAlignment) != 0) { - throw new IllegalArgumentException("unsupported height"); - } - final int heightInBlocks = Utils.divUp(height, mBlockHeight); - - // constrain by block count and by block aspect ratio - final int minWidthInBlocks = Math.max( - Utils.divUp(mBlockCountRange.getLower(), heightInBlocks), - (int)Math.ceil(mBlockAspectRatioRange.getLower().doubleValue() - * heightInBlocks)); - final int maxWidthInBlocks = Math.min( - mBlockCountRange.getUpper() / heightInBlocks, - (int)(mBlockAspectRatioRange.getUpper().doubleValue() - * heightInBlocks)); - range = range.intersect( - (minWidthInBlocks - 1) * mBlockWidth + mWidthAlignment, - maxWidthInBlocks * mBlockWidth); - - // constrain by smaller dimension limit - if (height > mSmallerDimensionUpperLimit) { - range = range.intersect(1, mSmallerDimensionUpperLimit); - } - - // constrain by aspect ratio - range = range.intersect( - (int)Math.ceil(mAspectRatioRange.getLower().doubleValue() - * height), - (int)(mAspectRatioRange.getUpper().doubleValue() * height)); - return range; - } catch (IllegalArgumentException e) { - // height is not supported because there are no suitable widths - Log.v(TAG, "could not get supported widths for " + height); - throw new IllegalArgumentException("unsupported height"); - } - } - - /** - * Returns the range of supported video heights for a video width - * @param width the width of the video - */ - public Range<Integer> getSupportedHeightsFor(int width) { - try { - Range<Integer> range = mHeightRange; - if (!mWidthRange.contains(width) - || (width % mWidthAlignment) != 0) { - throw new IllegalArgumentException("unsupported width"); - } - final int widthInBlocks = Utils.divUp(width, mBlockWidth); - - // constrain by block count and by block aspect ratio - final int minHeightInBlocks = Math.max( - Utils.divUp(mBlockCountRange.getLower(), widthInBlocks), - (int)Math.ceil(widthInBlocks / - mBlockAspectRatioRange.getUpper().doubleValue())); - final int maxHeightInBlocks = Math.min( - mBlockCountRange.getUpper() / widthInBlocks, - (int)(widthInBlocks / - mBlockAspectRatioRange.getLower().doubleValue())); - range = range.intersect( - (minHeightInBlocks - 1) * mBlockHeight + mHeightAlignment, - maxHeightInBlocks * mBlockHeight); - - // constrain by smaller dimension limit - if (width > mSmallerDimensionUpperLimit) { - range = range.intersect(1, mSmallerDimensionUpperLimit); - } - - // constrain by aspect ratio - range = range.intersect( - (int)Math.ceil(width / - mAspectRatioRange.getUpper().doubleValue()), - (int)(width / mAspectRatioRange.getLower().doubleValue())); - return range; - } catch (IllegalArgumentException e) { - // width is not supported because there are no suitable heights - Log.v(TAG, "could not get supported heights for " + width); - throw new IllegalArgumentException("unsupported width"); - } - } - - /** - * Returns the range of supported video frame rates for a video size. - * <p> - * This is not a performance indicator. Rather, it expresses the limits specified in - * the coding standard, based on the complexities of encoding material of a given - * size for later playback at a certain frame rate, or the decoding of such material - * in non-realtime. - - * @param width the width of the video - * @param height the height of the video - */ - public Range<Double> getSupportedFrameRatesFor(int width, int height) { - Range<Integer> range = mHeightRange; - if (!supports(width, height, null)) { - throw new IllegalArgumentException("unsupported size"); - } - final int blockCount = - Utils.divUp(width, mBlockWidth) * Utils.divUp(height, mBlockHeight); - - return Range.create( - Math.max(mBlocksPerSecondRange.getLower() / (double) blockCount, - (double) mFrameRateRange.getLower()), - Math.min(mBlocksPerSecondRange.getUpper() / (double) blockCount, - (double) mFrameRateRange.getUpper())); - } - - private int getBlockCount(int width, int height) { - return Utils.divUp(width, mBlockWidth) * Utils.divUp(height, mBlockHeight); - } - - @NonNull - private Size findClosestSize(int width, int height) { - int targetBlockCount = getBlockCount(width, height); - Size closestSize = null; - int minDiff = Integer.MAX_VALUE; - for (Size size : mMeasuredFrameRates.keySet()) { - int diff = Math.abs(targetBlockCount - - getBlockCount(size.getWidth(), size.getHeight())); - if (diff < minDiff) { - minDiff = diff; - closestSize = size; - } - } - return closestSize; - } - - private Range<Double> estimateFrameRatesFor(int width, int height) { - Size size = findClosestSize(width, height); - Range<Long> range = mMeasuredFrameRates.get(size); - Double ratio = getBlockCount(size.getWidth(), size.getHeight()) - / (double)Math.max(getBlockCount(width, height), 1); - return Range.create(range.getLower() * ratio, range.getUpper() * ratio); - } - - /** - * Returns the range of achievable video frame rates for a video size. - * May return {@code null}, if the codec did not publish any measurement - * data. - * <p> - * This is a performance estimate provided by the device manufacturer based on statistical - * sampling of full-speed decoding and encoding measurements in various configurations - * of common video sizes supported by the codec. As such it should only be used to - * compare individual codecs on the device. The value is not suitable for comparing - * different devices or even different android releases for the same device. - * <p> - * <em>On {@link android.os.Build.VERSION_CODES#M} release</em> the returned range - * corresponds to the fastest frame rates achieved in the tested configurations. As - * such, it should not be used to gauge guaranteed or even average codec performance - * on the device. - * <p> - * <em>On {@link android.os.Build.VERSION_CODES#N} release</em> the returned range - * corresponds closer to sustained performance <em>in tested configurations</em>. - * One can expect to achieve sustained performance higher than the lower limit more than - * 50% of the time, and higher than half of the lower limit at least 90% of the time - * <em>in tested configurations</em>. - * Conversely, one can expect performance lower than twice the upper limit at least - * 90% of the time. - * <p class=note> - * Tested configurations use a single active codec. For use cases where multiple - * codecs are active, applications can expect lower and in most cases significantly lower - * performance. - * <p class=note> - * The returned range value is interpolated from the nearest frame size(s) tested. - * Codec performance is severely impacted by other activity on the device as well - * as environmental factors (such as battery level, temperature or power source), and can - * vary significantly even in a steady environment. - * <p class=note> - * Use this method in cases where only codec performance matters, e.g. to evaluate if - * a codec has any chance of meeting a performance target. Codecs are listed - * in {@link MediaCodecList} in the preferred order as defined by the device - * manufacturer. As such, applications should use the first suitable codec in the - * list to achieve the best balance between power use and performance. - * - * @param width the width of the video - * @param height the height of the video - * - * @throws IllegalArgumentException if the video size is not supported. - */ - @Nullable - public Range<Double> getAchievableFrameRatesFor(int width, int height) { - if (!supports(width, height, null)) { - throw new IllegalArgumentException("unsupported size"); - } - - if (mMeasuredFrameRates == null || mMeasuredFrameRates.size() <= 0) { - Log.w(TAG, "Codec did not publish any measurement data."); - return null; - } - - return estimateFrameRatesFor(width, height); - } /** * Video performance points are a set of standard performance points defined by number of @@ -2225,6 +2330,24 @@ public final class MediaCodecInfo { } /** + * Width in macroblocks. + * + * @hide + */ + /** package private */ int getWidth() { + return mWidth; + } + + /** + * Height in macroblocks. + * + * @hide + */ + /** package private */ int getHeight() { + return mHeight; + } + + /** * Maximum frame rate in frames per second. * * @hide @@ -2244,6 +2367,24 @@ public final class MediaCodecInfo { return mMaxMacroBlockRate; } + /** + * Codec block width in macroblocks. + * + * @hide + */ + /** package private */ int getBlockWidth() { + return mBlockSize.getWidth(); + } + + /** + * Codec block height in macroblocks. + * + * @hide + */ + /** package private */ int getBlockHeight() { + return mBlockSize.getHeight(); + } + /** Convert to a debug string */ public String toString() { int blockWidth = 16 * mBlockSize.getWidth(); @@ -2331,6 +2472,20 @@ public final class MediaCodecInfo { this(width, height, frameRate, frameRate /* maxFrameRate */, new Size(16, 16)); } + /* package private */ PerformancePoint(int width, int height, int maxFrameRate, + long maxMacroBlockRate, int blockSizeWidth, int blockSizeHeight) { + mWidth = width; + mHeight = height; + mMaxFrameRate = maxFrameRate; + mMaxMacroBlockRate = maxMacroBlockRate; + mBlockSize = new Size(blockSizeWidth, blockSizeHeight); + } + + private PerformancePoint(PerformancePoint pp) { + this(pp.mWidth, pp.mHeight, pp.mMaxFrameRate, pp.mMaxMacroBlockRate, + pp.mBlockSize.getWidth(), pp.mBlockSize.getHeight()); + } + /** Saturates a long value to int */ private int saturateLongToInt(long value) { if (value < Integer.MIN_VALUE) { @@ -2384,14 +2539,18 @@ public final class MediaCodecInfo { * @return {@code true} if the performance point covers the other. */ public boolean covers(@NonNull PerformancePoint other) { - // convert performance points to common block size - Size commonSize = getCommonBlockSize(other); - PerformancePoint aligned = new PerformancePoint(this, commonSize); - PerformancePoint otherAligned = new PerformancePoint(other, commonSize); + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + return native_covers(other); + } else { + // convert performance points to common block size + Size commonSize = getCommonBlockSize(other); + PerformancePoint aligned = new PerformancePoint(this, commonSize); + PerformancePoint otherAligned = new PerformancePoint(other, commonSize); - return (aligned.getMaxMacroBlocks() >= otherAligned.getMaxMacroBlocks() - && aligned.mMaxFrameRate >= otherAligned.mMaxFrameRate - && aligned.mMaxMacroBlockRate >= otherAligned.mMaxMacroBlockRate); + return (aligned.getMaxMacroBlocks() >= otherAligned.getMaxMacroBlocks() + && aligned.mMaxFrameRate >= otherAligned.mMaxFrameRate + && aligned.mMaxMacroBlockRate >= otherAligned.mMaxMacroBlockRate); + } } private @NonNull Size getCommonBlockSize(@NonNull PerformancePoint other) { @@ -2405,17 +2564,28 @@ public final class MediaCodecInfo { if (o instanceof PerformancePoint) { // convert performance points to common block size PerformancePoint other = (PerformancePoint)o; - Size commonSize = getCommonBlockSize(other); - PerformancePoint aligned = new PerformancePoint(this, commonSize); - PerformancePoint otherAligned = new PerformancePoint(other, commonSize); + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + return native_equals(other); + } else { + Size commonSize = getCommonBlockSize(other); + PerformancePoint aligned = new PerformancePoint(this, commonSize); + PerformancePoint otherAligned = new PerformancePoint(other, commonSize); - return (aligned.getMaxMacroBlocks() == otherAligned.getMaxMacroBlocks() - && aligned.mMaxFrameRate == otherAligned.mMaxFrameRate - && aligned.mMaxMacroBlockRate == otherAligned.mMaxMacroBlockRate); + return (aligned.getMaxMacroBlocks() == otherAligned.getMaxMacroBlocks() + && aligned.mMaxFrameRate == otherAligned.mMaxFrameRate + && aligned.mMaxMacroBlockRate == otherAligned.mMaxMacroBlockRate); + } } return false; } + private native boolean native_covers(PerformancePoint other); + private native boolean native_equals(PerformancePoint other); + + static { + System.loadLibrary("media_jni"); + } + /** 480p 24fps */ @NonNull public static final PerformancePoint SD_24 = new PerformancePoint(720, 480, 24); @@ -2520,1351 +2690,1870 @@ public final class MediaCodecInfo { public static final PerformancePoint UHD_240 = new PerformancePoint(3840, 2160, 240); } - /** - * Returns the supported performance points. May return {@code null} if the codec did not - * publish any performance point information (e.g. the vendor codecs have not been updated - * to the latest android release). May return an empty list if the codec published that - * if does not guarantee any performance points. - * <p> - * This is a performance guarantee provided by the device manufacturer for hardware codecs - * based on hardware capabilities of the device. - * <p> - * The returned list is sorted first by decreasing number of pixels, then by decreasing - * width, and finally by decreasing frame rate. - * Performance points assume a single active codec. For use cases where multiple - * codecs are active, should use that highest pixel count, and add the frame rates of - * each individual codec. - * <p class=note> - * 32-bit processes will not support resolutions larger than 4096x4096 due to - * the limited address space, but performance points will be presented as is. - * In other words, even though a component publishes a performance point for - * a resolution higher than 4096x4096, it does not mean that the resolution is supported - * for 32-bit processes. - */ - @Nullable - public List<PerformancePoint> getSupportedPerformancePoints() { - return mPerformancePoints; - } + /* package private */ interface VideoCapsIntf { + public Range<Integer> getBitrateRange(); - /** - * Returns whether a given video size ({@code width} and - * {@code height}) and {@code frameRate} combination is supported. - */ - public boolean areSizeAndRateSupported( - int width, int height, double frameRate) { - return supports(width, height, frameRate); - } + public Range<Integer> getSupportedWidths(); - /** - * Returns whether a given video size ({@code width} and - * {@code height}) is supported. - */ - public boolean isSizeSupported(int width, int height) { - return supports(width, height, null); - } + public Range<Integer> getSupportedHeights(); - private boolean supports(Integer width, Integer height, Number rate) { - boolean ok = true; + public int getWidthAlignment(); - if (ok && width != null) { - ok = mWidthRange.contains(width) - && (width % mWidthAlignment == 0); - } - if (ok && height != null) { - ok = mHeightRange.contains(height) - && (height % mHeightAlignment == 0); - } - if (ok && rate != null) { - ok = mFrameRateRange.contains(Utils.intRangeFor(rate.doubleValue())); - } - if (ok && height != null && width != null) { - ok = Math.min(height, width) <= mSmallerDimensionUpperLimit; + public int getHeightAlignment(); - final int widthInBlocks = Utils.divUp(width, mBlockWidth); - final int heightInBlocks = Utils.divUp(height, mBlockHeight); - final int blockCount = widthInBlocks * heightInBlocks; - ok = ok && mBlockCountRange.contains(blockCount) - && mBlockAspectRatioRange.contains( - new Rational(widthInBlocks, heightInBlocks)) - && mAspectRatioRange.contains(new Rational(width, height)); - if (ok && rate != null) { - double blocksPerSec = blockCount * rate.doubleValue(); - ok = mBlocksPerSecondRange.contains( - Utils.longRangeFor(blocksPerSec)); - } - } - return ok; - } + public int getSmallerDimensionUpperLimit(); - /* package private */ - // must not contain KEY_PROFILE - static final Set<String> VIDEO_LEVEL_CRITICAL_FORMAT_KEYS = Set.of( - MediaFormat.KEY_WIDTH, - MediaFormat.KEY_HEIGHT, - MediaFormat.KEY_FRAME_RATE, - MediaFormat.KEY_BIT_RATE, - MediaFormat.KEY_MIME); + public Range<Integer> getSupportedFrameRates(); - /** - * @hide - * @throws java.lang.ClassCastException */ - public boolean supportsFormat(MediaFormat format) { - final Map<String, Object> map = format.getMap(); - Integer width = (Integer)map.get(MediaFormat.KEY_WIDTH); - Integer height = (Integer)map.get(MediaFormat.KEY_HEIGHT); - Number rate = (Number)map.get(MediaFormat.KEY_FRAME_RATE); + public Range<Integer> getSupportedWidthsFor(int height); - if (!supports(width, height, rate)) { - return false; - } + public Range<Integer> getSupportedHeightsFor(int width); - if (!CodecCapabilities.supportsBitrate(mBitrateRange, format)) { - return false; - } + public Range<Double> getSupportedFrameRatesFor(int width, int height); - // we ignore color-format for now as it is not reliably reported by codec - return true; - } + public Range<Double> getAchievableFrameRatesFor(int width, int height); - /* no public constructor */ - private VideoCapabilities() { } + public boolean areSizeAndRateSupported(int width, int height, double frameRate); - /** @hide */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) - public static VideoCapabilities create( - MediaFormat info, CodecCapabilities parent) { - VideoCapabilities caps = new VideoCapabilities(); - caps.init(info, parent); - return caps; - } + public boolean isSizeSupported(int width, int height); - private void init(MediaFormat info, CodecCapabilities parent) { - mParent = parent; - initWithPlatformLimits(); - applyLevelLimits(); - parseFromInfo(info); - updateLimits(); - } + public boolean supportsFormat(MediaFormat format); - /** @hide */ - public Size getBlockSize() { - return new Size(mBlockWidth, mBlockHeight); + public List<PerformancePoint> getSupportedPerformancePoints(); } - /** @hide */ - public Range<Integer> getBlockCountRange() { - return mBlockCountRange; - } + /* package private */ static final class VideoCapsLegacyImpl implements VideoCapsIntf { + /* package private */ + // must not contain KEY_PROFILE + static final Set<String> VIDEO_LEVEL_CRITICAL_FORMAT_KEYS = Set.of( + MediaFormat.KEY_WIDTH, + MediaFormat.KEY_HEIGHT, + MediaFormat.KEY_FRAME_RATE, + MediaFormat.KEY_BIT_RATE, + MediaFormat.KEY_MIME); + + private CodecCapabilities.CodecCapsLegacyImpl mParent; + private Range<Integer> mBitrateRange; + + private Range<Integer> mHeightRange; + private Range<Integer> mWidthRange; + private Range<Integer> mBlockCountRange; + private Range<Integer> mHorizontalBlockRange; + private Range<Integer> mVerticalBlockRange; + private Range<Rational> mAspectRatioRange; + private Range<Rational> mBlockAspectRatioRange; + private Range<Long> mBlocksPerSecondRange; + private Map<Size, Range<Long>> mMeasuredFrameRates; + private List<PerformancePoint> mPerformancePoints; + private Range<Integer> mFrameRateRange; + + private int mBlockWidth; + private int mBlockHeight; + private int mWidthAlignment; + private int mHeightAlignment; + private int mSmallerDimensionUpperLimit; + + private boolean mAllowMbOverride; // allow XML to override calculated limits + + /* no public constructor */ + private VideoCapsLegacyImpl() { } + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + public static VideoCapsLegacyImpl create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + Log.d(TAG, "Legacy implementation is called while native flag is on."); + } - /** @hide */ - public Range<Long> getBlocksPerSecondRange() { - return mBlocksPerSecondRange; - } + VideoCapsLegacyImpl caps = new VideoCapsLegacyImpl(); + caps.init(info, parent); + return caps; + } - /** @hide */ - public Range<Rational> getAspectRatioRange(boolean blocks) { - return blocks ? mBlockAspectRatioRange : mAspectRatioRange; - } + private void init(MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + mParent = parent; + initWithPlatformLimits(); + applyLevelLimits(); + parseFromInfo(info); + updateLimits(); + } - private void initWithPlatformLimits() { - mBitrateRange = BITRATE_RANGE; + public Range<Integer> getBitrateRange() { + return mBitrateRange; + } - mWidthRange = getSizeRange(); - mHeightRange = getSizeRange(); - mFrameRateRange = FRAME_RATE_RANGE; + public Range<Integer> getSupportedWidths() { + return mWidthRange; + } - mHorizontalBlockRange = getSizeRange(); - mVerticalBlockRange = getSizeRange(); + public Range<Integer> getSupportedHeights() { + return mHeightRange; + } - // full positive ranges are supported as these get calculated - mBlockCountRange = POSITIVE_INTEGERS; - mBlocksPerSecondRange = POSITIVE_LONGS; + public int getWidthAlignment() { + return mWidthAlignment; + } - mBlockAspectRatioRange = POSITIVE_RATIONALS; - mAspectRatioRange = POSITIVE_RATIONALS; + public int getHeightAlignment() { + return mHeightAlignment; + } - mWidthAlignment = 1; - mHeightAlignment = 1; - mBlockWidth = 1; - mBlockHeight = 1; - mSmallerDimensionUpperLimit = getSizeRange().getUpper(); - } + /** @hide */ + public int getSmallerDimensionUpperLimit() { + return mSmallerDimensionUpperLimit; + } + + public Range<Integer> getSupportedFrameRates() { + return mFrameRateRange; + } - private @Nullable List<PerformancePoint> getPerformancePoints(Map<String, Object> map) { - Vector<PerformancePoint> ret = new Vector<>(); - final String prefix = "performance-point-"; - Set<String> keys = map.keySet(); - for (String key : keys) { - // looking for: performance-point-WIDTHxHEIGHT-range - if (!key.startsWith(prefix)) { - continue; + public Range<Integer> getSupportedWidthsFor(int height) { + try { + Range<Integer> range = mWidthRange; + if (!mHeightRange.contains(height) + || (height % mHeightAlignment) != 0) { + throw new IllegalArgumentException("unsupported height"); + } + final int heightInBlocks = Utils.divUp(height, mBlockHeight); + + // constrain by block count and by block aspect ratio + final int minWidthInBlocks = Math.max( + Utils.divUp(mBlockCountRange.getLower(), heightInBlocks), + (int) Math.ceil(mBlockAspectRatioRange.getLower().doubleValue() + * heightInBlocks)); + final int maxWidthInBlocks = Math.min( + mBlockCountRange.getUpper() / heightInBlocks, + (int) (mBlockAspectRatioRange.getUpper().doubleValue() + * heightInBlocks)); + range = range.intersect( + (minWidthInBlocks - 1) * mBlockWidth + mWidthAlignment, + maxWidthInBlocks * mBlockWidth); + + // constrain by smaller dimension limit + if (height > mSmallerDimensionUpperLimit) { + range = range.intersect(1, mSmallerDimensionUpperLimit); + } + + // constrain by aspect ratio + range = range.intersect( + (int) Math.ceil(mAspectRatioRange.getLower().doubleValue() + * height), + (int) (mAspectRatioRange.getUpper().doubleValue() * height)); + return range; + } catch (IllegalArgumentException e) { + // height is not supported because there are no suitable widths + Log.v(TAG, "could not get supported widths for " + height); + throw new IllegalArgumentException("unsupported height"); } - String subKey = key.substring(prefix.length()); - if (subKey.equals("none") && ret.size() == 0) { - // This means that component knowingly did not publish performance points. - // This is different from when the component forgot to publish performance - // points. - return Collections.unmodifiableList(ret); + } + + public Range<Integer> getSupportedHeightsFor(int width) { + try { + Range<Integer> range = mHeightRange; + if (!mWidthRange.contains(width) + || (width % mWidthAlignment) != 0) { + throw new IllegalArgumentException("unsupported width"); + } + final int widthInBlocks = Utils.divUp(width, mBlockWidth); + + // constrain by block count and by block aspect ratio + final int minHeightInBlocks = Math.max( + Utils.divUp(mBlockCountRange.getLower(), widthInBlocks), + (int) Math.ceil(widthInBlocks + / mBlockAspectRatioRange.getUpper().doubleValue())); + final int maxHeightInBlocks = Math.min( + mBlockCountRange.getUpper() / widthInBlocks, + (int) (widthInBlocks + / mBlockAspectRatioRange.getLower().doubleValue())); + range = range.intersect( + (minHeightInBlocks - 1) * mBlockHeight + mHeightAlignment, + maxHeightInBlocks * mBlockHeight); + + // constrain by smaller dimension limit + if (width > mSmallerDimensionUpperLimit) { + range = range.intersect(1, mSmallerDimensionUpperLimit); + } + + // constrain by aspect ratio + range = range.intersect( + (int) Math.ceil(width + / mAspectRatioRange.getUpper().doubleValue()), + (int) (width / mAspectRatioRange.getLower().doubleValue())); + return range; + } catch (IllegalArgumentException e) { + // width is not supported because there are no suitable heights + Log.v(TAG, "could not get supported heights for " + width); + throw new IllegalArgumentException("unsupported width"); } - String[] temp = key.split("-"); - if (temp.length != 4) { - continue; + } + + public Range<Double> getSupportedFrameRatesFor(int width, int height) { + Range<Integer> range = mHeightRange; + if (!supports(width, height, null)) { + throw new IllegalArgumentException("unsupported size"); } - String sizeStr = temp[2]; - Size size = Utils.parseSize(sizeStr, null); - if (size == null || size.getWidth() * size.getHeight() <= 0) { - continue; + final int blockCount = + Utils.divUp(width, mBlockWidth) * Utils.divUp(height, mBlockHeight); + + return Range.create( + Math.max(mBlocksPerSecondRange.getLower() / (double) blockCount, + (double) mFrameRateRange.getLower()), + Math.min(mBlocksPerSecondRange.getUpper() / (double) blockCount, + (double) mFrameRateRange.getUpper())); + } + + private int getBlockCount(int width, int height) { + return Utils.divUp(width, mBlockWidth) * Utils.divUp(height, mBlockHeight); + } + + @NonNull + private Size findClosestSize(int width, int height) { + int targetBlockCount = getBlockCount(width, height); + Size closestSize = null; + int minDiff = Integer.MAX_VALUE; + for (Size size : mMeasuredFrameRates.keySet()) { + int diff = Math.abs(targetBlockCount - + getBlockCount(size.getWidth(), size.getHeight())); + if (diff < minDiff) { + minDiff = diff; + closestSize = size; + } } - Range<Long> range = Utils.parseLongRange(map.get(key), null); - if (range == null || range.getLower() < 0 || range.getUpper() < 0) { - continue; + return closestSize; + } + + private Range<Double> estimateFrameRatesFor(int width, int height) { + Size size = findClosestSize(width, height); + Range<Long> range = mMeasuredFrameRates.get(size); + Double ratio = getBlockCount(size.getWidth(), size.getHeight()) + / (double)Math.max(getBlockCount(width, height), 1); + return Range.create(range.getLower() * ratio, range.getUpper() * ratio); + } + + /** @throws IllegalArgumentException if the video size is not supported. */ + @Nullable + public Range<Double> getAchievableFrameRatesFor(int width, int height) { + if (!supports(width, height, null)) { + throw new IllegalArgumentException("unsupported size"); } - PerformancePoint given = new PerformancePoint( - size.getWidth(), size.getHeight(), range.getLower().intValue(), - range.getUpper().intValue(), new Size(mBlockWidth, mBlockHeight)); - PerformancePoint rotated = new PerformancePoint( - size.getHeight(), size.getWidth(), range.getLower().intValue(), - range.getUpper().intValue(), new Size(mBlockWidth, mBlockHeight)); - ret.add(given); - if (!given.covers(rotated)) { - ret.add(rotated); + + if (mMeasuredFrameRates == null || mMeasuredFrameRates.size() <= 0) { + Log.w(TAG, "Codec did not publish any measurement data."); + return null; } + + return estimateFrameRatesFor(width, height); } - // check if the component specified no performance point indication - if (ret.size() == 0) { - return null; + @Nullable + public List<PerformancePoint> getSupportedPerformancePoints() { + return mPerformancePoints; } - // sort reversed by area first, then by frame rate - ret.sort((a, b) -> - -((a.getMaxMacroBlocks() != b.getMaxMacroBlocks()) ? - (a.getMaxMacroBlocks() < b.getMaxMacroBlocks() ? -1 : 1) : - (a.getMaxMacroBlockRate() != b.getMaxMacroBlockRate()) ? - (a.getMaxMacroBlockRate() < b.getMaxMacroBlockRate() ? -1 : 1) : - (a.getMaxFrameRate() != b.getMaxFrameRate()) ? - (a.getMaxFrameRate() < b.getMaxFrameRate() ? -1 : 1) : 0)); + public boolean areSizeAndRateSupported( + int width, int height, double frameRate) { + return supports(width, height, frameRate); + } - return Collections.unmodifiableList(ret); - } + public boolean isSizeSupported(int width, int height) { + return supports(width, height, null); + } + + private boolean supports(Integer width, Integer height, Number rate) { + boolean ok = true; - private Map<Size, Range<Long>> getMeasuredFrameRates(Map<String, Object> map) { - Map<Size, Range<Long>> ret = new HashMap<Size, Range<Long>>(); - final String prefix = "measured-frame-rate-"; - Set<String> keys = map.keySet(); - for (String key : keys) { - // looking for: measured-frame-rate-WIDTHxHEIGHT-range - if (!key.startsWith(prefix)) { - continue; + if (ok && width != null) { + ok = mWidthRange.contains(width) + && (width % mWidthAlignment == 0); } - String subKey = key.substring(prefix.length()); - String[] temp = key.split("-"); - if (temp.length != 5) { - continue; + if (ok && height != null) { + ok = mHeightRange.contains(height) + && (height % mHeightAlignment == 0); } - String sizeStr = temp[3]; - Size size = Utils.parseSize(sizeStr, null); - if (size == null || size.getWidth() * size.getHeight() <= 0) { - continue; + if (ok && rate != null) { + ok = mFrameRateRange.contains(Utils.intRangeFor(rate.doubleValue())); } - Range<Long> range = Utils.parseLongRange(map.get(key), null); - if (range == null || range.getLower() < 0 || range.getUpper() < 0) { - continue; + if (ok && height != null && width != null) { + ok = Math.min(height, width) <= mSmallerDimensionUpperLimit; + + final int widthInBlocks = Utils.divUp(width, mBlockWidth); + final int heightInBlocks = Utils.divUp(height, mBlockHeight); + final int blockCount = widthInBlocks * heightInBlocks; + ok = ok && mBlockCountRange.contains(blockCount) + && mBlockAspectRatioRange.contains( + new Rational(widthInBlocks, heightInBlocks)) + && mAspectRatioRange.contains(new Rational(width, height)); + if (ok && rate != null) { + double blocksPerSec = blockCount * rate.doubleValue(); + ok = mBlocksPerSecondRange.contains( + Utils.longRangeFor(blocksPerSec)); + } } - ret.put(size, range); + return ok; } - return ret; - } - private static Pair<Range<Integer>, Range<Integer>> parseWidthHeightRanges(Object o) { - Pair<Size, Size> range = Utils.parseSizeRange(o); - if (range != null) { - try { - return Pair.create( - Range.create(range.first.getWidth(), range.second.getWidth()), - Range.create(range.first.getHeight(), range.second.getHeight())); - } catch (IllegalArgumentException e) { - Log.w(TAG, "could not parse size range '" + o + "'"); + /** + * @hide + * @throws java.lang.ClassCastException */ + public boolean supportsFormat(MediaFormat format) { + final Map<String, Object> map = format.getMap(); + Integer width = (Integer)map.get(MediaFormat.KEY_WIDTH); + Integer height = (Integer)map.get(MediaFormat.KEY_HEIGHT); + Number rate = (Number)map.get(MediaFormat.KEY_FRAME_RATE); + + if (!supports(width, height, rate)) { + return false; + } + + if (!CodecCapabilities.CodecCapsLegacyImpl.supportsBitrate(mBitrateRange, format)) { + return false; } + + // we ignore color-format for now as it is not reliably reported by codec + return true; } - return null; - } - /** @hide */ - public static int equivalentVP9Level(MediaFormat info) { - final Map<String, Object> map = info.getMap(); - - Size blockSize = Utils.parseSize(map.get("block-size"), new Size(8, 8)); - int BS = blockSize.getWidth() * blockSize.getHeight(); - - Range<Integer> counts = Utils.parseIntRange(map.get("block-count-range"), null); - int FS = counts == null ? 0 : BS * counts.getUpper(); - - Range<Long> blockRates = - Utils.parseLongRange(map.get("blocks-per-second-range"), null); - long SR = blockRates == null ? 0 : BS * blockRates.getUpper(); - - Pair<Range<Integer>, Range<Integer>> dimensionRanges = - parseWidthHeightRanges(map.get("size-range")); - int D = dimensionRanges == null ? 0 : Math.max( - dimensionRanges.first.getUpper(), dimensionRanges.second.getUpper()); - - Range<Integer> bitRates = Utils.parseIntRange(map.get("bitrate-range"), null); - int BR = bitRates == null ? 0 : Utils.divUp(bitRates.getUpper(), 1000); - - if (SR <= 829440 && FS <= 36864 && BR <= 200 && D <= 512) - return CodecProfileLevel.VP9Level1; - if (SR <= 2764800 && FS <= 73728 && BR <= 800 && D <= 768) - return CodecProfileLevel.VP9Level11; - if (SR <= 4608000 && FS <= 122880 && BR <= 1800 && D <= 960) - return CodecProfileLevel.VP9Level2; - if (SR <= 9216000 && FS <= 245760 && BR <= 3600 && D <= 1344) - return CodecProfileLevel.VP9Level21; - if (SR <= 20736000 && FS <= 552960 && BR <= 7200 && D <= 2048) - return CodecProfileLevel.VP9Level3; - if (SR <= 36864000 && FS <= 983040 && BR <= 12000 && D <= 2752) - return CodecProfileLevel.VP9Level31; - if (SR <= 83558400 && FS <= 2228224 && BR <= 18000 && D <= 4160) - return CodecProfileLevel.VP9Level4; - if (SR <= 160432128 && FS <= 2228224 && BR <= 30000 && D <= 4160) - return CodecProfileLevel.VP9Level41; - if (SR <= 311951360 && FS <= 8912896 && BR <= 60000 && D <= 8384) - return CodecProfileLevel.VP9Level5; - if (SR <= 588251136 && FS <= 8912896 && BR <= 120000 && D <= 8384) - return CodecProfileLevel.VP9Level51; - if (SR <= 1176502272 && FS <= 8912896 && BR <= 180000 && D <= 8384) - return CodecProfileLevel.VP9Level52; - if (SR <= 1176502272 && FS <= 35651584 && BR <= 180000 && D <= 16832) - return CodecProfileLevel.VP9Level6; - if (SR <= 2353004544L && FS <= 35651584 && BR <= 240000 && D <= 16832) - return CodecProfileLevel.VP9Level61; - if (SR <= 4706009088L && FS <= 35651584 && BR <= 480000 && D <= 16832) - return CodecProfileLevel.VP9Level62; - // returning largest level - return CodecProfileLevel.VP9Level62; - } + /** @hide */ + public Size getBlockSize() { + return new Size(mBlockWidth, mBlockHeight); + } - private void parseFromInfo(MediaFormat info) { - final Map<String, Object> map = info.getMap(); - Size blockSize = new Size(mBlockWidth, mBlockHeight); - Size alignment = new Size(mWidthAlignment, mHeightAlignment); - Range<Integer> counts = null, widths = null, heights = null; - Range<Integer> frameRates = null, bitRates = null; - Range<Long> blockRates = null; - Range<Rational> ratios = null, blockRatios = null; - - blockSize = Utils.parseSize(map.get("block-size"), blockSize); - alignment = Utils.parseSize(map.get("alignment"), alignment); - counts = Utils.parseIntRange(map.get("block-count-range"), null); - blockRates = - Utils.parseLongRange(map.get("blocks-per-second-range"), null); - mMeasuredFrameRates = getMeasuredFrameRates(map); - mPerformancePoints = getPerformancePoints(map); - Pair<Range<Integer>, Range<Integer>> sizeRanges = - parseWidthHeightRanges(map.get("size-range")); - if (sizeRanges != null) { - widths = sizeRanges.first; - heights = sizeRanges.second; - } - // for now this just means using the smaller max size as 2nd - // upper limit. - // for now we are keeping the profile specific "width/height - // in macroblocks" limits. - if (map.containsKey("feature-can-swap-width-height")) { - if (widths != null) { - mSmallerDimensionUpperLimit = - Math.min(widths.getUpper(), heights.getUpper()); - widths = heights = widths.extend(heights); - } else { - Log.w(TAG, "feature can-swap-width-height is best used with size-range"); - mSmallerDimensionUpperLimit = - Math.min(mWidthRange.getUpper(), mHeightRange.getUpper()); - mWidthRange = mHeightRange = mWidthRange.extend(mHeightRange); - } + /** @hide */ + public Range<Integer> getBlockCountRange() { + return mBlockCountRange; } - ratios = Utils.parseRationalRange( - map.get("block-aspect-ratio-range"), null); - blockRatios = Utils.parseRationalRange( - map.get("pixel-aspect-ratio-range"), null); - frameRates = Utils.parseIntRange(map.get("frame-rate-range"), null); - if (frameRates != null) { - try { - frameRates = frameRates.intersect(FRAME_RATE_RANGE); - } catch (IllegalArgumentException e) { - Log.w(TAG, "frame rate range (" + frameRates - + ") is out of limits: " + FRAME_RATE_RANGE); - frameRates = null; - } + /** @hide */ + public Range<Long> getBlocksPerSecondRange() { + return mBlocksPerSecondRange; } - bitRates = Utils.parseIntRange(map.get("bitrate-range"), null); - if (bitRates != null) { - try { - bitRates = bitRates.intersect(BITRATE_RANGE); - } catch (IllegalArgumentException e) { - Log.w(TAG, "bitrate range (" + bitRates - + ") is out of limits: " + BITRATE_RANGE); - bitRates = null; - } + + /** @hide */ + public Range<Rational> getAspectRatioRange(boolean blocks) { + return blocks ? mBlockAspectRatioRange : mAspectRatioRange; } - checkPowerOfTwo( - blockSize.getWidth(), "block-size width must be power of two"); - checkPowerOfTwo( - blockSize.getHeight(), "block-size height must be power of two"); + private void initWithPlatformLimits() { + mBitrateRange = BITRATE_RANGE; - checkPowerOfTwo( - alignment.getWidth(), "alignment width must be power of two"); - checkPowerOfTwo( - alignment.getHeight(), "alignment height must be power of two"); + mWidthRange = getSizeRange(); + mHeightRange = getSizeRange(); + mFrameRateRange = FRAME_RATE_RANGE; - // update block-size and alignment - applyMacroBlockLimits( - Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, - Long.MAX_VALUE, blockSize.getWidth(), blockSize.getHeight(), - alignment.getWidth(), alignment.getHeight()); + mHorizontalBlockRange = getSizeRange(); + mVerticalBlockRange = getSizeRange(); - if ((mParent.mError & ERROR_UNSUPPORTED) != 0 || mAllowMbOverride) { - // codec supports profiles that we don't know. - // Use supplied values clipped to platform limits - if (widths != null) { - mWidthRange = getSizeRange().intersect(widths); - } - if (heights != null) { - mHeightRange = getSizeRange().intersect(heights); - } - if (counts != null) { - mBlockCountRange = POSITIVE_INTEGERS.intersect( - Utils.factorRange(counts, mBlockWidth * mBlockHeight - / blockSize.getWidth() / blockSize.getHeight())); + // full positive ranges are supported as these get calculated + mBlockCountRange = POSITIVE_INTEGERS; + mBlocksPerSecondRange = POSITIVE_LONGS; + + mBlockAspectRatioRange = POSITIVE_RATIONALS; + mAspectRatioRange = POSITIVE_RATIONALS; + + mWidthAlignment = 1; + mHeightAlignment = 1; + mBlockWidth = 1; + mBlockHeight = 1; + mSmallerDimensionUpperLimit = getSizeRange().getUpper(); + } + + private @Nullable List<PerformancePoint> getPerformancePoints(Map<String, Object> map) { + Vector<PerformancePoint> ret = new Vector<>(); + final String prefix = "performance-point-"; + Set<String> keys = map.keySet(); + for (String key : keys) { + // looking for: performance-point-WIDTHxHEIGHT-range + if (!key.startsWith(prefix)) { + continue; + } + String subKey = key.substring(prefix.length()); + if (subKey.equals("none") && ret.size() == 0) { + // This means that component knowingly did not publish performance points. + // This is different from when the component forgot to publish performance + // points. + return Collections.unmodifiableList(ret); + } + String[] temp = key.split("-"); + if (temp.length != 4) { + continue; + } + String sizeStr = temp[2]; + Size size = Utils.parseSize(sizeStr, null); + if (size == null || size.getWidth() * size.getHeight() <= 0) { + continue; + } + Range<Long> range = Utils.parseLongRange(map.get(key), null); + if (range == null || range.getLower() < 0 || range.getUpper() < 0) { + continue; + } + PerformancePoint given = new PerformancePoint( + size.getWidth(), size.getHeight(), range.getLower().intValue(), + range.getUpper().intValue(), new Size(mBlockWidth, mBlockHeight)); + PerformancePoint rotated = new PerformancePoint( + size.getHeight(), size.getWidth(), range.getLower().intValue(), + range.getUpper().intValue(), new Size(mBlockWidth, mBlockHeight)); + ret.add(given); + if (!given.covers(rotated)) { + ret.add(rotated); + } } - if (blockRates != null) { - mBlocksPerSecondRange = POSITIVE_LONGS.intersect( - Utils.factorRange(blockRates, mBlockWidth * mBlockHeight - / blockSize.getWidth() / blockSize.getHeight())); + + // check if the component specified no performance point indication + if (ret.size() == 0) { + return null; } - if (blockRatios != null) { - mBlockAspectRatioRange = POSITIVE_RATIONALS.intersect( - Utils.scaleRange(blockRatios, - mBlockHeight / blockSize.getHeight(), - mBlockWidth / blockSize.getWidth())); + + // sort reversed by area first, then by frame rate + ret.sort((a, b) -> + -((a.getMaxMacroBlocks() != b.getMaxMacroBlocks()) ? + (a.getMaxMacroBlocks() < b.getMaxMacroBlocks() ? -1 : 1) : + (a.getMaxMacroBlockRate() != b.getMaxMacroBlockRate()) ? + (a.getMaxMacroBlockRate() < b.getMaxMacroBlockRate() ? -1 : 1) : + (a.getMaxFrameRate() != b.getMaxFrameRate()) ? + (a.getMaxFrameRate() < b.getMaxFrameRate() ? -1 : 1) : 0)); + + return Collections.unmodifiableList(ret); + } + + private Map<Size, Range<Long>> getMeasuredFrameRates(Map<String, Object> map) { + Map<Size, Range<Long>> ret = new HashMap<Size, Range<Long>>(); + final String prefix = "measured-frame-rate-"; + Set<String> keys = map.keySet(); + for (String key : keys) { + // looking for: measured-frame-rate-WIDTHxHEIGHT-range + if (!key.startsWith(prefix)) { + continue; + } + String subKey = key.substring(prefix.length()); + String[] temp = key.split("-"); + if (temp.length != 5) { + continue; + } + String sizeStr = temp[3]; + Size size = Utils.parseSize(sizeStr, null); + if (size == null || size.getWidth() * size.getHeight() <= 0) { + continue; + } + Range<Long> range = Utils.parseLongRange(map.get(key), null); + if (range == null || range.getLower() < 0 || range.getUpper() < 0) { + continue; + } + ret.put(size, range); } - if (ratios != null) { - mAspectRatioRange = POSITIVE_RATIONALS.intersect(ratios); + return ret; + } + + private static Pair<Range<Integer>, Range<Integer>> parseWidthHeightRanges(Object o) { + Pair<Size, Size> range = Utils.parseSizeRange(o); + if (range != null) { + try { + return Pair.create( + Range.create(range.first.getWidth(), range.second.getWidth()), + Range.create(range.first.getHeight(), range.second.getHeight())); + } catch (IllegalArgumentException e) { + Log.w(TAG, "could not parse size range '" + o + "'"); + } } - if (frameRates != null) { - mFrameRateRange = FRAME_RATE_RANGE.intersect(frameRates); + return null; + } + + /** @hide */ + public static int equivalentVP9Level(MediaFormat info) { + final Map<String, Object> map = info.getMap(); + + Size blockSize = Utils.parseSize(map.get("block-size"), new Size(8, 8)); + int BS = blockSize.getWidth() * blockSize.getHeight(); + + Range<Integer> counts = Utils.parseIntRange(map.get("block-count-range"), null); + int FS = counts == null ? 0 : BS * counts.getUpper(); + + Range<Long> blockRates = + Utils.parseLongRange(map.get("blocks-per-second-range"), null); + long SR = blockRates == null ? 0 : BS * blockRates.getUpper(); + + Pair<Range<Integer>, Range<Integer>> dimensionRanges = + parseWidthHeightRanges(map.get("size-range")); + int D = dimensionRanges == null ? 0 : Math.max( + dimensionRanges.first.getUpper(), dimensionRanges.second.getUpper()); + + Range<Integer> bitRates = Utils.parseIntRange(map.get("bitrate-range"), null); + int BR = bitRates == null ? 0 : Utils.divUp(bitRates.getUpper(), 1000); + + if (SR <= 829440 && FS <= 36864 && BR <= 200 && D <= 512) + return CodecProfileLevel.VP9Level1; + if (SR <= 2764800 && FS <= 73728 && BR <= 800 && D <= 768) + return CodecProfileLevel.VP9Level11; + if (SR <= 4608000 && FS <= 122880 && BR <= 1800 && D <= 960) + return CodecProfileLevel.VP9Level2; + if (SR <= 9216000 && FS <= 245760 && BR <= 3600 && D <= 1344) + return CodecProfileLevel.VP9Level21; + if (SR <= 20736000 && FS <= 552960 && BR <= 7200 && D <= 2048) + return CodecProfileLevel.VP9Level3; + if (SR <= 36864000 && FS <= 983040 && BR <= 12000 && D <= 2752) + return CodecProfileLevel.VP9Level31; + if (SR <= 83558400 && FS <= 2228224 && BR <= 18000 && D <= 4160) + return CodecProfileLevel.VP9Level4; + if (SR <= 160432128 && FS <= 2228224 && BR <= 30000 && D <= 4160) + return CodecProfileLevel.VP9Level41; + if (SR <= 311951360 && FS <= 8912896 && BR <= 60000 && D <= 8384) + return CodecProfileLevel.VP9Level5; + if (SR <= 588251136 && FS <= 8912896 && BR <= 120000 && D <= 8384) + return CodecProfileLevel.VP9Level51; + if (SR <= 1176502272 && FS <= 8912896 && BR <= 180000 && D <= 8384) + return CodecProfileLevel.VP9Level52; + if (SR <= 1176502272 && FS <= 35651584 && BR <= 180000 && D <= 16832) + return CodecProfileLevel.VP9Level6; + if (SR <= 2353004544L && FS <= 35651584 && BR <= 240000 && D <= 16832) + return CodecProfileLevel.VP9Level61; + if (SR <= 4706009088L && FS <= 35651584 && BR <= 480000 && D <= 16832) + return CodecProfileLevel.VP9Level62; + // returning largest level + return CodecProfileLevel.VP9Level62; + } + + private void parseFromInfo(MediaFormat info) { + final Map<String, Object> map = info.getMap(); + Size blockSize = new Size(mBlockWidth, mBlockHeight); + Size alignment = new Size(mWidthAlignment, mHeightAlignment); + Range<Integer> counts = null, widths = null, heights = null; + Range<Integer> frameRates = null, bitRates = null; + Range<Long> blockRates = null; + Range<Rational> ratios = null, blockRatios = null; + + blockSize = Utils.parseSize(map.get("block-size"), blockSize); + alignment = Utils.parseSize(map.get("alignment"), alignment); + counts = Utils.parseIntRange(map.get("block-count-range"), null); + blockRates = + Utils.parseLongRange(map.get("blocks-per-second-range"), null); + mMeasuredFrameRates = getMeasuredFrameRates(map); + mPerformancePoints = getPerformancePoints(map); + Pair<Range<Integer>, Range<Integer>> sizeRanges = + parseWidthHeightRanges(map.get("size-range")); + if (sizeRanges != null) { + widths = sizeRanges.first; + heights = sizeRanges.second; } - if (bitRates != null) { - // only allow bitrate override if unsupported profiles were encountered - if ((mParent.mError & ERROR_UNSUPPORTED) != 0) { - mBitrateRange = BITRATE_RANGE.intersect(bitRates); + // for now this just means using the smaller max size as 2nd + // upper limit. + // for now we are keeping the profile specific "width/height + // in macroblocks" limits. + if (map.containsKey("feature-can-swap-width-height")) { + if (widths != null) { + mSmallerDimensionUpperLimit = + Math.min(widths.getUpper(), heights.getUpper()); + widths = heights = widths.extend(heights); } else { - mBitrateRange = mBitrateRange.intersect(bitRates); + Log.w(TAG, "feature can-swap-width-height is best used with size-range"); + mSmallerDimensionUpperLimit = + Math.min(mWidthRange.getUpper(), mHeightRange.getUpper()); + mWidthRange = mHeightRange = mWidthRange.extend(mHeightRange); } } - } else { - // no unsupported profile/levels, so restrict values to known limits - if (widths != null) { - mWidthRange = mWidthRange.intersect(widths); - } - if (heights != null) { - mHeightRange = mHeightRange.intersect(heights); - } - if (counts != null) { - mBlockCountRange = mBlockCountRange.intersect( - Utils.factorRange(counts, mBlockWidth * mBlockHeight - / blockSize.getWidth() / blockSize.getHeight())); - } - if (blockRates != null) { - mBlocksPerSecondRange = mBlocksPerSecondRange.intersect( - Utils.factorRange(blockRates, mBlockWidth * mBlockHeight - / blockSize.getWidth() / blockSize.getHeight())); - } - if (blockRatios != null) { - mBlockAspectRatioRange = mBlockAspectRatioRange.intersect( - Utils.scaleRange(blockRatios, - mBlockHeight / blockSize.getHeight(), - mBlockWidth / blockSize.getWidth())); - } - if (ratios != null) { - mAspectRatioRange = mAspectRatioRange.intersect(ratios); - } + + ratios = Utils.parseRationalRange( + map.get("block-aspect-ratio-range"), null); + blockRatios = Utils.parseRationalRange( + map.get("pixel-aspect-ratio-range"), null); + frameRates = Utils.parseIntRange(map.get("frame-rate-range"), null); if (frameRates != null) { - mFrameRateRange = mFrameRateRange.intersect(frameRates); + try { + frameRates = frameRates.intersect(FRAME_RATE_RANGE); + } catch (IllegalArgumentException e) { + Log.w(TAG, "frame rate range (" + frameRates + + ") is out of limits: " + FRAME_RATE_RANGE); + frameRates = null; + } } + bitRates = Utils.parseIntRange(map.get("bitrate-range"), null); if (bitRates != null) { - mBitrateRange = mBitrateRange.intersect(bitRates); + try { + bitRates = bitRates.intersect(BITRATE_RANGE); + } catch (IllegalArgumentException e) { + Log.w(TAG, "bitrate range (" + bitRates + + ") is out of limits: " + BITRATE_RANGE); + bitRates = null; + } } - } - updateLimits(); - } - private void applyBlockLimits( - int blockWidth, int blockHeight, - Range<Integer> counts, Range<Long> rates, Range<Rational> ratios) { - checkPowerOfTwo(blockWidth, "blockWidth must be a power of two"); - checkPowerOfTwo(blockHeight, "blockHeight must be a power of two"); - - final int newBlockWidth = Math.max(blockWidth, mBlockWidth); - final int newBlockHeight = Math.max(blockHeight, mBlockHeight); - - // factor will always be a power-of-2 - int factor = - newBlockWidth * newBlockHeight / mBlockWidth / mBlockHeight; - if (factor != 1) { - mBlockCountRange = Utils.factorRange(mBlockCountRange, factor); - mBlocksPerSecondRange = Utils.factorRange( - mBlocksPerSecondRange, factor); - mBlockAspectRatioRange = Utils.scaleRange( - mBlockAspectRatioRange, - newBlockHeight / mBlockHeight, - newBlockWidth / mBlockWidth); - mHorizontalBlockRange = Utils.factorRange( - mHorizontalBlockRange, newBlockWidth / mBlockWidth); - mVerticalBlockRange = Utils.factorRange( - mVerticalBlockRange, newBlockHeight / mBlockHeight); - } - factor = newBlockWidth * newBlockHeight / blockWidth / blockHeight; - if (factor != 1) { - counts = Utils.factorRange(counts, factor); - rates = Utils.factorRange(rates, factor); - ratios = Utils.scaleRange( - ratios, newBlockHeight / blockHeight, - newBlockWidth / blockWidth); - } - mBlockCountRange = mBlockCountRange.intersect(counts); - mBlocksPerSecondRange = mBlocksPerSecondRange.intersect(rates); - mBlockAspectRatioRange = mBlockAspectRatioRange.intersect(ratios); - mBlockWidth = newBlockWidth; - mBlockHeight = newBlockHeight; - } + checkPowerOfTwo( + blockSize.getWidth(), "block-size width must be power of two"); + checkPowerOfTwo( + blockSize.getHeight(), "block-size height must be power of two"); - private void applyAlignment(int widthAlignment, int heightAlignment) { - checkPowerOfTwo(widthAlignment, "widthAlignment must be a power of two"); - checkPowerOfTwo(heightAlignment, "heightAlignment must be a power of two"); + checkPowerOfTwo( + alignment.getWidth(), "alignment width must be power of two"); + checkPowerOfTwo( + alignment.getHeight(), "alignment height must be power of two"); - if (widthAlignment > mBlockWidth || heightAlignment > mBlockHeight) { - // maintain assumption that 0 < alignment <= block-size - applyBlockLimits( - Math.max(widthAlignment, mBlockWidth), - Math.max(heightAlignment, mBlockHeight), - POSITIVE_INTEGERS, POSITIVE_LONGS, POSITIVE_RATIONALS); + // update block-size and alignment + applyMacroBlockLimits( + Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, + Long.MAX_VALUE, blockSize.getWidth(), blockSize.getHeight(), + alignment.getWidth(), alignment.getHeight()); + + if ((mParent.mError & ERROR_UNSUPPORTED) != 0 || mAllowMbOverride) { + // codec supports profiles that we don't know. + // Use supplied values clipped to platform limits + if (widths != null) { + mWidthRange = getSizeRange().intersect(widths); + } + if (heights != null) { + mHeightRange = getSizeRange().intersect(heights); + } + if (counts != null) { + mBlockCountRange = POSITIVE_INTEGERS.intersect( + Utils.factorRange(counts, mBlockWidth * mBlockHeight + / blockSize.getWidth() / blockSize.getHeight())); + } + if (blockRates != null) { + mBlocksPerSecondRange = POSITIVE_LONGS.intersect( + Utils.factorRange(blockRates, mBlockWidth * mBlockHeight + / blockSize.getWidth() / blockSize.getHeight())); + } + if (blockRatios != null) { + mBlockAspectRatioRange = POSITIVE_RATIONALS.intersect( + Utils.scaleRange(blockRatios, + mBlockHeight / blockSize.getHeight(), + mBlockWidth / blockSize.getWidth())); + } + if (ratios != null) { + mAspectRatioRange = POSITIVE_RATIONALS.intersect(ratios); + } + if (frameRates != null) { + mFrameRateRange = FRAME_RATE_RANGE.intersect(frameRates); + } + if (bitRates != null) { + // only allow bitrate override if unsupported profiles were encountered + if ((mParent.mError & ERROR_UNSUPPORTED) != 0) { + mBitrateRange = BITRATE_RANGE.intersect(bitRates); + } else { + mBitrateRange = mBitrateRange.intersect(bitRates); + } + } + } else { + // no unsupported profile/levels, so restrict values to known limits + if (widths != null) { + mWidthRange = mWidthRange.intersect(widths); + } + if (heights != null) { + mHeightRange = mHeightRange.intersect(heights); + } + if (counts != null) { + mBlockCountRange = mBlockCountRange.intersect( + Utils.factorRange(counts, mBlockWidth * mBlockHeight + / blockSize.getWidth() / blockSize.getHeight())); + } + if (blockRates != null) { + mBlocksPerSecondRange = mBlocksPerSecondRange.intersect( + Utils.factorRange(blockRates, mBlockWidth * mBlockHeight + / blockSize.getWidth() / blockSize.getHeight())); + } + if (blockRatios != null) { + mBlockAspectRatioRange = mBlockAspectRatioRange.intersect( + Utils.scaleRange(blockRatios, + mBlockHeight / blockSize.getHeight(), + mBlockWidth / blockSize.getWidth())); + } + if (ratios != null) { + mAspectRatioRange = mAspectRatioRange.intersect(ratios); + } + if (frameRates != null) { + mFrameRateRange = mFrameRateRange.intersect(frameRates); + } + if (bitRates != null) { + mBitrateRange = mBitrateRange.intersect(bitRates); + } + } + updateLimits(); } - mWidthAlignment = Math.max(widthAlignment, mWidthAlignment); - mHeightAlignment = Math.max(heightAlignment, mHeightAlignment); + private void applyBlockLimits( + int blockWidth, int blockHeight, + Range<Integer> counts, Range<Long> rates, Range<Rational> ratios) { + checkPowerOfTwo(blockWidth, "blockWidth must be a power of two"); + checkPowerOfTwo(blockHeight, "blockHeight must be a power of two"); + + final int newBlockWidth = Math.max(blockWidth, mBlockWidth); + final int newBlockHeight = Math.max(blockHeight, mBlockHeight); + + // factor will always be a power-of-2 + int factor = + newBlockWidth * newBlockHeight / mBlockWidth / mBlockHeight; + if (factor != 1) { + mBlockCountRange = Utils.factorRange(mBlockCountRange, factor); + mBlocksPerSecondRange = Utils.factorRange( + mBlocksPerSecondRange, factor); + mBlockAspectRatioRange = Utils.scaleRange( + mBlockAspectRatioRange, + newBlockHeight / mBlockHeight, + newBlockWidth / mBlockWidth); + mHorizontalBlockRange = Utils.factorRange( + mHorizontalBlockRange, newBlockWidth / mBlockWidth); + mVerticalBlockRange = Utils.factorRange( + mVerticalBlockRange, newBlockHeight / mBlockHeight); + } + factor = newBlockWidth * newBlockHeight / blockWidth / blockHeight; + if (factor != 1) { + counts = Utils.factorRange(counts, factor); + rates = Utils.factorRange(rates, factor); + ratios = Utils.scaleRange( + ratios, newBlockHeight / blockHeight, + newBlockWidth / blockWidth); + } + mBlockCountRange = mBlockCountRange.intersect(counts); + mBlocksPerSecondRange = mBlocksPerSecondRange.intersect(rates); + mBlockAspectRatioRange = mBlockAspectRatioRange.intersect(ratios); + mBlockWidth = newBlockWidth; + mBlockHeight = newBlockHeight; + } - mWidthRange = Utils.alignRange(mWidthRange, mWidthAlignment); - mHeightRange = Utils.alignRange(mHeightRange, mHeightAlignment); - } + private void applyAlignment(int widthAlignment, int heightAlignment) { + checkPowerOfTwo(widthAlignment, "widthAlignment must be a power of two"); + checkPowerOfTwo(heightAlignment, "heightAlignment must be a power of two"); - private void updateLimits() { - // pixels -> blocks <- counts - mHorizontalBlockRange = mHorizontalBlockRange.intersect( - Utils.factorRange(mWidthRange, mBlockWidth)); - mHorizontalBlockRange = mHorizontalBlockRange.intersect( - Range.create( - mBlockCountRange.getLower() / mVerticalBlockRange.getUpper(), - mBlockCountRange.getUpper() / mVerticalBlockRange.getLower())); - mVerticalBlockRange = mVerticalBlockRange.intersect( - Utils.factorRange(mHeightRange, mBlockHeight)); - mVerticalBlockRange = mVerticalBlockRange.intersect( - Range.create( - mBlockCountRange.getLower() / mHorizontalBlockRange.getUpper(), - mBlockCountRange.getUpper() / mHorizontalBlockRange.getLower())); - mBlockCountRange = mBlockCountRange.intersect( - Range.create( - mHorizontalBlockRange.getLower() - * mVerticalBlockRange.getLower(), - mHorizontalBlockRange.getUpper() - * mVerticalBlockRange.getUpper())); - mBlockAspectRatioRange = mBlockAspectRatioRange.intersect( - new Rational( - mHorizontalBlockRange.getLower(), mVerticalBlockRange.getUpper()), - new Rational( - mHorizontalBlockRange.getUpper(), mVerticalBlockRange.getLower())); - - // blocks -> pixels - mWidthRange = mWidthRange.intersect( - (mHorizontalBlockRange.getLower() - 1) * mBlockWidth + mWidthAlignment, - mHorizontalBlockRange.getUpper() * mBlockWidth); - mHeightRange = mHeightRange.intersect( - (mVerticalBlockRange.getLower() - 1) * mBlockHeight + mHeightAlignment, - mVerticalBlockRange.getUpper() * mBlockHeight); - mAspectRatioRange = mAspectRatioRange.intersect( - new Rational(mWidthRange.getLower(), mHeightRange.getUpper()), - new Rational(mWidthRange.getUpper(), mHeightRange.getLower())); - - mSmallerDimensionUpperLimit = Math.min( - mSmallerDimensionUpperLimit, - Math.min(mWidthRange.getUpper(), mHeightRange.getUpper())); - - // blocks -> rate - mBlocksPerSecondRange = mBlocksPerSecondRange.intersect( - mBlockCountRange.getLower() * (long)mFrameRateRange.getLower(), - mBlockCountRange.getUpper() * (long)mFrameRateRange.getUpper()); - mFrameRateRange = mFrameRateRange.intersect( - (int)(mBlocksPerSecondRange.getLower() - / mBlockCountRange.getUpper()), - (int)(mBlocksPerSecondRange.getUpper() - / (double)mBlockCountRange.getLower())); - } + if (widthAlignment > mBlockWidth || heightAlignment > mBlockHeight) { + // maintain assumption that 0 < alignment <= block-size + applyBlockLimits( + Math.max(widthAlignment, mBlockWidth), + Math.max(heightAlignment, mBlockHeight), + POSITIVE_INTEGERS, POSITIVE_LONGS, POSITIVE_RATIONALS); + } - private void applyMacroBlockLimits( - int maxHorizontalBlocks, int maxVerticalBlocks, - int maxBlocks, long maxBlocksPerSecond, - int blockWidth, int blockHeight, - int widthAlignment, int heightAlignment) { - applyMacroBlockLimits( - 1 /* minHorizontalBlocks */, 1 /* minVerticalBlocks */, - maxHorizontalBlocks, maxVerticalBlocks, - maxBlocks, maxBlocksPerSecond, - blockWidth, blockHeight, widthAlignment, heightAlignment); - } + mWidthAlignment = Math.max(widthAlignment, mWidthAlignment); + mHeightAlignment = Math.max(heightAlignment, mHeightAlignment); - private void applyMacroBlockLimits( - int minHorizontalBlocks, int minVerticalBlocks, - int maxHorizontalBlocks, int maxVerticalBlocks, - int maxBlocks, long maxBlocksPerSecond, - int blockWidth, int blockHeight, - int widthAlignment, int heightAlignment) { - applyAlignment(widthAlignment, heightAlignment); - applyBlockLimits( - blockWidth, blockHeight, Range.create(1, maxBlocks), - Range.create(1L, maxBlocksPerSecond), - Range.create( - new Rational(1, maxVerticalBlocks), - new Rational(maxHorizontalBlocks, 1))); - mHorizontalBlockRange = - mHorizontalBlockRange.intersect( - Utils.divUp(minHorizontalBlocks, (mBlockWidth / blockWidth)), - maxHorizontalBlocks / (mBlockWidth / blockWidth)); - mVerticalBlockRange = - mVerticalBlockRange.intersect( - Utils.divUp(minVerticalBlocks, (mBlockHeight / blockHeight)), - maxVerticalBlocks / (mBlockHeight / blockHeight)); - } + mWidthRange = Utils.alignRange(mWidthRange, mWidthAlignment); + mHeightRange = Utils.alignRange(mHeightRange, mHeightAlignment); + } - private void applyLevelLimits() { - long maxBlocksPerSecond = 0; - int maxBlocks = 0; - int maxBps = 0; - int maxDPBBlocks = 0; - - int errors = ERROR_NONE_SUPPORTED; - CodecProfileLevel[] profileLevels = mParent.profileLevels; - String mime = mParent.getMimeType(); - - if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AVC)) { - maxBlocks = 99; - maxBlocksPerSecond = 1485; - maxBps = 64000; - maxDPBBlocks = 396; - for (CodecProfileLevel profileLevel: profileLevels) { - int MBPS = 0, FS = 0, BR = 0, DPB = 0; - boolean supported = true; - switch (profileLevel.level) { - case CodecProfileLevel.AVCLevel1: - MBPS = 1485; FS = 99; BR = 64; DPB = 396; break; - case CodecProfileLevel.AVCLevel1b: - MBPS = 1485; FS = 99; BR = 128; DPB = 396; break; - case CodecProfileLevel.AVCLevel11: - MBPS = 3000; FS = 396; BR = 192; DPB = 900; break; - case CodecProfileLevel.AVCLevel12: - MBPS = 6000; FS = 396; BR = 384; DPB = 2376; break; - case CodecProfileLevel.AVCLevel13: - MBPS = 11880; FS = 396; BR = 768; DPB = 2376; break; - case CodecProfileLevel.AVCLevel2: - MBPS = 11880; FS = 396; BR = 2000; DPB = 2376; break; - case CodecProfileLevel.AVCLevel21: - MBPS = 19800; FS = 792; BR = 4000; DPB = 4752; break; - case CodecProfileLevel.AVCLevel22: - MBPS = 20250; FS = 1620; BR = 4000; DPB = 8100; break; - case CodecProfileLevel.AVCLevel3: - MBPS = 40500; FS = 1620; BR = 10000; DPB = 8100; break; - case CodecProfileLevel.AVCLevel31: - MBPS = 108000; FS = 3600; BR = 14000; DPB = 18000; break; - case CodecProfileLevel.AVCLevel32: - MBPS = 216000; FS = 5120; BR = 20000; DPB = 20480; break; - case CodecProfileLevel.AVCLevel4: - MBPS = 245760; FS = 8192; BR = 20000; DPB = 32768; break; - case CodecProfileLevel.AVCLevel41: - MBPS = 245760; FS = 8192; BR = 50000; DPB = 32768; break; - case CodecProfileLevel.AVCLevel42: - MBPS = 522240; FS = 8704; BR = 50000; DPB = 34816; break; - case CodecProfileLevel.AVCLevel5: - MBPS = 589824; FS = 22080; BR = 135000; DPB = 110400; break; - case CodecProfileLevel.AVCLevel51: - MBPS = 983040; FS = 36864; BR = 240000; DPB = 184320; break; - case CodecProfileLevel.AVCLevel52: - MBPS = 2073600; FS = 36864; BR = 240000; DPB = 184320; break; - case CodecProfileLevel.AVCLevel6: - MBPS = 4177920; FS = 139264; BR = 240000; DPB = 696320; break; - case CodecProfileLevel.AVCLevel61: - MBPS = 8355840; FS = 139264; BR = 480000; DPB = 696320; break; - case CodecProfileLevel.AVCLevel62: - MBPS = 16711680; FS = 139264; BR = 800000; DPB = 696320; break; - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - switch (profileLevel.profile) { - case CodecProfileLevel.AVCProfileConstrainedHigh: - case CodecProfileLevel.AVCProfileHigh: - BR *= 1250; break; - case CodecProfileLevel.AVCProfileHigh10: - BR *= 3000; break; - case CodecProfileLevel.AVCProfileExtended: - case CodecProfileLevel.AVCProfileHigh422: - case CodecProfileLevel.AVCProfileHigh444: - Log.w(TAG, "Unsupported profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNSUPPORTED; - supported = false; - // fall through - treat as base profile - case CodecProfileLevel.AVCProfileConstrainedBaseline: - case CodecProfileLevel.AVCProfileBaseline: - case CodecProfileLevel.AVCProfileMain: - BR *= 1000; break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - BR *= 1000; - } - if (supported) { - errors &= ~ERROR_NONE_SUPPORTED; - } - maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR, maxBps); - maxDPBBlocks = Math.max(maxDPBBlocks, DPB); - } + private void updateLimits() { + // pixels -> blocks <- counts + mHorizontalBlockRange = mHorizontalBlockRange.intersect( + Utils.factorRange(mWidthRange, mBlockWidth)); + mHorizontalBlockRange = mHorizontalBlockRange.intersect( + Range.create( + mBlockCountRange.getLower() / mVerticalBlockRange.getUpper(), + mBlockCountRange.getUpper() / mVerticalBlockRange.getLower())); + mVerticalBlockRange = mVerticalBlockRange.intersect( + Utils.factorRange(mHeightRange, mBlockHeight)); + mVerticalBlockRange = mVerticalBlockRange.intersect( + Range.create( + mBlockCountRange.getLower() / mHorizontalBlockRange.getUpper(), + mBlockCountRange.getUpper() / mHorizontalBlockRange.getLower())); + mBlockCountRange = mBlockCountRange.intersect( + Range.create( + mHorizontalBlockRange.getLower() + * mVerticalBlockRange.getLower(), + mHorizontalBlockRange.getUpper() + * mVerticalBlockRange.getUpper())); + mBlockAspectRatioRange = mBlockAspectRatioRange.intersect( + new Rational( + mHorizontalBlockRange.getLower(), mVerticalBlockRange.getUpper()), + new Rational( + mHorizontalBlockRange.getUpper(), mVerticalBlockRange.getLower())); + + // blocks -> pixels + mWidthRange = mWidthRange.intersect( + (mHorizontalBlockRange.getLower() - 1) * mBlockWidth + mWidthAlignment, + mHorizontalBlockRange.getUpper() * mBlockWidth); + mHeightRange = mHeightRange.intersect( + (mVerticalBlockRange.getLower() - 1) * mBlockHeight + mHeightAlignment, + mVerticalBlockRange.getUpper() * mBlockHeight); + mAspectRatioRange = mAspectRatioRange.intersect( + new Rational(mWidthRange.getLower(), mHeightRange.getUpper()), + new Rational(mWidthRange.getUpper(), mHeightRange.getLower())); + + mSmallerDimensionUpperLimit = Math.min( + mSmallerDimensionUpperLimit, + Math.min(mWidthRange.getUpper(), mHeightRange.getUpper())); + + // blocks -> rate + mBlocksPerSecondRange = mBlocksPerSecondRange.intersect( + mBlockCountRange.getLower() * (long)mFrameRateRange.getLower(), + mBlockCountRange.getUpper() * (long)mFrameRateRange.getUpper()); + mFrameRateRange = mFrameRateRange.intersect( + (int)(mBlocksPerSecondRange.getLower() + / mBlockCountRange.getUpper()), + (int)(mBlocksPerSecondRange.getUpper() + / (double)mBlockCountRange.getLower())); + } - int maxLengthInBlocks = (int)(Math.sqrt(maxBlocks * 8)); + private void applyMacroBlockLimits( + int maxHorizontalBlocks, int maxVerticalBlocks, + int maxBlocks, long maxBlocksPerSecond, + int blockWidth, int blockHeight, + int widthAlignment, int heightAlignment) { applyMacroBlockLimits( - maxLengthInBlocks, maxLengthInBlocks, + 1 /* minHorizontalBlocks */, 1 /* minVerticalBlocks */, + maxHorizontalBlocks, maxVerticalBlocks, maxBlocks, maxBlocksPerSecond, - 16 /* blockWidth */, 16 /* blockHeight */, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG2)) { - int maxWidth = 11, maxHeight = 9, maxRate = 15; - maxBlocks = 99; - maxBlocksPerSecond = 1485; - maxBps = 64000; - for (CodecProfileLevel profileLevel: profileLevels) { - int MBPS = 0, FS = 0, BR = 0, FR = 0, W = 0, H = 0; - boolean supported = true; - switch (profileLevel.profile) { - case CodecProfileLevel.MPEG2ProfileSimple: - switch (profileLevel.level) { - case CodecProfileLevel.MPEG2LevelML: - FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 15000; break; - default: - Log.w(TAG, "Unrecognized profile/level " - + profileLevel.profile + "/" - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - break; - case CodecProfileLevel.MPEG2ProfileMain: - switch (profileLevel.level) { - case CodecProfileLevel.MPEG2LevelLL: - FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 4000; break; - case CodecProfileLevel.MPEG2LevelML: - FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 15000; break; - case CodecProfileLevel.MPEG2LevelH14: - FR = 60; W = 90; H = 68; MBPS = 183600; FS = 6120; BR = 60000; break; - case CodecProfileLevel.MPEG2LevelHL: - FR = 60; W = 120; H = 68; MBPS = 244800; FS = 8160; BR = 80000; break; - case CodecProfileLevel.MPEG2LevelHP: - FR = 60; W = 120; H = 68; MBPS = 489600; FS = 8160; BR = 80000; break; - default: - Log.w(TAG, "Unrecognized profile/level " - + profileLevel.profile + "/" - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - break; - case CodecProfileLevel.MPEG2Profile422: - case CodecProfileLevel.MPEG2ProfileSNR: - case CodecProfileLevel.MPEG2ProfileSpatial: - case CodecProfileLevel.MPEG2ProfileHigh: - Log.i(TAG, "Unsupported profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNSUPPORTED; - supported = false; - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + blockWidth, blockHeight, widthAlignment, heightAlignment); + } + + private void applyMacroBlockLimits( + int minHorizontalBlocks, int minVerticalBlocks, + int maxHorizontalBlocks, int maxVerticalBlocks, + int maxBlocks, long maxBlocksPerSecond, + int blockWidth, int blockHeight, + int widthAlignment, int heightAlignment) { + applyAlignment(widthAlignment, heightAlignment); + applyBlockLimits( + blockWidth, blockHeight, Range.create(1, maxBlocks), + Range.create(1L, maxBlocksPerSecond), + Range.create( + new Rational(1, maxVerticalBlocks), + new Rational(maxHorizontalBlocks, 1))); + mHorizontalBlockRange = + mHorizontalBlockRange.intersect( + Utils.divUp(minHorizontalBlocks, (mBlockWidth / blockWidth)), + maxHorizontalBlocks / (mBlockWidth / blockWidth)); + mVerticalBlockRange = + mVerticalBlockRange.intersect( + Utils.divUp(minVerticalBlocks, (mBlockHeight / blockHeight)), + maxVerticalBlocks / (mBlockHeight / blockHeight)); + } + + private void applyLevelLimits() { + long maxBlocksPerSecond = 0; + int maxBlocks = 0; + int maxBps = 0; + int maxDPBBlocks = 0; + + int errors = ERROR_NONE_SUPPORTED; + CodecProfileLevel[] profileLevels = mParent.getProfileLevels(); + String mime = mParent.getMimeType(); + + if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AVC)) { + maxBlocks = 99; + maxBlocksPerSecond = 1485; + maxBps = 64000; + maxDPBBlocks = 396; + for (CodecProfileLevel profileLevel: profileLevels) { + int MBPS = 0, FS = 0, BR = 0, DPB = 0; + boolean supported = true; + switch (profileLevel.level) { + case CodecProfileLevel.AVCLevel1: + MBPS = 1485; FS = 99; BR = 64; DPB = 396; break; + case CodecProfileLevel.AVCLevel1b: + MBPS = 1485; FS = 99; BR = 128; DPB = 396; break; + case CodecProfileLevel.AVCLevel11: + MBPS = 3000; FS = 396; BR = 192; DPB = 900; break; + case CodecProfileLevel.AVCLevel12: + MBPS = 6000; FS = 396; BR = 384; DPB = 2376; break; + case CodecProfileLevel.AVCLevel13: + MBPS = 11880; FS = 396; BR = 768; DPB = 2376; break; + case CodecProfileLevel.AVCLevel2: + MBPS = 11880; FS = 396; BR = 2000; DPB = 2376; break; + case CodecProfileLevel.AVCLevel21: + MBPS = 19800; FS = 792; BR = 4000; DPB = 4752; break; + case CodecProfileLevel.AVCLevel22: + MBPS = 20250; FS = 1620; BR = 4000; DPB = 8100; break; + case CodecProfileLevel.AVCLevel3: + MBPS = 40500; FS = 1620; BR = 10000; DPB = 8100; break; + case CodecProfileLevel.AVCLevel31: + MBPS = 108000; FS = 3600; BR = 14000; DPB = 18000; break; + case CodecProfileLevel.AVCLevel32: + MBPS = 216000; FS = 5120; BR = 20000; DPB = 20480; break; + case CodecProfileLevel.AVCLevel4: + MBPS = 245760; FS = 8192; BR = 20000; DPB = 32768; break; + case CodecProfileLevel.AVCLevel41: + MBPS = 245760; FS = 8192; BR = 50000; DPB = 32768; break; + case CodecProfileLevel.AVCLevel42: + MBPS = 522240; FS = 8704; BR = 50000; DPB = 34816; break; + case CodecProfileLevel.AVCLevel5: + MBPS = 589824; FS = 22080; BR = 135000; DPB = 110400; break; + case CodecProfileLevel.AVCLevel51: + MBPS = 983040; FS = 36864; BR = 240000; DPB = 184320; break; + case CodecProfileLevel.AVCLevel52: + MBPS = 2073600; FS = 36864; BR = 240000; DPB = 184320; break; + case CodecProfileLevel.AVCLevel6: + MBPS = 4177920; FS = 139264; BR = 240000; DPB = 696320; break; + case CodecProfileLevel.AVCLevel61: + MBPS = 8355840; FS = 139264; BR = 480000; DPB = 696320; break; + case CodecProfileLevel.AVCLevel62: + MBPS = 16711680; FS = 139264; BR = 800000; DPB = 696320; break; + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.AVCProfileConstrainedHigh: + case CodecProfileLevel.AVCProfileHigh: + BR *= 1250; break; + case CodecProfileLevel.AVCProfileHigh10: + BR *= 3000; break; + case CodecProfileLevel.AVCProfileExtended: + case CodecProfileLevel.AVCProfileHigh422: + case CodecProfileLevel.AVCProfileHigh444: + Log.w(TAG, "Unsupported profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNSUPPORTED; + supported = false; + // fall through - treat as base profile + case CodecProfileLevel.AVCProfileConstrainedBaseline: + case CodecProfileLevel.AVCProfileBaseline: + case CodecProfileLevel.AVCProfileMain: + BR *= 1000; break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + BR *= 1000; + } + if (supported) { + errors &= ~ERROR_NONE_SUPPORTED; + } + maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR, maxBps); + maxDPBBlocks = Math.max(maxDPBBlocks, DPB); } - if (supported) { - errors &= ~ERROR_NONE_SUPPORTED; + + int maxLengthInBlocks = (int)(Math.sqrt(maxBlocks * 8)); + applyMacroBlockLimits( + maxLengthInBlocks, maxLengthInBlocks, + maxBlocks, maxBlocksPerSecond, + 16 /* blockWidth */, 16 /* blockHeight */, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG2)) { + int maxWidth = 11, maxHeight = 9, maxRate = 15; + maxBlocks = 99; + maxBlocksPerSecond = 1485; + maxBps = 64000; + for (CodecProfileLevel profileLevel: profileLevels) { + int MBPS = 0, FS = 0, BR = 0, FR = 0, W = 0, H = 0; + boolean supported = true; + switch (profileLevel.profile) { + case CodecProfileLevel.MPEG2ProfileSimple: + switch (profileLevel.level) { + case CodecProfileLevel.MPEG2LevelML: + FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 15000; break; + default: + Log.w(TAG, "Unrecognized profile/level " + + profileLevel.profile + "/" + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + break; + case CodecProfileLevel.MPEG2ProfileMain: + switch (profileLevel.level) { + case CodecProfileLevel.MPEG2LevelLL: + FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 4000; break; + case CodecProfileLevel.MPEG2LevelML: + FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 15000; break; + case CodecProfileLevel.MPEG2LevelH14: + FR = 60; W = 90; H = 68; MBPS = 183600; FS = 6120; BR = 60000; break; + case CodecProfileLevel.MPEG2LevelHL: + FR = 60; W = 120; H = 68; MBPS = 244800; FS = 8160; BR = 80000; break; + case CodecProfileLevel.MPEG2LevelHP: + FR = 60; W = 120; H = 68; MBPS = 489600; FS = 8160; BR = 80000; break; + default: + Log.w(TAG, "Unrecognized profile/level " + + profileLevel.profile + "/" + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + break; + case CodecProfileLevel.MPEG2Profile422: + case CodecProfileLevel.MPEG2ProfileSNR: + case CodecProfileLevel.MPEG2ProfileSpatial: + case CodecProfileLevel.MPEG2ProfileHigh: + Log.i(TAG, "Unsupported profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNSUPPORTED; + supported = false; + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + if (supported) { + errors &= ~ERROR_NONE_SUPPORTED; + } + maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); + maxWidth = Math.max(W, maxWidth); + maxHeight = Math.max(H, maxHeight); + maxRate = Math.max(FR, maxRate); } - maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); - maxWidth = Math.max(W, maxWidth); - maxHeight = Math.max(H, maxHeight); - maxRate = Math.max(FR, maxRate); - } - applyMacroBlockLimits(maxWidth, maxHeight, - maxBlocks, maxBlocksPerSecond, - 16 /* blockWidth */, 16 /* blockHeight */, - 1 /* widthAlignment */, 1 /* heightAlignment */); - mFrameRateRange = mFrameRateRange.intersect(12, maxRate); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG4)) { - int maxWidth = 11, maxHeight = 9, maxRate = 15; - maxBlocks = 99; - maxBlocksPerSecond = 1485; - maxBps = 64000; - for (CodecProfileLevel profileLevel: profileLevels) { - int MBPS = 0, FS = 0, BR = 0, FR = 0, W = 0, H = 0; - boolean strict = false; // true: W, H and FR are individual max limits - boolean supported = true; - switch (profileLevel.profile) { - case CodecProfileLevel.MPEG4ProfileSimple: - switch (profileLevel.level) { - case CodecProfileLevel.MPEG4Level0: - strict = true; - FR = 15; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 64; break; - case CodecProfileLevel.MPEG4Level1: - FR = 30; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 64; break; - case CodecProfileLevel.MPEG4Level0b: - strict = true; - FR = 15; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 128; break; - case CodecProfileLevel.MPEG4Level2: - FR = 30; W = 22; H = 18; MBPS = 5940; FS = 396; BR = 128; break; - case CodecProfileLevel.MPEG4Level3: - FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 384; break; - case CodecProfileLevel.MPEG4Level4a: - FR = 30; W = 40; H = 30; MBPS = 36000; FS = 1200; BR = 4000; break; - case CodecProfileLevel.MPEG4Level5: - FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 8000; break; - case CodecProfileLevel.MPEG4Level6: - FR = 30; W = 80; H = 45; MBPS = 108000; FS = 3600; BR = 12000; break; - default: - Log.w(TAG, "Unrecognized profile/level " - + profileLevel.profile + "/" - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - break; - case CodecProfileLevel.MPEG4ProfileAdvancedSimple: - switch (profileLevel.level) { - case CodecProfileLevel.MPEG4Level0: - case CodecProfileLevel.MPEG4Level1: - FR = 30; W = 11; H = 9; MBPS = 2970; FS = 99; BR = 128; break; - case CodecProfileLevel.MPEG4Level2: - FR = 30; W = 22; H = 18; MBPS = 5940; FS = 396; BR = 384; break; - case CodecProfileLevel.MPEG4Level3: - FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 768; break; - case CodecProfileLevel.MPEG4Level3b: - FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 1500; break; - case CodecProfileLevel.MPEG4Level4: - FR = 30; W = 44; H = 36; MBPS = 23760; FS = 792; BR = 3000; break; - case CodecProfileLevel.MPEG4Level5: - FR = 30; W = 45; H = 36; MBPS = 48600; FS = 1620; BR = 8000; break; - default: - Log.w(TAG, "Unrecognized profile/level " - + profileLevel.profile + "/" - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - break; - case CodecProfileLevel.MPEG4ProfileMain: // 2-4 - case CodecProfileLevel.MPEG4ProfileNbit: // 2 - case CodecProfileLevel.MPEG4ProfileAdvancedRealTime: // 1-4 - case CodecProfileLevel.MPEG4ProfileCoreScalable: // 1-3 - case CodecProfileLevel.MPEG4ProfileAdvancedCoding: // 1-4 - case CodecProfileLevel.MPEG4ProfileCore: // 1-2 - case CodecProfileLevel.MPEG4ProfileAdvancedCore: // 1-4 - case CodecProfileLevel.MPEG4ProfileSimpleScalable: // 0-2 - case CodecProfileLevel.MPEG4ProfileHybrid: // 1-2 - - // Studio profiles are not supported by our codecs. - - // Only profiles that can decode simple object types are considered. - // The following profiles are not able to. - case CodecProfileLevel.MPEG4ProfileBasicAnimated: // 1-2 - case CodecProfileLevel.MPEG4ProfileScalableTexture: // 1 - case CodecProfileLevel.MPEG4ProfileSimpleFace: // 1-2 - case CodecProfileLevel.MPEG4ProfileAdvancedScalable: // 1-3 - case CodecProfileLevel.MPEG4ProfileSimpleFBA: // 1-2 - Log.i(TAG, "Unsupported profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNSUPPORTED; - supported = false; - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + applyMacroBlockLimits(maxWidth, maxHeight, + maxBlocks, maxBlocksPerSecond, + 16 /* blockWidth */, 16 /* blockHeight */, + 1 /* widthAlignment */, 1 /* heightAlignment */); + mFrameRateRange = mFrameRateRange.intersect(12, maxRate); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG4)) { + int maxWidth = 11, maxHeight = 9, maxRate = 15; + maxBlocks = 99; + maxBlocksPerSecond = 1485; + maxBps = 64000; + for (CodecProfileLevel profileLevel: profileLevels) { + int MBPS = 0, FS = 0, BR = 0, FR = 0, W = 0, H = 0; + boolean strict = false; // true: W, H and FR are individual max limits + boolean supported = true; + switch (profileLevel.profile) { + case CodecProfileLevel.MPEG4ProfileSimple: + switch (profileLevel.level) { + case CodecProfileLevel.MPEG4Level0: + strict = true; + FR = 15; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 64; break; + case CodecProfileLevel.MPEG4Level1: + FR = 30; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 64; break; + case CodecProfileLevel.MPEG4Level0b: + strict = true; + FR = 15; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 128; break; + case CodecProfileLevel.MPEG4Level2: + FR = 30; W = 22; H = 18; MBPS = 5940; FS = 396; BR = 128; break; + case CodecProfileLevel.MPEG4Level3: + FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 384; break; + case CodecProfileLevel.MPEG4Level4a: + FR = 30; W = 40; H = 30; MBPS = 36000; FS = 1200; BR = 4000; break; + case CodecProfileLevel.MPEG4Level5: + FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 8000; break; + case CodecProfileLevel.MPEG4Level6: + FR = 30; W = 80; H = 45; MBPS = 108000; FS = 3600; BR = 12000; break; + default: + Log.w(TAG, "Unrecognized profile/level " + + profileLevel.profile + "/" + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + break; + case CodecProfileLevel.MPEG4ProfileAdvancedSimple: + switch (profileLevel.level) { + case CodecProfileLevel.MPEG4Level0: + case CodecProfileLevel.MPEG4Level1: + FR = 30; W = 11; H = 9; MBPS = 2970; FS = 99; BR = 128; break; + case CodecProfileLevel.MPEG4Level2: + FR = 30; W = 22; H = 18; MBPS = 5940; FS = 396; BR = 384; break; + case CodecProfileLevel.MPEG4Level3: + FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 768; break; + case CodecProfileLevel.MPEG4Level3b: + FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 1500; break; + case CodecProfileLevel.MPEG4Level4: + FR = 30; W = 44; H = 36; MBPS = 23760; FS = 792; BR = 3000; break; + case CodecProfileLevel.MPEG4Level5: + FR = 30; W = 45; H = 36; MBPS = 48600; FS = 1620; BR = 8000; break; + default: + Log.w(TAG, "Unrecognized profile/level " + + profileLevel.profile + "/" + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + break; + case CodecProfileLevel.MPEG4ProfileMain: // 2-4 + case CodecProfileLevel.MPEG4ProfileNbit: // 2 + case CodecProfileLevel.MPEG4ProfileAdvancedRealTime: // 1-4 + case CodecProfileLevel.MPEG4ProfileCoreScalable: // 1-3 + case CodecProfileLevel.MPEG4ProfileAdvancedCoding: // 1-4 + case CodecProfileLevel.MPEG4ProfileCore: // 1-2 + case CodecProfileLevel.MPEG4ProfileAdvancedCore: // 1-4 + case CodecProfileLevel.MPEG4ProfileSimpleScalable: // 0-2 + case CodecProfileLevel.MPEG4ProfileHybrid: // 1-2 + + // Studio profiles are not supported by our codecs. + + // Only profiles that can decode simple object types are considered. + // The following profiles are not able to. + case CodecProfileLevel.MPEG4ProfileBasicAnimated: // 1-2 + case CodecProfileLevel.MPEG4ProfileScalableTexture: // 1 + case CodecProfileLevel.MPEG4ProfileSimpleFace: // 1-2 + case CodecProfileLevel.MPEG4ProfileAdvancedScalable: // 1-3 + case CodecProfileLevel.MPEG4ProfileSimpleFBA: // 1-2 + Log.i(TAG, "Unsupported profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNSUPPORTED; + supported = false; + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + if (supported) { + errors &= ~ERROR_NONE_SUPPORTED; + } + maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); + if (strict) { + maxWidth = Math.max(W, maxWidth); + maxHeight = Math.max(H, maxHeight); + maxRate = Math.max(FR, maxRate); + } else { + // assuming max 60 fps frame rate and 1:2 aspect ratio + int maxDim = (int)Math.sqrt(FS * 2); + maxWidth = Math.max(maxDim, maxWidth); + maxHeight = Math.max(maxDim, maxHeight); + maxRate = Math.max(Math.max(FR, 60), maxRate); + } } - if (supported) { + applyMacroBlockLimits(maxWidth, maxHeight, + maxBlocks, maxBlocksPerSecond, + 16 /* blockWidth */, 16 /* blockHeight */, + 1 /* widthAlignment */, 1 /* heightAlignment */); + mFrameRateRange = mFrameRateRange.intersect(12, maxRate); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263)) { + int maxWidth = 11, maxHeight = 9, maxRate = 15; + int minWidth = maxWidth, minHeight = maxHeight; + int minAlignment = 16; + maxBlocks = 99; + maxBlocksPerSecond = 1485; + maxBps = 64000; + for (CodecProfileLevel profileLevel: profileLevels) { + int MBPS = 0, BR = 0, FR = 0, W = 0, H = 0, minW = minWidth, minH = minHeight; + boolean strict = false; // true: support only sQCIF, QCIF (maybe CIF) + switch (profileLevel.level) { + case CodecProfileLevel.H263Level10: + strict = true; // only supports sQCIF & QCIF + FR = 15; W = 11; H = 9; BR = 1; MBPS = W * H * FR; break; + case CodecProfileLevel.H263Level20: + strict = true; // only supports sQCIF, QCIF & CIF + FR = 30; W = 22; H = 18; BR = 2; MBPS = W * H * 15; break; + case CodecProfileLevel.H263Level30: + strict = true; // only supports sQCIF, QCIF & CIF + FR = 30; W = 22; H = 18; BR = 6; MBPS = W * H * FR; break; + case CodecProfileLevel.H263Level40: + strict = true; // only supports sQCIF, QCIF & CIF + FR = 30; W = 22; H = 18; BR = 32; MBPS = W * H * FR; break; + case CodecProfileLevel.H263Level45: + // only implies level 10 support + strict = profileLevel.profile == CodecProfileLevel.H263ProfileBaseline + || profileLevel.profile == + CodecProfileLevel.H263ProfileBackwardCompatible; + if (!strict) { + minW = 1; minH = 1; minAlignment = 4; + } + FR = 15; W = 11; H = 9; BR = 2; MBPS = W * H * FR; break; + case CodecProfileLevel.H263Level50: + // only supports 50fps for H > 15 + minW = 1; minH = 1; minAlignment = 4; + FR = 60; W = 22; H = 18; BR = 64; MBPS = W * H * 50; break; + case CodecProfileLevel.H263Level60: + // only supports 50fps for H > 15 + minW = 1; minH = 1; minAlignment = 4; + FR = 60; W = 45; H = 18; BR = 128; MBPS = W * H * 50; break; + case CodecProfileLevel.H263Level70: + // only supports 50fps for H > 30 + minW = 1; minH = 1; minAlignment = 4; + FR = 60; W = 45; H = 36; BR = 256; MBPS = W * H * 50; break; + default: + Log.w(TAG, "Unrecognized profile/level " + profileLevel.profile + + "/" + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.H263ProfileBackwardCompatible: + case CodecProfileLevel.H263ProfileBaseline: + case CodecProfileLevel.H263ProfileH320Coding: + case CodecProfileLevel.H263ProfileHighCompression: + case CodecProfileLevel.H263ProfileHighLatency: + case CodecProfileLevel.H263ProfileInterlace: + case CodecProfileLevel.H263ProfileInternet: + case CodecProfileLevel.H263ProfileISWV2: + case CodecProfileLevel.H263ProfileISWV3: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + if (strict) { + // Strict levels define sub-QCIF min size and enumerated sizes. We + // cannot express support for "only sQCIF & QCIF (& CIF)" using + // VideoCapabilities but we can express "only QCIF (& CIF)", so set + // minimume size at QCIF.minW = 8; minH = 6; + minW = 11; minH = 9; + } else { + // any support for non-strict levels (including unrecognized profiles or + // levels) allow custom frame size support beyond supported limits + // (other than bitrate) + mAllowMbOverride = true; + } errors &= ~ERROR_NONE_SUPPORTED; - } - maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); - if (strict) { + maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); + maxBlocks = Math.max(W * H, maxBlocks); + maxBps = Math.max(BR * 64000, maxBps); maxWidth = Math.max(W, maxWidth); maxHeight = Math.max(H, maxHeight); maxRate = Math.max(FR, maxRate); - } else { - // assuming max 60 fps frame rate and 1:2 aspect ratio - int maxDim = (int)Math.sqrt(FS * 2); - maxWidth = Math.max(maxDim, maxWidth); - maxHeight = Math.max(maxDim, maxHeight); - maxRate = Math.max(Math.max(FR, 60), maxRate); - } - } - applyMacroBlockLimits(maxWidth, maxHeight, - maxBlocks, maxBlocksPerSecond, - 16 /* blockWidth */, 16 /* blockHeight */, - 1 /* widthAlignment */, 1 /* heightAlignment */); - mFrameRateRange = mFrameRateRange.intersect(12, maxRate); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263)) { - int maxWidth = 11, maxHeight = 9, maxRate = 15; - int minWidth = maxWidth, minHeight = maxHeight; - int minAlignment = 16; - maxBlocks = 99; - maxBlocksPerSecond = 1485; - maxBps = 64000; - for (CodecProfileLevel profileLevel: profileLevels) { - int MBPS = 0, BR = 0, FR = 0, W = 0, H = 0, minW = minWidth, minH = minHeight; - boolean strict = false; // true: support only sQCIF, QCIF (maybe CIF) - switch (profileLevel.level) { - case CodecProfileLevel.H263Level10: - strict = true; // only supports sQCIF & QCIF - FR = 15; W = 11; H = 9; BR = 1; MBPS = W * H * FR; break; - case CodecProfileLevel.H263Level20: - strict = true; // only supports sQCIF, QCIF & CIF - FR = 30; W = 22; H = 18; BR = 2; MBPS = W * H * 15; break; - case CodecProfileLevel.H263Level30: - strict = true; // only supports sQCIF, QCIF & CIF - FR = 30; W = 22; H = 18; BR = 6; MBPS = W * H * FR; break; - case CodecProfileLevel.H263Level40: - strict = true; // only supports sQCIF, QCIF & CIF - FR = 30; W = 22; H = 18; BR = 32; MBPS = W * H * FR; break; - case CodecProfileLevel.H263Level45: - // only implies level 10 support - strict = profileLevel.profile == CodecProfileLevel.H263ProfileBaseline - || profileLevel.profile == - CodecProfileLevel.H263ProfileBackwardCompatible; - if (!strict) { - minW = 1; minH = 1; minAlignment = 4; - } - FR = 15; W = 11; H = 9; BR = 2; MBPS = W * H * FR; break; - case CodecProfileLevel.H263Level50: - // only supports 50fps for H > 15 - minW = 1; minH = 1; minAlignment = 4; - FR = 60; W = 22; H = 18; BR = 64; MBPS = W * H * 50; break; - case CodecProfileLevel.H263Level60: - // only supports 50fps for H > 15 - minW = 1; minH = 1; minAlignment = 4; - FR = 60; W = 45; H = 18; BR = 128; MBPS = W * H * 50; break; - case CodecProfileLevel.H263Level70: - // only supports 50fps for H > 30 - minW = 1; minH = 1; minAlignment = 4; - FR = 60; W = 45; H = 36; BR = 256; MBPS = W * H * 50; break; - default: - Log.w(TAG, "Unrecognized profile/level " + profileLevel.profile - + "/" + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - switch (profileLevel.profile) { - case CodecProfileLevel.H263ProfileBackwardCompatible: - case CodecProfileLevel.H263ProfileBaseline: - case CodecProfileLevel.H263ProfileH320Coding: - case CodecProfileLevel.H263ProfileHighCompression: - case CodecProfileLevel.H263ProfileHighLatency: - case CodecProfileLevel.H263ProfileInterlace: - case CodecProfileLevel.H263ProfileInternet: - case CodecProfileLevel.H263ProfileISWV2: - case CodecProfileLevel.H263ProfileISWV3: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - if (strict) { - // Strict levels define sub-QCIF min size and enumerated sizes. We cannot - // express support for "only sQCIF & QCIF (& CIF)" using VideoCapabilities - // but we can express "only QCIF (& CIF)", so set minimume size at QCIF. - // minW = 8; minH = 6; - minW = 11; minH = 9; - } else { - // any support for non-strict levels (including unrecognized profiles or - // levels) allow custom frame size support beyond supported limits - // (other than bitrate) - mAllowMbOverride = true; + minWidth = Math.min(minW, minWidth); + minHeight = Math.min(minH, minHeight); } - errors &= ~ERROR_NONE_SUPPORTED; - maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); - maxBlocks = Math.max(W * H, maxBlocks); - maxBps = Math.max(BR * 64000, maxBps); - maxWidth = Math.max(W, maxWidth); - maxHeight = Math.max(H, maxHeight); - maxRate = Math.max(FR, maxRate); - minWidth = Math.min(minW, minWidth); - minHeight = Math.min(minH, minHeight); - } - // unless we encountered custom frame size support, limit size to QCIF and CIF - // using aspect ratio. - if (!mAllowMbOverride) { - mBlockAspectRatioRange = - Range.create(new Rational(11, 9), new Rational(11, 9)); - } - applyMacroBlockLimits( - minWidth, minHeight, - maxWidth, maxHeight, - maxBlocks, maxBlocksPerSecond, - 16 /* blockWidth */, 16 /* blockHeight */, - minAlignment /* widthAlignment */, minAlignment /* heightAlignment */); - mFrameRateRange = Range.create(1, maxRate); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP8)) { - maxBlocks = Integer.MAX_VALUE; - maxBlocksPerSecond = Integer.MAX_VALUE; - - // TODO: set to 100Mbps for now, need a number for VP8 - maxBps = 100000000; - - // profile levels are not indicative for VPx, but verify - // them nonetheless - for (CodecProfileLevel profileLevel: profileLevels) { - switch (profileLevel.level) { - case CodecProfileLevel.VP8Level_Version0: - case CodecProfileLevel.VP8Level_Version1: - case CodecProfileLevel.VP8Level_Version2: - case CodecProfileLevel.VP8Level_Version3: - break; - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + // unless we encountered custom frame size support, limit size to QCIF and CIF + // using aspect ratio. + if (!mAllowMbOverride) { + mBlockAspectRatioRange = + Range.create(new Rational(11, 9), new Rational(11, 9)); } - switch (profileLevel.profile) { - case CodecProfileLevel.VP8ProfileMain: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + applyMacroBlockLimits( + minWidth, minHeight, + maxWidth, maxHeight, + maxBlocks, maxBlocksPerSecond, + 16 /* blockWidth */, 16 /* blockHeight */, + minAlignment /* widthAlignment */, minAlignment /* heightAlignment */); + mFrameRateRange = Range.create(1, maxRate); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP8)) { + maxBlocks = Integer.MAX_VALUE; + maxBlocksPerSecond = Integer.MAX_VALUE; + + // TODO: set to 100Mbps for now, need a number for VP8 + maxBps = 100000000; + + // profile levels are not indicative for VPx, but verify + // them nonetheless + for (CodecProfileLevel profileLevel: profileLevels) { + switch (profileLevel.level) { + case CodecProfileLevel.VP8Level_Version0: + case CodecProfileLevel.VP8Level_Version1: + case CodecProfileLevel.VP8Level_Version2: + case CodecProfileLevel.VP8Level_Version3: + break; + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.VP8ProfileMain: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + errors &= ~ERROR_NONE_SUPPORTED; } - errors &= ~ERROR_NONE_SUPPORTED; - } - final int blockSize = 16; - applyMacroBlockLimits(Short.MAX_VALUE, Short.MAX_VALUE, - maxBlocks, maxBlocksPerSecond, blockSize, blockSize, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP9)) { - maxBlocksPerSecond = 829440; - maxBlocks = 36864; - maxBps = 200000; - int maxDim = 512; - - for (CodecProfileLevel profileLevel: profileLevels) { - long SR = 0; // luma sample rate - int FS = 0; // luma picture size - int BR = 0; // bit rate kbps - int D = 0; // luma dimension - switch (profileLevel.level) { - case CodecProfileLevel.VP9Level1: - SR = 829440; FS = 36864; BR = 200; D = 512; break; - case CodecProfileLevel.VP9Level11: - SR = 2764800; FS = 73728; BR = 800; D = 768; break; - case CodecProfileLevel.VP9Level2: - SR = 4608000; FS = 122880; BR = 1800; D = 960; break; - case CodecProfileLevel.VP9Level21: - SR = 9216000; FS = 245760; BR = 3600; D = 1344; break; - case CodecProfileLevel.VP9Level3: - SR = 20736000; FS = 552960; BR = 7200; D = 2048; break; - case CodecProfileLevel.VP9Level31: - SR = 36864000; FS = 983040; BR = 12000; D = 2752; break; - case CodecProfileLevel.VP9Level4: - SR = 83558400; FS = 2228224; BR = 18000; D = 4160; break; - case CodecProfileLevel.VP9Level41: - SR = 160432128; FS = 2228224; BR = 30000; D = 4160; break; - case CodecProfileLevel.VP9Level5: - SR = 311951360; FS = 8912896; BR = 60000; D = 8384; break; - case CodecProfileLevel.VP9Level51: - SR = 588251136; FS = 8912896; BR = 120000; D = 8384; break; - case CodecProfileLevel.VP9Level52: - SR = 1176502272; FS = 8912896; BR = 180000; D = 8384; break; - case CodecProfileLevel.VP9Level6: - SR = 1176502272; FS = 35651584; BR = 180000; D = 16832; break; - case CodecProfileLevel.VP9Level61: - SR = 2353004544L; FS = 35651584; BR = 240000; D = 16832; break; - case CodecProfileLevel.VP9Level62: - SR = 4706009088L; FS = 35651584; BR = 480000; D = 16832; break; - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - switch (profileLevel.profile) { - case CodecProfileLevel.VP9Profile0: - case CodecProfileLevel.VP9Profile1: - case CodecProfileLevel.VP9Profile2: - case CodecProfileLevel.VP9Profile3: - case CodecProfileLevel.VP9Profile2HDR: - case CodecProfileLevel.VP9Profile3HDR: - case CodecProfileLevel.VP9Profile2HDR10Plus: - case CodecProfileLevel.VP9Profile3HDR10Plus: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + final int blockSize = 16; + applyMacroBlockLimits(Short.MAX_VALUE, Short.MAX_VALUE, + maxBlocks, maxBlocksPerSecond, blockSize, blockSize, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP9)) { + maxBlocksPerSecond = 829440; + maxBlocks = 36864; + maxBps = 200000; + int maxDim = 512; + + for (CodecProfileLevel profileLevel: profileLevels) { + long SR = 0; // luma sample rate + int FS = 0; // luma picture size + int BR = 0; // bit rate kbps + int D = 0; // luma dimension + switch (profileLevel.level) { + case CodecProfileLevel.VP9Level1: + SR = 829440; FS = 36864; BR = 200; D = 512; break; + case CodecProfileLevel.VP9Level11: + SR = 2764800; FS = 73728; BR = 800; D = 768; break; + case CodecProfileLevel.VP9Level2: + SR = 4608000; FS = 122880; BR = 1800; D = 960; break; + case CodecProfileLevel.VP9Level21: + SR = 9216000; FS = 245760; BR = 3600; D = 1344; break; + case CodecProfileLevel.VP9Level3: + SR = 20736000; FS = 552960; BR = 7200; D = 2048; break; + case CodecProfileLevel.VP9Level31: + SR = 36864000; FS = 983040; BR = 12000; D = 2752; break; + case CodecProfileLevel.VP9Level4: + SR = 83558400; FS = 2228224; BR = 18000; D = 4160; break; + case CodecProfileLevel.VP9Level41: + SR = 160432128; FS = 2228224; BR = 30000; D = 4160; break; + case CodecProfileLevel.VP9Level5: + SR = 311951360; FS = 8912896; BR = 60000; D = 8384; break; + case CodecProfileLevel.VP9Level51: + SR = 588251136; FS = 8912896; BR = 120000; D = 8384; break; + case CodecProfileLevel.VP9Level52: + SR = 1176502272; FS = 8912896; BR = 180000; D = 8384; break; + case CodecProfileLevel.VP9Level6: + SR = 1176502272; FS = 35651584; BR = 180000; D = 16832; break; + case CodecProfileLevel.VP9Level61: + SR = 2353004544L; FS = 35651584; BR = 240000; D = 16832; break; + case CodecProfileLevel.VP9Level62: + SR = 4706009088L; FS = 35651584; BR = 480000; D = 16832; break; + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.VP9Profile0: + case CodecProfileLevel.VP9Profile1: + case CodecProfileLevel.VP9Profile2: + case CodecProfileLevel.VP9Profile3: + case CodecProfileLevel.VP9Profile2HDR: + case CodecProfileLevel.VP9Profile3HDR: + case CodecProfileLevel.VP9Profile2HDR10Plus: + case CodecProfileLevel.VP9Profile3HDR10Plus: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + errors &= ~ERROR_NONE_SUPPORTED; + maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); + maxDim = Math.max(D, maxDim); } - errors &= ~ERROR_NONE_SUPPORTED; - maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); - maxDim = Math.max(D, maxDim); - } - final int blockSize = 8; - int maxLengthInBlocks = Utils.divUp(maxDim, blockSize); - maxBlocks = Utils.divUp(maxBlocks, blockSize * blockSize); - maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize); + final int blockSize = 8; + int maxLengthInBlocks = Utils.divUp(maxDim, blockSize); + maxBlocks = Utils.divUp(maxBlocks, blockSize * blockSize); + maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize); + + applyMacroBlockLimits( + maxLengthInBlocks, maxLengthInBlocks, + maxBlocks, maxBlocksPerSecond, + blockSize, blockSize, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { + // CTBs are at least 8x8 so use 8x8 block size + maxBlocks = 36864 >> 6; // 192x192 pixels == 576 8x8 blocks + maxBlocksPerSecond = maxBlocks * 15; + maxBps = 128000; + for (CodecProfileLevel profileLevel: profileLevels) { + double FR = 0; + int FS = 0; + int BR = 0; + switch (profileLevel.level) { + /* The HEVC spec talks only in a very convoluted manner about the + existence of levels 1-3.1 for High tier, which could also be + understood as 'decoders and encoders should treat these levels + as if they were Main tier', so we do that. */ + case CodecProfileLevel.HEVCMainTierLevel1: + case CodecProfileLevel.HEVCHighTierLevel1: + FR = 15; FS = 36864; BR = 128; break; + case CodecProfileLevel.HEVCMainTierLevel2: + case CodecProfileLevel.HEVCHighTierLevel2: + FR = 30; FS = 122880; BR = 1500; break; + case CodecProfileLevel.HEVCMainTierLevel21: + case CodecProfileLevel.HEVCHighTierLevel21: + FR = 30; FS = 245760; BR = 3000; break; + case CodecProfileLevel.HEVCMainTierLevel3: + case CodecProfileLevel.HEVCHighTierLevel3: + FR = 30; FS = 552960; BR = 6000; break; + case CodecProfileLevel.HEVCMainTierLevel31: + case CodecProfileLevel.HEVCHighTierLevel31: + FR = 33.75; FS = 983040; BR = 10000; break; + case CodecProfileLevel.HEVCMainTierLevel4: + FR = 30; FS = 2228224; BR = 12000; break; + case CodecProfileLevel.HEVCHighTierLevel4: + FR = 30; FS = 2228224; BR = 30000; break; + case CodecProfileLevel.HEVCMainTierLevel41: + FR = 60; FS = 2228224; BR = 20000; break; + case CodecProfileLevel.HEVCHighTierLevel41: + FR = 60; FS = 2228224; BR = 50000; break; + case CodecProfileLevel.HEVCMainTierLevel5: + FR = 30; FS = 8912896; BR = 25000; break; + case CodecProfileLevel.HEVCHighTierLevel5: + FR = 30; FS = 8912896; BR = 100000; break; + case CodecProfileLevel.HEVCMainTierLevel51: + FR = 60; FS = 8912896; BR = 40000; break; + case CodecProfileLevel.HEVCHighTierLevel51: + FR = 60; FS = 8912896; BR = 160000; break; + case CodecProfileLevel.HEVCMainTierLevel52: + FR = 120; FS = 8912896; BR = 60000; break; + case CodecProfileLevel.HEVCHighTierLevel52: + FR = 120; FS = 8912896; BR = 240000; break; + case CodecProfileLevel.HEVCMainTierLevel6: + FR = 30; FS = 35651584; BR = 60000; break; + case CodecProfileLevel.HEVCHighTierLevel6: + FR = 30; FS = 35651584; BR = 240000; break; + case CodecProfileLevel.HEVCMainTierLevel61: + FR = 60; FS = 35651584; BR = 120000; break; + case CodecProfileLevel.HEVCHighTierLevel61: + FR = 60; FS = 35651584; BR = 480000; break; + case CodecProfileLevel.HEVCMainTierLevel62: + FR = 120; FS = 35651584; BR = 240000; break; + case CodecProfileLevel.HEVCHighTierLevel62: + FR = 120; FS = 35651584; BR = 800000; break; + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.HEVCProfileMain: + case CodecProfileLevel.HEVCProfileMain10: + case CodecProfileLevel.HEVCProfileMainStill: + case CodecProfileLevel.HEVCProfileMain10HDR10: + case CodecProfileLevel.HEVCProfileMain10HDR10Plus: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } - applyMacroBlockLimits( - maxLengthInBlocks, maxLengthInBlocks, - maxBlocks, maxBlocksPerSecond, - blockSize, blockSize, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { - // CTBs are at least 8x8 so use 8x8 block size - maxBlocks = 36864 >> 6; // 192x192 pixels == 576 8x8 blocks - maxBlocksPerSecond = maxBlocks * 15; - maxBps = 128000; - for (CodecProfileLevel profileLevel: profileLevels) { - double FR = 0; - int FS = 0; - int BR = 0; - switch (profileLevel.level) { - /* The HEVC spec talks only in a very convoluted manner about the - existence of levels 1-3.1 for High tier, which could also be - understood as 'decoders and encoders should treat these levels - as if they were Main tier', so we do that. */ - case CodecProfileLevel.HEVCMainTierLevel1: - case CodecProfileLevel.HEVCHighTierLevel1: - FR = 15; FS = 36864; BR = 128; break; - case CodecProfileLevel.HEVCMainTierLevel2: - case CodecProfileLevel.HEVCHighTierLevel2: - FR = 30; FS = 122880; BR = 1500; break; - case CodecProfileLevel.HEVCMainTierLevel21: - case CodecProfileLevel.HEVCHighTierLevel21: - FR = 30; FS = 245760; BR = 3000; break; - case CodecProfileLevel.HEVCMainTierLevel3: - case CodecProfileLevel.HEVCHighTierLevel3: - FR = 30; FS = 552960; BR = 6000; break; - case CodecProfileLevel.HEVCMainTierLevel31: - case CodecProfileLevel.HEVCHighTierLevel31: - FR = 33.75; FS = 983040; BR = 10000; break; - case CodecProfileLevel.HEVCMainTierLevel4: - FR = 30; FS = 2228224; BR = 12000; break; - case CodecProfileLevel.HEVCHighTierLevel4: - FR = 30; FS = 2228224; BR = 30000; break; - case CodecProfileLevel.HEVCMainTierLevel41: - FR = 60; FS = 2228224; BR = 20000; break; - case CodecProfileLevel.HEVCHighTierLevel41: - FR = 60; FS = 2228224; BR = 50000; break; - case CodecProfileLevel.HEVCMainTierLevel5: - FR = 30; FS = 8912896; BR = 25000; break; - case CodecProfileLevel.HEVCHighTierLevel5: - FR = 30; FS = 8912896; BR = 100000; break; - case CodecProfileLevel.HEVCMainTierLevel51: - FR = 60; FS = 8912896; BR = 40000; break; - case CodecProfileLevel.HEVCHighTierLevel51: - FR = 60; FS = 8912896; BR = 160000; break; - case CodecProfileLevel.HEVCMainTierLevel52: - FR = 120; FS = 8912896; BR = 60000; break; - case CodecProfileLevel.HEVCHighTierLevel52: - FR = 120; FS = 8912896; BR = 240000; break; - case CodecProfileLevel.HEVCMainTierLevel6: - FR = 30; FS = 35651584; BR = 60000; break; - case CodecProfileLevel.HEVCHighTierLevel6: - FR = 30; FS = 35651584; BR = 240000; break; - case CodecProfileLevel.HEVCMainTierLevel61: - FR = 60; FS = 35651584; BR = 120000; break; - case CodecProfileLevel.HEVCHighTierLevel61: - FR = 60; FS = 35651584; BR = 480000; break; - case CodecProfileLevel.HEVCMainTierLevel62: - FR = 120; FS = 35651584; BR = 240000; break; - case CodecProfileLevel.HEVCHighTierLevel62: - FR = 120; FS = 35651584; BR = 800000; break; - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + /* DPB logic: + if (width * height <= FS / 4) DPB = 16; + else if (width * height <= FS / 2) DPB = 12; + else if (width * height <= FS * 0.75) DPB = 8; + else DPB = 6; + */ + + FS >>= 6; // convert pixels to blocks + errors &= ~ERROR_NONE_SUPPORTED; + maxBlocksPerSecond = Math.max((int)(FR * FS), maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); } - switch (profileLevel.profile) { - case CodecProfileLevel.HEVCProfileMain: - case CodecProfileLevel.HEVCProfileMain10: - case CodecProfileLevel.HEVCProfileMainStill: - case CodecProfileLevel.HEVCProfileMain10HDR10: - case CodecProfileLevel.HEVCProfileMain10HDR10Plus: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + + int maxLengthInBlocks = (int)(Math.sqrt(maxBlocks * 8)); + applyMacroBlockLimits( + maxLengthInBlocks, maxLengthInBlocks, + maxBlocks, maxBlocksPerSecond, + 8 /* blockWidth */, 8 /* blockHeight */, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AV1)) { + maxBlocksPerSecond = 829440; + maxBlocks = 36864; + maxBps = 200000; + int maxDim = 512; + + // Sample rate, Picture Size, Bit rate and luma dimension for AV1 Codec, + // corresponding to the definitions in + // "AV1 Bitstream & Decoding Process Specification", Annex A + // found at https://aomedia.org/av1-bitstream-and-decoding-process-specification/ + for (CodecProfileLevel profileLevel: profileLevels) { + long SR = 0; // luma sample rate + int FS = 0; // luma picture size + int BR = 0; // bit rate kbps + int D = 0; // luma D + switch (profileLevel.level) { + case CodecProfileLevel.AV1Level2: + SR = 5529600; FS = 147456; BR = 1500; D = 2048; break; + case CodecProfileLevel.AV1Level21: + case CodecProfileLevel.AV1Level22: + case CodecProfileLevel.AV1Level23: + SR = 10454400; FS = 278784; BR = 3000; D = 2816; break; + + case CodecProfileLevel.AV1Level3: + SR = 24969600; FS = 665856; BR = 6000; D = 4352; break; + case CodecProfileLevel.AV1Level31: + case CodecProfileLevel.AV1Level32: + case CodecProfileLevel.AV1Level33: + SR = 39938400; FS = 1065024; BR = 10000; D = 5504; break; + + case CodecProfileLevel.AV1Level4: + SR = 77856768; FS = 2359296; BR = 12000; D = 6144; break; + case CodecProfileLevel.AV1Level41: + case CodecProfileLevel.AV1Level42: + case CodecProfileLevel.AV1Level43: + SR = 155713536; FS = 2359296; BR = 20000; D = 6144; break; + + case CodecProfileLevel.AV1Level5: + SR = 273715200; FS = 8912896; BR = 30000; D = 8192; break; + case CodecProfileLevel.AV1Level51: + SR = 547430400; FS = 8912896; BR = 40000; D = 8192; break; + case CodecProfileLevel.AV1Level52: + SR = 1094860800; FS = 8912896; BR = 60000; D = 8192; break; + case CodecProfileLevel.AV1Level53: + SR = 1176502272; FS = 8912896; BR = 60000; D = 8192; break; + + case CodecProfileLevel.AV1Level6: + SR = 1176502272; FS = 35651584; BR = 60000; D = 16384; break; + case CodecProfileLevel.AV1Level61: + SR = 2189721600L; FS = 35651584; BR = 100000; D = 16384; break; + case CodecProfileLevel.AV1Level62: + SR = 4379443200L; FS = 35651584; BR = 160000; D = 16384; break; + case CodecProfileLevel.AV1Level63: + SR = 4706009088L; FS = 35651584; BR = 160000; D = 16384; break; + + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.AV1ProfileMain8: + case CodecProfileLevel.AV1ProfileMain10: + case CodecProfileLevel.AV1ProfileMain10HDR10: + case CodecProfileLevel.AV1ProfileMain10HDR10Plus: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + errors &= ~ERROR_NONE_SUPPORTED; + maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); + maxDim = Math.max(D, maxDim); } - /* DPB logic: - if (width * height <= FS / 4) DPB = 16; - else if (width * height <= FS / 2) DPB = 12; - else if (width * height <= FS * 0.75) DPB = 8; - else DPB = 6; - */ - - FS >>= 6; // convert pixels to blocks - errors &= ~ERROR_NONE_SUPPORTED; - maxBlocksPerSecond = Math.max((int)(FR * FS), maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); + final int blockSize = 8; + int maxLengthInBlocks = Utils.divUp(maxDim, blockSize); + maxBlocks = Utils.divUp(maxBlocks, blockSize * blockSize); + maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize); + applyMacroBlockLimits( + maxLengthInBlocks, maxLengthInBlocks, + maxBlocks, maxBlocksPerSecond, + blockSize, blockSize, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else { + Log.w(TAG, "Unsupported mime " + mime); + // using minimal bitrate here. should be overridden by + // info from media_codecs.xml + maxBps = 64000; + errors |= ERROR_UNSUPPORTED; } + mBitrateRange = Range.create(1, maxBps); + mParent.mError |= errors; + } + } - int maxLengthInBlocks = (int)(Math.sqrt(maxBlocks * 8)); - applyMacroBlockLimits( - maxLengthInBlocks, maxLengthInBlocks, - maxBlocks, maxBlocksPerSecond, - 8 /* blockWidth */, 8 /* blockHeight */, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AV1)) { - maxBlocksPerSecond = 829440; - maxBlocks = 36864; - maxBps = 200000; - int maxDim = 512; - - // Sample rate, Picture Size, Bit rate and luma dimension for AV1 Codec, - // corresponding to the definitions in - // "AV1 Bitstream & Decoding Process Specification", Annex A - // found at https://aomedia.org/av1-bitstream-and-decoding-process-specification/ - for (CodecProfileLevel profileLevel: profileLevels) { - long SR = 0; // luma sample rate - int FS = 0; // luma picture size - int BR = 0; // bit rate kbps - int D = 0; // luma D - switch (profileLevel.level) { - case CodecProfileLevel.AV1Level2: - SR = 5529600; FS = 147456; BR = 1500; D = 2048; break; - case CodecProfileLevel.AV1Level21: - case CodecProfileLevel.AV1Level22: - case CodecProfileLevel.AV1Level23: - SR = 10454400; FS = 278784; BR = 3000; D = 2816; break; - - case CodecProfileLevel.AV1Level3: - SR = 24969600; FS = 665856; BR = 6000; D = 4352; break; - case CodecProfileLevel.AV1Level31: - case CodecProfileLevel.AV1Level32: - case CodecProfileLevel.AV1Level33: - SR = 39938400; FS = 1065024; BR = 10000; D = 5504; break; - - case CodecProfileLevel.AV1Level4: - SR = 77856768; FS = 2359296; BR = 12000; D = 6144; break; - case CodecProfileLevel.AV1Level41: - case CodecProfileLevel.AV1Level42: - case CodecProfileLevel.AV1Level43: - SR = 155713536; FS = 2359296; BR = 20000; D = 6144; break; - - case CodecProfileLevel.AV1Level5: - SR = 273715200; FS = 8912896; BR = 30000; D = 8192; break; - case CodecProfileLevel.AV1Level51: - SR = 547430400; FS = 8912896; BR = 40000; D = 8192; break; - case CodecProfileLevel.AV1Level52: - SR = 1094860800; FS = 8912896; BR = 60000; D = 8192; break; - case CodecProfileLevel.AV1Level53: - SR = 1176502272; FS = 8912896; BR = 60000; D = 8192; break; - - case CodecProfileLevel.AV1Level6: - SR = 1176502272; FS = 35651584; BR = 60000; D = 16384; break; - case CodecProfileLevel.AV1Level61: - SR = 2189721600L; FS = 35651584; BR = 100000; D = 16384; break; - case CodecProfileLevel.AV1Level62: - SR = 4379443200L; FS = 35651584; BR = 160000; D = 16384; break; - case CodecProfileLevel.AV1Level63: - SR = 4706009088L; FS = 35651584; BR = 160000; D = 16384; break; - - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - switch (profileLevel.profile) { - case CodecProfileLevel.AV1ProfileMain8: - case CodecProfileLevel.AV1ProfileMain10: - case CodecProfileLevel.AV1ProfileMain10HDR10: - case CodecProfileLevel.AV1ProfileMain10HDR10Plus: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - errors &= ~ERROR_NONE_SUPPORTED; - maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); - maxDim = Math.max(D, maxDim); + /* package private */ static final class VideoCapsNativeImpl implements VideoCapsIntf { + private long mNativeContext; // accessed by native methods + + private Range<Integer> mBitrateRange; + private Range<Integer> mHeightRange; + private Range<Integer> mWidthRange; + private Range<Integer> mFrameRateRange; + private List<PerformancePoint> mPerformancePoints; + + private int mWidthAlignment; + private int mHeightAlignment; + + // Used by JNI to construct Java VideoCapsNativeImpl + /** package private */ VideoCapsNativeImpl(Range<Integer> bitrateRange, + Range<Integer> widthRange, Range<Integer> heightRange, + Range<Integer> frameRateRange, List<PerformancePoint> performancePoints, + int widthAlignment, int heightAlignment) { + mBitrateRange = new Range<Integer>(bitrateRange.getLower(), + bitrateRange.getUpper()); + mWidthRange = new Range<Integer>(widthRange.getLower(), widthRange.getUpper()); + mHeightRange = new Range<Integer>(heightRange.getLower(), heightRange.getUpper()); + mFrameRateRange = new Range<Integer>(frameRateRange.getLower(), + frameRateRange.getUpper()); + mPerformancePoints = new ArrayList<PerformancePoint>(); + for (PerformancePoint pp : performancePoints) { + mPerformancePoints.add(new PerformancePoint(pp)); } + mWidthAlignment = widthAlignment; + mHeightAlignment = heightAlignment; + } - final int blockSize = 8; - int maxLengthInBlocks = Utils.divUp(maxDim, blockSize); - maxBlocks = Utils.divUp(maxBlocks, blockSize * blockSize); - maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize); - applyMacroBlockLimits( - maxLengthInBlocks, maxLengthInBlocks, - maxBlocks, maxBlocksPerSecond, - blockSize, blockSize, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else { - Log.w(TAG, "Unsupported mime " + mime); - // using minimal bitrate here. should be overriden by - // info from media_codecs.xml - maxBps = 64000; - errors |= ERROR_UNSUPPORTED; - } - mBitrateRange = Range.create(1, maxBps); - mParent.mError |= errors; + /* no public constructor */ + private VideoCapsNativeImpl() { } + + public Range<Integer> getBitrateRange() { + return mBitrateRange; + } + + public Range<Integer> getSupportedWidths() { + return mWidthRange; + } + + public Range<Integer> getSupportedHeights() { + return mHeightRange; + } + + public int getWidthAlignment() { + return mWidthAlignment; + } + + public int getHeightAlignment() { + return mHeightAlignment; + } + + /** @hide */ + public int getSmallerDimensionUpperLimit() { + return native_getSmallerDimensionUpperLimit(); + } + + public Range<Integer> getSupportedFrameRates() { + return mFrameRateRange; + } + + @Nullable + public List<PerformancePoint> getSupportedPerformancePoints() { + return mPerformancePoints; + } + + public Range<Integer> getSupportedWidthsFor(int height) { + return native_getSupportedWidthsFor(height); + } + + public Range<Integer> getSupportedHeightsFor(int width) { + return native_getSupportedHeightsFor(width); + } + + public Range<Double> getSupportedFrameRatesFor(int width, int height) { + return native_getSupportedFrameRatesFor(width, height); + } + + /** @throws IllegalArgumentException if the video size is not supported. */ + @Nullable + public Range<Double> getAchievableFrameRatesFor(int width, int height) { + return native_getAchievableFrameRatesFor(width, height); + } + + public boolean areSizeAndRateSupported(int width, int height, double frameRate) { + return native_areSizeAndRateSupported(width, height, frameRate); + } + + public boolean isSizeSupported(int width, int height) { + return native_isSizeSupported(width, height); + } + + /** @hide */ + public boolean supportsFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); + } + + private native Range<Integer> native_getSupportedWidthsFor(int height); + private native Range<Integer> native_getSupportedHeightsFor(int width); + private native Range<Double> native_getSupportedFrameRatesFor(int width, int height); + private native Range<Double> native_getAchievableFrameRatesFor(int width, int height); + private native boolean native_areSizeAndRateSupported( + int width, int height, double frameRate); + private native boolean native_isSizeSupported(int width, int height); + private native int native_getSmallerDimensionUpperLimit(); + + private static native void native_init(); + + static { + System.loadLibrary("media_jni"); + native_init(); + } } - } - /** - * A class that supports querying the encoding capabilities of a codec. - */ - public static final class EncoderCapabilities { + private VideoCapsIntf mImpl; + + /** @hide */ + public static VideoCapabilities create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + VideoCapsLegacyImpl impl = VideoCapsLegacyImpl.create(info, parent); + VideoCapabilities caps = new VideoCapabilities(impl); + return caps; + } + + /* package private */ VideoCapabilities(VideoCapsIntf impl) { + mImpl = impl; + } + + /* no public constructor */ + private VideoCapabilities() { } + /** - * Returns the supported range of quality values. + * Returns the range of supported bitrates in bits/second. + */ + public Range<Integer> getBitrateRange() { + return mImpl.getBitrateRange(); + } + + /** + * Returns the range of supported video widths. + * <p class=note> + * 32-bit processes will not support resolutions larger than 4096x4096 due to + * the limited address space. + */ + public Range<Integer> getSupportedWidths() { + return mImpl.getSupportedWidths(); + } + + /** + * Returns the range of supported video heights. + * <p class=note> + * 32-bit processes will not support resolutions larger than 4096x4096 due to + * the limited address space. + */ + public Range<Integer> getSupportedHeights() { + return mImpl.getSupportedHeights(); + } + + /** + * Returns the alignment requirement for video width (in pixels). * - * Quality is implementation-specific. As a general rule, a higher quality - * setting results in a better image quality and a lower compression ratio. + * This is a power-of-2 value that video width must be a + * multiple of. */ - public Range<Integer> getQualityRange() { - return mQualityRange; + public int getWidthAlignment() { + return mImpl.getWidthAlignment(); } /** - * Returns the supported range of encoder complexity values. + * Returns the alignment requirement for video height (in pixels). + * + * This is a power-of-2 value that video height must be a + * multiple of. + */ + public int getHeightAlignment() { + return mImpl.getWidthAlignment(); + } + + /** + * Return the upper limit on the smaller dimension of width or height. + * <p></p> + * Some codecs have a limit on the smaller dimension, whether it be + * the width or the height. E.g. a codec may only be able to handle + * up to 1920x1080 both in landscape and portrait mode (1080x1920). + * In this case the maximum width and height are both 1920, but the + * smaller dimension limit will be 1080. For other codecs, this is + * {@code Math.min(getSupportedWidths().getUpper(), + * getSupportedHeights().getUpper())}. + * + * @hide + */ + public int getSmallerDimensionUpperLimit() { + return mImpl.getSmallerDimensionUpperLimit(); + } + + /** + * Returns the range of supported frame rates. * <p> - * Some codecs may support multiple complexity levels, where higher - * complexity values use more encoder tools (e.g. perform more - * intensive calculations) to improve the quality or the compression - * ratio. Use a lower value to save power and/or time. + * This is not a performance indicator. Rather, it expresses the + * limits specified in the coding standard, based on the complexities + * of encoding material for later playback at a certain frame rate, + * or the decoding of such material in non-realtime. */ - public Range<Integer> getComplexityRange() { - return mComplexityRange; + public Range<Integer> getSupportedFrameRates() { + return mImpl.getSupportedFrameRates(); + } + + /** + * Returns the range of supported video widths for a video height. + * @param height the height of the video + */ + public Range<Integer> getSupportedWidthsFor(int height) { + return mImpl.getSupportedWidthsFor(height); + } + + /** + * Returns the range of supported video heights for a video width + * @param width the width of the video + */ + public Range<Integer> getSupportedHeightsFor(int width) { + return mImpl.getSupportedHeightsFor(width); + } + + /** + * Returns the range of supported video frame rates for a video size. + * <p> + * This is not a performance indicator. Rather, it expresses the limits specified in + * the coding standard, based on the complexities of encoding material of a given + * size for later playback at a certain frame rate, or the decoding of such material + * in non-realtime. + + * @param width the width of the video + * @param height the height of the video + */ + public Range<Double> getSupportedFrameRatesFor(int width, int height) { + return mImpl.getSupportedFrameRatesFor(width, height); } + /** + * Returns the range of achievable video frame rates for a video size. + * May return {@code null}, if the codec did not publish any measurement + * data. + * <p> + * This is a performance estimate provided by the device manufacturer based on statistical + * sampling of full-speed decoding and encoding measurements in various configurations + * of common video sizes supported by the codec. As such it should only be used to + * compare individual codecs on the device. The value is not suitable for comparing + * different devices or even different android releases for the same device. + * <p> + * <em>On {@link android.os.Build.VERSION_CODES#M} release</em> the returned range + * corresponds to the fastest frame rates achieved in the tested configurations. As + * such, it should not be used to gauge guaranteed or even average codec performance + * on the device. + * <p> + * <em>On {@link android.os.Build.VERSION_CODES#N} release</em> the returned range + * corresponds closer to sustained performance <em>in tested configurations</em>. + * One can expect to achieve sustained performance higher than the lower limit more than + * 50% of the time, and higher than half of the lower limit at least 90% of the time + * <em>in tested configurations</em>. + * Conversely, one can expect performance lower than twice the upper limit at least + * 90% of the time. + * <p class=note> + * Tested configurations use a single active codec. For use cases where multiple + * codecs are active, applications can expect lower and in most cases significantly lower + * performance. + * <p class=note> + * The returned range value is interpolated from the nearest frame size(s) tested. + * Codec performance is severely impacted by other activity on the device as well + * as environmental factors (such as battery level, temperature or power source), and can + * vary significantly even in a steady environment. + * <p class=note> + * Use this method in cases where only codec performance matters, e.g. to evaluate if + * a codec has any chance of meeting a performance target. Codecs are listed + * in {@link MediaCodecList} in the preferred order as defined by the device + * manufacturer. As such, applications should use the first suitable codec in the + * list to achieve the best balance between power use and performance. + * + * @param width the width of the video + * @param height the height of the video + * + * @throws IllegalArgumentException if the video size is not supported. + */ + @Nullable + public Range<Double> getAchievableFrameRatesFor(int width, int height) { + return mImpl.getAchievableFrameRatesFor(width, height); + } + + /** + * Returns the supported performance points. May return {@code null} if the codec did not + * publish any performance point information (e.g. the vendor codecs have not been updated + * to the latest android release). May return an empty list if the codec published that + * if does not guarantee any performance points. + * <p> + * This is a performance guarantee provided by the device manufacturer for hardware codecs + * based on hardware capabilities of the device. + * <p> + * The returned list is sorted first by decreasing number of pixels, then by decreasing + * width, and finally by decreasing frame rate. + * Performance points assume a single active codec. For use cases where multiple + * codecs are active, should use that highest pixel count, and add the frame rates of + * each individual codec. + * <p class=note> + * 32-bit processes will not support resolutions larger than 4096x4096 due to + * the limited address space, but performance points will be presented as is. + * In other words, even though a component publishes a performance point for + * a resolution higher than 4096x4096, it does not mean that the resolution is supported + * for 32-bit processes. + */ + @Nullable + public List<PerformancePoint> getSupportedPerformancePoints() { + return mImpl.getSupportedPerformancePoints(); + } + + /** + * Returns whether a given video size ({@code width} and + * {@code height}) and {@code frameRate} combination is supported. + */ + public boolean areSizeAndRateSupported(int width, int height, double frameRate) { + return mImpl.areSizeAndRateSupported(width, height, frameRate); + } + + /** + * Returns whether a given video size ({@code width} and + * {@code height}) is supported. + */ + public boolean isSizeSupported(int width, int height) { + return mImpl.isSizeSupported(width, height); + } + + /** + * @hide + * @throws java.lang.ClassCastException + * @throws java.lang.UnsupportedOperationException + */ + public boolean supportsFormat(MediaFormat format) { + return mImpl.supportsFormat(format); + } + } + + /** + * A class that supports querying the encoding capabilities of a codec. + */ + public static final class EncoderCapabilities { + private static final String TAG = "EncoderCapabilities"; + /** Constant quality mode */ public static final int BITRATE_MODE_CQ = 0; /** Variable bitrate mode */ @@ -3874,188 +4563,314 @@ public final class MediaCodecInfo { /** Constant bitrate mode with frame drops */ public static final int BITRATE_MODE_CBR_FD = 3; - private static final Feature[] bitrates = new Feature[] { - new Feature("VBR", BITRATE_MODE_VBR, true), - new Feature("CBR", BITRATE_MODE_CBR, false), - new Feature("CQ", BITRATE_MODE_CQ, false), - new Feature("CBR-FD", BITRATE_MODE_CBR_FD, false) - }; - - private static int parseBitrateMode(String mode) { - for (Feature feat: bitrates) { - if (feat.mName.equalsIgnoreCase(mode)) { - return feat.mValue; - } - } - return 0; - } + /* package private */ interface EncoderCapsIntf { + public Range<Integer> getQualityRange(); - /** - * Query whether a bitrate mode is supported. - */ - public boolean isBitrateModeSupported(int mode) { - for (Feature feat: bitrates) { - if (mode == feat.mValue) { - return (mBitControl & (1 << mode)) != 0; - } - } - return false; - } + public Range<Integer> getComplexityRange(); - private Range<Integer> mQualityRange; - private Range<Integer> mComplexityRange; - private CodecCapabilities mParent; + public boolean isBitrateModeSupported(int mode); - /* no public constructor */ - private EncoderCapabilities() { } + public void getDefaultFormat(MediaFormat format); - /** @hide */ - public static EncoderCapabilities create( - MediaFormat info, CodecCapabilities parent) { - EncoderCapabilities caps = new EncoderCapabilities(); - caps.init(info, parent); - return caps; + public boolean supportsFormat(MediaFormat format); } - private void init(MediaFormat info, CodecCapabilities parent) { - // no support for complexity or quality yet - mParent = parent; - mComplexityRange = Range.create(0, 0); - mQualityRange = Range.create(0, 0); - mBitControl = (1 << BITRATE_MODE_VBR); + /* package private */ static final class EncoderCapsLegacyImpl implements EncoderCapsIntf { + private CodecCapabilities.CodecCapsLegacyImpl mParent; - applyLevelLimits(); - parseFromInfo(info); - } + private Range<Integer> mQualityRange; + private Range<Integer> mComplexityRange; - private void applyLevelLimits() { - String mime = mParent.getMimeType(); - if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_FLAC)) { - mComplexityRange = Range.create(0, 8); - mBitControl = (1 << BITRATE_MODE_CQ); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_NB) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_WB) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_ALAW) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_MLAW) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MSGSM)) { - mBitControl = (1 << BITRATE_MODE_CBR); + public Range<Integer> getQualityRange() { + return mQualityRange; } - } - private int mBitControl; - private Integer mDefaultComplexity; - private Integer mDefaultQuality; - private String mQualityScale; + public Range<Integer> getComplexityRange() { + return mComplexityRange; + } - private void parseFromInfo(MediaFormat info) { - Map<String, Object> map = info.getMap(); + private static final Feature[] bitrates = new Feature[] { + new Feature("VBR", BITRATE_MODE_VBR, true), + new Feature("CBR", BITRATE_MODE_CBR, false), + new Feature("CQ", BITRATE_MODE_CQ, false), + new Feature("CBR-FD", BITRATE_MODE_CBR_FD, false) + }; - if (info.containsKey("complexity-range")) { - mComplexityRange = Utils - .parseIntRange(info.getString("complexity-range"), mComplexityRange); - // TODO should we limit this to level limits? + private static int parseBitrateMode(String mode) { + for (Feature feat: bitrates) { + if (feat.mName.equalsIgnoreCase(mode)) { + return feat.mValue; + } + } + return 0; } - if (info.containsKey("quality-range")) { - mQualityRange = Utils - .parseIntRange(info.getString("quality-range"), mQualityRange); + + public boolean isBitrateModeSupported(int mode) { + for (Feature feat: bitrates) { + if (mode == feat.mValue) { + return (mBitControl & (1 << mode)) != 0; + } + } + return false; } - if (info.containsKey("feature-bitrate-modes")) { - mBitControl = 0; - for (String mode: info.getString("feature-bitrate-modes").split(",")) { - mBitControl |= (1 << parseBitrateMode(mode)); + + /* no public constructor */ + private EncoderCapsLegacyImpl() { } + + /** @hide */ + public static EncoderCapsLegacyImpl create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + Log.d(TAG, "Legacy implementation is called while native flag is on."); } + + EncoderCapsLegacyImpl caps = new EncoderCapsLegacyImpl(); + caps.init(info, parent); + return caps; } - try { - mDefaultComplexity = Integer.parseInt((String)map.get("complexity-default")); - } catch (NumberFormatException e) { } + private void init(MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + // no support for complexity or quality yet + mParent = parent; + mComplexityRange = Range.create(0, 0); + mQualityRange = Range.create(0, 0); + mBitControl = (1 << BITRATE_MODE_VBR); - try { - mDefaultQuality = Integer.parseInt((String)map.get("quality-default")); - } catch (NumberFormatException e) { } + applyLevelLimits(); + parseFromInfo(info); + } - mQualityScale = (String)map.get("quality-scale"); - } + private void applyLevelLimits() { + String mime = mParent.getMimeType(); + if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_FLAC)) { + mComplexityRange = Range.create(0, 8); + mBitControl = (1 << BITRATE_MODE_CQ); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_NB) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_WB) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_ALAW) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_MLAW) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MSGSM)) { + mBitControl = (1 << BITRATE_MODE_CBR); + } + } + + private int mBitControl; + private Integer mDefaultComplexity; + private Integer mDefaultQuality; + private String mQualityScale; - private boolean supports( - Integer complexity, Integer quality, Integer profile) { - boolean ok = true; - if (ok && complexity != null) { - ok = mComplexityRange.contains(complexity); + private void parseFromInfo(MediaFormat info) { + Map<String, Object> map = info.getMap(); + + if (info.containsKey("complexity-range")) { + mComplexityRange = Utils + .parseIntRange(info.getString("complexity-range"), mComplexityRange); + // TODO should we limit this to level limits? + } + if (info.containsKey("quality-range")) { + mQualityRange = Utils + .parseIntRange(info.getString("quality-range"), mQualityRange); + } + if (info.containsKey("feature-bitrate-modes")) { + mBitControl = 0; + for (String mode: info.getString("feature-bitrate-modes").split(",")) { + mBitControl |= (1 << parseBitrateMode(mode)); + } + } + + try { + mDefaultComplexity = Integer.parseInt((String)map.get("complexity-default")); + } catch (NumberFormatException e) { } + + try { + mDefaultQuality = Integer.parseInt((String)map.get("quality-default")); + } catch (NumberFormatException e) { } + + mQualityScale = (String)map.get("quality-scale"); } - if (ok && quality != null) { - ok = mQualityRange.contains(quality); + + private boolean supports( + Integer complexity, Integer quality, Integer profile) { + boolean ok = true; + if (ok && complexity != null) { + ok = mComplexityRange.contains(complexity); + } + if (ok && quality != null) { + ok = mQualityRange.contains(quality); + } + if (ok && profile != null) { + for (CodecProfileLevel pl: mParent.getProfileLevels()) { + if (pl.profile == profile) { + profile = null; + break; + } + } + ok = profile == null; + } + return ok; } - if (ok && profile != null) { - for (CodecProfileLevel pl: mParent.profileLevels) { - if (pl.profile == profile) { - profile = null; + + /** @hide */ + public void getDefaultFormat(MediaFormat format) { + // don't list trivial quality/complexity as default for now + if (!mQualityRange.getUpper().equals(mQualityRange.getLower()) + && mDefaultQuality != null) { + format.setInteger(MediaFormat.KEY_QUALITY, mDefaultQuality); + } + if (!mComplexityRange.getUpper().equals(mComplexityRange.getLower()) + && mDefaultComplexity != null) { + format.setInteger(MediaFormat.KEY_COMPLEXITY, mDefaultComplexity); + } + // bitrates are listed in order of preference + for (Feature feat: bitrates) { + if ((mBitControl & (1 << feat.mValue)) != 0) { + format.setInteger(MediaFormat.KEY_BITRATE_MODE, feat.mValue); break; } } - ok = profile == null; } - return ok; - } - /** @hide */ - public void getDefaultFormat(MediaFormat format) { - // don't list trivial quality/complexity as default for now - if (!mQualityRange.getUpper().equals(mQualityRange.getLower()) - && mDefaultQuality != null) { - format.setInteger(MediaFormat.KEY_QUALITY, mDefaultQuality); - } - if (!mComplexityRange.getUpper().equals(mComplexityRange.getLower()) - && mDefaultComplexity != null) { - format.setInteger(MediaFormat.KEY_COMPLEXITY, mDefaultComplexity); - } - // bitrates are listed in order of preference - for (Feature feat: bitrates) { - if ((mBitControl & (1 << feat.mValue)) != 0) { - format.setInteger(MediaFormat.KEY_BITRATE_MODE, feat.mValue); - break; + /** @hide */ + public boolean supportsFormat(MediaFormat format) { + final Map<String, Object> map = format.getMap(); + final String mime = mParent.getMimeType(); + + Integer mode = (Integer)map.get(MediaFormat.KEY_BITRATE_MODE); + if (mode != null && !isBitrateModeSupported(mode)) { + return false; + } + + Integer complexity = (Integer)map.get(MediaFormat.KEY_COMPLEXITY); + if (MediaFormat.MIMETYPE_AUDIO_FLAC.equalsIgnoreCase(mime)) { + Integer flacComplexity = + (Integer)map.get(MediaFormat.KEY_FLAC_COMPRESSION_LEVEL); + if (complexity == null) { + complexity = flacComplexity; + } else if (flacComplexity != null && !complexity.equals(flacComplexity)) { + throw new IllegalArgumentException( + "conflicting values for complexity and " + + "flac-compression-level"); + } } + + // other audio parameters + Integer profile = (Integer)map.get(MediaFormat.KEY_PROFILE); + if (MediaFormat.MIMETYPE_AUDIO_AAC.equalsIgnoreCase(mime)) { + Integer aacProfile = (Integer)map.get(MediaFormat.KEY_AAC_PROFILE); + if (profile == null) { + profile = aacProfile; + } else if (aacProfile != null && !aacProfile.equals(profile)) { + throw new IllegalArgumentException( + "conflicting values for profile and aac-profile"); + } + } + + Integer quality = (Integer)map.get(MediaFormat.KEY_QUALITY); + + return supports(complexity, quality, profile); } } - /** @hide */ - public boolean supportsFormat(MediaFormat format) { - final Map<String, Object> map = format.getMap(); - final String mime = mParent.getMimeType(); + /* package private */ static final class EncoderCapsNativeImpl implements EncoderCapsIntf { + private long mNativeContext; // accessed by native methods - Integer mode = (Integer)map.get(MediaFormat.KEY_BITRATE_MODE); - if (mode != null && !isBitrateModeSupported(mode)) { - return false; + private Range<Integer> mQualityRange; + private Range<Integer> mComplexityRange; + + /* no public constructor */ + private EncoderCapsNativeImpl() { } + + // Constructor called from native + /* package private */ EncoderCapsNativeImpl(Range<Integer> qualityRange, + Range<Integer> complexityRange) { + mQualityRange = qualityRange; + mComplexityRange = complexityRange; } - Integer complexity = (Integer)map.get(MediaFormat.KEY_COMPLEXITY); - if (MediaFormat.MIMETYPE_AUDIO_FLAC.equalsIgnoreCase(mime)) { - Integer flacComplexity = - (Integer)map.get(MediaFormat.KEY_FLAC_COMPRESSION_LEVEL); - if (complexity == null) { - complexity = flacComplexity; - } else if (flacComplexity != null && !complexity.equals(flacComplexity)) { - throw new IllegalArgumentException( - "conflicting values for complexity and " + - "flac-compression-level"); - } + public Range<Integer> getQualityRange() { + return mQualityRange; } - // other audio parameters - Integer profile = (Integer)map.get(MediaFormat.KEY_PROFILE); - if (MediaFormat.MIMETYPE_AUDIO_AAC.equalsIgnoreCase(mime)) { - Integer aacProfile = (Integer)map.get(MediaFormat.KEY_AAC_PROFILE); - if (profile == null) { - profile = aacProfile; - } else if (aacProfile != null && !aacProfile.equals(profile)) { - throw new IllegalArgumentException( - "conflicting values for profile and aac-profile"); - } + public Range<Integer> getComplexityRange() { + return mComplexityRange; + } + + public boolean isBitrateModeSupported(int mode) { + return native_isBitrateModeSupported(mode); } - Integer quality = (Integer)map.get(MediaFormat.KEY_QUALITY); + // This API is for internal Java implementation only. Should not be called. + public void getDefaultFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); + } + + // This API is for internal Java implementation only. Should not be called. + public boolean supportsFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); + } - return supports(complexity, quality, profile); + private native boolean native_isBitrateModeSupported(int mode); + private static native void native_init(); + + static { + System.loadLibrary("media_jni"); + native_init(); + } + } + + private EncoderCapsIntf mImpl; + + /** @hide */ + public static EncoderCapabilities create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + EncoderCapsLegacyImpl impl = EncoderCapsLegacyImpl.create(info, parent); + EncoderCapabilities caps = new EncoderCapabilities(impl); + return caps; + } + + /* package private */ EncoderCapabilities(EncoderCapsIntf impl) { + mImpl = impl; + } + + /** + * Returns the supported range of quality values. + * + * Quality is implementation-specific. As a general rule, a higher quality + * setting results in a better image quality and a lower compression ratio. + */ + public Range<Integer> getQualityRange() { + return mImpl.getQualityRange(); + } + + /** + * Returns the supported range of encoder complexity values. + * <p> + * Some codecs may support multiple complexity levels, where higher + * complexity values use more encoder tools (e.g. perform more + * intensive calculations) to improve the quality or the compression + * ratio. Use a lower value to save power and/or time. + */ + public Range<Integer> getComplexityRange() { + return mImpl.getComplexityRange(); + } + + /** + * Query whether a bitrate mode is supported. + */ + public boolean isBitrateModeSupported(int mode) { + return mImpl.isBitrateModeSupported(mode); + } + + /** @hide */ + public void getDefaultFormat(MediaFormat format) { + mImpl.getDefaultFormat(format); + } + + /** @hide */ + public boolean supportsFormat(MediaFormat format) { + return mImpl.supportsFormat(format); } }; @@ -4960,4 +5775,19 @@ public final class MediaCodecInfo { mName, mCanonicalName, mFlags, caps.toArray(new CodecCapabilities[caps.size()])); } + + /* package private */ class GenericHelper { + private static Range<Integer> constructIntegerRange(int lower, int upper) { + return Range.create(Integer.valueOf(lower), Integer.valueOf(upper)); + } + + private static Range<Double> constructDoubleRange(double lower, double upper) { + return Range.create(Double.valueOf(lower), Double.valueOf(upper)); + } + + private static List<VideoCapabilities.PerformancePoint> + constructPerformancePointList(VideoCapabilities.PerformancePoint[] array) { + return Arrays.asList(array); + } + } } diff --git a/media/jni/Android.bp b/media/jni/Android.bp index f09dc7218d7d..af545d5a4bc4 100644 --- a/media/jni/Android.bp +++ b/media/jni/Android.bp @@ -25,6 +25,7 @@ cc_library_shared { min_sdk_version: "", srcs: [ + "android_media_CodecCapabilities.cpp", "android_media_ImageWriter.cpp", "android_media_ImageReader.cpp", "android_media_JetPlayer.cpp", @@ -64,6 +65,7 @@ cc_library_shared { "libbinder", "libmedia", "libmedia_codeclist", + "libmedia_codeclist_capabilities", "libmedia_jni_utils", "libmedia_omx", "libmediametrics", diff --git a/media/jni/android_media_CodecCapabilities.cpp b/media/jni/android_media_CodecCapabilities.cpp new file mode 100644 index 000000000000..df0c826d8d87 --- /dev/null +++ b/media/jni/android_media_CodecCapabilities.cpp @@ -0,0 +1,1015 @@ +/* + * Copyright 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#define LOG_NDEBUG 0 +#define LOG_TAG "MediaCodec-JNI" + +#include "android_media_CodecCapabilities.h" +#include "android_media_Streams.h" +#include "android_runtime/AndroidRuntime.h" +#include "jni.h" + +#include <media/AudioCapabilities.h> +#include <media/CodecCapabilities.h> +#include <media/EncoderCapabilities.h> +#include <media/VideoCapabilities.h> +#include <media/stagefright/foundation/ADebug.h> +#include <media/stagefright/foundation/AMessage.h> +#include <nativehelper/JNIHelp.h> +#include <nativehelper/ScopedLocalRef.h> +#include <utils/Log.h> + +namespace android { + +struct fields_t { + jfieldID audioCapsContext; + jfieldID videoCapsContext; + jfieldID encoderCapsContext; + jfieldID codecCapsContext; +}; +static fields_t fields; + +// JCodecCapabilities + +JCodecCapabilities::JCodecCapabilities(std::shared_ptr<CodecCapabilities> codecCaps) + : mCodecCaps(codecCaps) {} + +std::shared_ptr<CodecCapabilities> JCodecCapabilities::getCodecCaps() const { + return mCodecCaps; +} + +int32_t JCodecCapabilities::getMaxSupportedInstances() const { + return mCodecCaps->getMaxSupportedInstances(); +} + +std::string JCodecCapabilities::getMediaType() const { + return mCodecCaps->getMediaType(); +} + +bool JCodecCapabilities::isFeatureRequired(const std::string& name) const { + return mCodecCaps->isFeatureRequired(name); +} + +bool JCodecCapabilities::isFeatureSupported(const std::string& name) const { + return mCodecCaps->isFeatureSupported(name); +} + +bool JCodecCapabilities::isFormatSupported(const sp<AMessage> &format) const { + return mCodecCaps->isFormatSupported(format); +} + +bool JCodecCapabilities::isRegular() const { + return mCodecCaps->isRegular(); +} + +// Setter + +static sp<JCodecCapabilities> setCodecCapabilities(JNIEnv *env, jobject thiz, + const sp<JCodecCapabilities>& jCodecCaps) { + sp<JCodecCapabilities> old + = (JCodecCapabilities*)env->GetLongField(thiz, fields.codecCapsContext); + if (jCodecCaps != NULL) { + jCodecCaps->incStrong(thiz); + } + if (old != NULL) { + old->decStrong(thiz); + } + env->SetLongField(thiz, fields.codecCapsContext, (jlong)jCodecCaps.get()); + return old; +} + +// Getters + +static AudioCapabilities* getAudioCapabilities(JNIEnv *env, jobject thiz) { + AudioCapabilities* const p = (AudioCapabilities*)env->GetLongField( + thiz, fields.audioCapsContext); + return p; +} + +static VideoCapabilities* getVideoCapabilities(JNIEnv *env, jobject thiz) { + VideoCapabilities* const p = (VideoCapabilities*)env->GetLongField( + thiz, fields.videoCapsContext); + return p; +} + +static EncoderCapabilities* getEncoderCapabilities(JNIEnv *env, jobject thiz) { + EncoderCapabilities* const p = (EncoderCapabilities*)env->GetLongField( + thiz, fields.encoderCapsContext); + return p; +} + +static sp<JCodecCapabilities> getCodecCapabilities(JNIEnv *env, jobject thiz) { + JCodecCapabilities* const p = (JCodecCapabilities*)env->GetLongField( + thiz, fields.codecCapsContext); + return sp<JCodecCapabilities>(p); +} + +// Utils + +static jobject convertToJavaIntRange(JNIEnv *env, const Range<int32_t>& range) { + jclass helperClazz = env->FindClass("android/media/MediaCodecInfo$GenericHelper"); + jmethodID constructIntegerRangeID = env->GetStaticMethodID(helperClazz, "constructIntegerRange", + "(II)Landroid/util/Range;"); + jobject jRange = env->CallStaticObjectMethod(helperClazz, constructIntegerRangeID, + range.lower(), range.upper()); + + return jRange; +} + +static jobject convertToJavaDoubleRange(JNIEnv *env, const Range<double>& range) { + jclass helperClazz = env->FindClass("android/media/MediaCodecInfo$GenericHelper"); + jmethodID constructDoubleRangeID = env->GetStaticMethodID(helperClazz, "constructDoubleRange", + "(DD)Landroid/util/Range;"); + jobject jRange = env->CallStaticObjectMethod(helperClazz, constructDoubleRangeID, + range.lower(), range.upper()); + return jRange; +} + +static jobjectArray convertToJavaIntRangeArray(JNIEnv *env, + const std::vector<Range<int32_t>>& ranges) { + jclass rangeClazz = env->FindClass("android/util/Range"); + CHECK(rangeClazz != NULL); + jobjectArray jRanges = env->NewObjectArray(ranges.size(), rangeClazz, NULL); + for (int i = 0; i < ranges.size(); i++) { + Range<int32_t> range = ranges.at(i); + jobject jRange = convertToJavaIntRange(env, range); + env->SetObjectArrayElement(jRanges, i, jRange); + env->DeleteLocalRef(jRange); + jRange = NULL; + } + return jRanges; +} + +// Converters between Java objects and native instances + +// The Java AudioCapabilities object keep bitrateRange, sampleRates, sampleRateRanges +// and inputChannelRanges in it to prevent reconstruction when called the getters functions. +static jobject convertToJavaAudioCapabilities( + JNIEnv *env, std::shared_ptr<AudioCapabilities> audioCaps) { + if (audioCaps == nullptr) { + return NULL; + } + + // construct Java bitrateRange + const Range<int32_t>& bitrateRange = audioCaps->getBitrateRange(); + jobject jBitrateRange = convertToJavaIntRange(env, bitrateRange); + + // construct Java sampleRates array + const std::vector<int32_t>& sampleRates = audioCaps->getSupportedSampleRates(); + jintArray jSampleRates = env->NewIntArray(sampleRates.size()); + for (size_t i = 0; i < sampleRates.size(); ++i) { + jint val = sampleRates.at(i); + env->SetIntArrayRegion(jSampleRates, i, 1, &val); + } + + // construct Java sampleRateRanges + const std::vector<Range<int32_t>>& sampleRateRanges = audioCaps->getSupportedSampleRateRanges(); + jobjectArray jSampleRateRanges = convertToJavaIntRangeArray(env, sampleRateRanges); + + // construct Java inputChannelRanges + const std::vector<Range<int32_t>>& inputChannelRanges = audioCaps->getInputChannelCountRanges(); + jobjectArray jInputChannelRanges = convertToJavaIntRangeArray(env, inputChannelRanges); + + // construct Java AudioCapsNativeImpl + jclass audioCapsImplClazz + = env->FindClass("android/media/MediaCodecInfo$AudioCapabilities$AudioCapsNativeImpl"); + CHECK(audioCapsImplClazz != NULL); + jmethodID audioCapsImplConstructID = env->GetMethodID(audioCapsImplClazz, "<init>", + "(Landroid/util/Range;" + "[I" + "[Landroid/util/Range;" + "[Landroid/util/Range;)V"); + jobject jAudioCapsImpl = env->NewObject(audioCapsImplClazz, audioCapsImplConstructID, + jBitrateRange, jSampleRates, jSampleRateRanges, jInputChannelRanges); + // The native AudioCapabilities won't be destructed until process ends. + env->SetLongField(jAudioCapsImpl, fields.audioCapsContext, (jlong)audioCaps.get()); + + // construct Java AudioCapabilities + jclass audioCapsClazz + = env->FindClass("android/media/MediaCodecInfo$AudioCapabilities"); + CHECK(audioCapsClazz != NULL); + jmethodID audioCapsConstructID = env->GetMethodID(audioCapsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$AudioCapabilities$AudioCapsIntf;)V"); + jobject jAudioCaps = env->NewObject(audioCapsClazz, audioCapsConstructID, jAudioCapsImpl); + + env->DeleteLocalRef(jBitrateRange); + jBitrateRange = NULL; + + env->DeleteLocalRef(jSampleRates); + jSampleRates = NULL; + + env->DeleteLocalRef(jSampleRateRanges); + jSampleRateRanges = NULL; + + env->DeleteLocalRef(jInputChannelRanges); + jInputChannelRanges = NULL; + + env->DeleteLocalRef(jAudioCapsImpl); + jAudioCapsImpl = NULL; + + return jAudioCaps; +} + +// convert native PerformancePoints to Java objects +static jobject convertToJavaPerformancePoints(JNIEnv *env, + const std::vector<VideoCapabilities::PerformancePoint>& performancePoints) { + jclass performancePointClazz = env->FindClass( + "android/media/MediaCodecInfo$VideoCapabilities$PerformancePoint"); + CHECK(performancePointClazz != NULL); + jmethodID performancePointConstructID = env->GetMethodID(performancePointClazz, "<init>", + "(IIIJII)V"); + + jobjectArray jPerformancePoints = env->NewObjectArray(performancePoints.size(), + performancePointClazz, NULL); + int i = 0; + for (auto it = performancePoints.begin(); it != performancePoints.end(); ++it, ++i) { + jobject jPerformancePoint = env->NewObject(performancePointClazz, + performancePointConstructID, it->getWidth(), + it->getHeight(), it->getMaxFrameRate(), + it->getMaxMacroBlockRate(), it->getBlockSize().getWidth(), + it->getBlockSize().getHeight()); + + env->SetObjectArrayElement(jPerformancePoints, i, jPerformancePoint); + + env->DeleteLocalRef(jPerformancePoint); + } + + jclass helperClazz = env->FindClass("android/media/MediaCodecInfo$GenericHelper"); + CHECK(helperClazz != NULL); + jmethodID asListID = env->GetStaticMethodID(helperClazz, "constructPerformancePointList", + "([Landroid/media/MediaCodecInfo$VideoCapabilities$PerformancePoint;)Ljava/util/List;"); + CHECK(asListID != NULL); + jobject jList = env->CallStaticObjectMethod(helperClazz, asListID, jPerformancePoints); + + return jList; +} + +static VideoCapabilities::PerformancePoint convertToNativePerformancePoint( + JNIEnv *env, jobject pp) { + if (pp == NULL) { + jniThrowException(env, "java/lang/NullPointerException", NULL); + } + + jclass clazz = env->FindClass( + "android/media/MediaCodecInfo$VideoCapabilities$PerformancePoint"); + CHECK(clazz != NULL); + CHECK(env->IsInstanceOf(pp, clazz)); + + jmethodID getWidthID = env->GetMethodID(clazz, "getWidth", "()I"); + CHECK(getWidthID != NULL); + jint width = env->CallIntMethod(pp, getWidthID); + + jmethodID getHeightID = env->GetMethodID(clazz, "getHeight", "()I"); + CHECK(getHeightID != NULL); + jint height = env->CallIntMethod(pp, getHeightID); + + jmethodID getMaxFrameRateID = env->GetMethodID(clazz, "getMaxFrameRate", "()I"); + CHECK(getMaxFrameRateID != NULL); + jint maxFrameRate = env->CallIntMethod(pp, getMaxFrameRateID); + + jmethodID getMaxMacroBlockRateID = env->GetMethodID(clazz, "getMaxMacroBlockRate", "()J"); + CHECK(getMaxMacroBlockRateID != NULL); + jlong maxMacroBlockRate = env->CallLongMethod(pp, getMaxMacroBlockRateID); + + jmethodID getBlockWidthID = env->GetMethodID(clazz, "getBlockWidth", "()I"); + CHECK(getBlockWidthID != NULL); + jint blockWidth = env->CallIntMethod(pp, getBlockWidthID); + + jmethodID getBlockHeightID = env->GetMethodID(clazz, "getBlockHeight", "()I"); + CHECK(getBlockHeightID != NULL); + jint blockHeight = env->CallIntMethod(pp, getBlockHeightID); + + return VideoCapabilities::PerformancePoint(VideoSize(blockWidth, blockHeight), + width, height, maxFrameRate, maxMacroBlockRate); +} + +static jobject convertToJavaVideoCapabilities(JNIEnv *env, + std::shared_ptr<VideoCapabilities> videoCaps) { + if (videoCaps == nullptr) { + return NULL; + } + + // get Java bitrateRange + const Range<int32_t>& bitrateRange = videoCaps->getBitrateRange(); + jobject jBitrateRange = convertToJavaIntRange(env, bitrateRange); + + // get Java widthRange + const Range<int32_t>& widthRange = videoCaps->getSupportedWidths(); + jobject jWidthRange = convertToJavaIntRange(env, widthRange); + + // get Java heightRange + const Range<int32_t>& heightRange = videoCaps->getSupportedHeights(); + jobject jHeightRange = convertToJavaIntRange(env, heightRange); + + // get Java frameRateRange + const Range<int32_t>& frameRateRange = videoCaps->getSupportedFrameRates(); + jobject jFrameRateRange = convertToJavaIntRange(env, frameRateRange); + + // get Java performancePoints + const std::vector<VideoCapabilities::PerformancePoint>& performancePoints + = videoCaps->getSupportedPerformancePoints(); + jobject jPerformancePoints = convertToJavaPerformancePoints(env, performancePoints); + + // get width alignment + int32_t widthAlignment = videoCaps->getWidthAlignment(); + + // get height alignment + int32_t heightAlignment = videoCaps->getHeightAlignment(); + + // get Java VideoCapsNativeImpl + jclass videoCapsImplClazz = env->FindClass( + "android/media/MediaCodecInfo$VideoCapabilities$VideoCapsNativeImpl"); + CHECK(videoCapsImplClazz != NULL); + jmethodID videoCapsImplConstructID = env->GetMethodID(videoCapsImplClazz, "<init>", + "(Landroid/util/Range;" + "Landroid/util/Range;" + "Landroid/util/Range;" + "Landroid/util/Range;" + "Ljava/util/List;II)V"); + jobject jVideoCapsImpl = env->NewObject(videoCapsImplClazz, videoCapsImplConstructID, + jBitrateRange, jWidthRange, jHeightRange, jFrameRateRange, jPerformancePoints, + widthAlignment, heightAlignment); + // The native VideoCapabilities won't be destructed until process ends. + env->SetLongField(jVideoCapsImpl, fields.videoCapsContext, (jlong)videoCaps.get()); + + // get Java VideoCapabilities + jclass videoCapsClazz + = env->FindClass("android/media/MediaCodecInfo$VideoCapabilities"); + CHECK(videoCapsClazz != NULL); + jmethodID videoCapsConstructID = env->GetMethodID(videoCapsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$VideoCapabilities$VideoCapsIntf;)V"); + jobject jVideoCaps = env->NewObject(videoCapsClazz, videoCapsConstructID, jVideoCapsImpl); + + env->DeleteLocalRef(jBitrateRange); + jBitrateRange = NULL; + + env->DeleteLocalRef(jWidthRange); + jWidthRange = NULL; + + env->DeleteLocalRef(jHeightRange); + jHeightRange = NULL; + + env->DeleteLocalRef(jFrameRateRange); + jFrameRateRange = NULL; + + env->DeleteLocalRef(jPerformancePoints); + jPerformancePoints = NULL; + + env->DeleteLocalRef(jVideoCapsImpl); + jVideoCapsImpl = NULL; + + return jVideoCaps; +} + +static jobject convertToJavaEncoderCapabilities(JNIEnv *env, + std::shared_ptr<EncoderCapabilities> encoderCaps) { + if (encoderCaps == nullptr) { + return NULL; + } + + // get quality range + const Range<int>& qualityRange = encoderCaps->getQualityRange(); + jobject jQualityRange = convertToJavaIntRange(env, qualityRange); + + // get complexity range + const Range<int>& complexityRange = encoderCaps->getComplexityRange(); + jobject jComplexityRange = convertToJavaIntRange(env, complexityRange); + + // construct java EncoderCapsNativeImpl + jclass encoderCapsImplClazz = env->FindClass( + "android/media/MediaCodecInfo$EncoderCapabilities$EncoderCapsNativeImpl"); + CHECK(encoderCapsImplClazz != NULL); + jmethodID encoderCapsImplConstructID = env->GetMethodID(encoderCapsImplClazz, "<init>", + "(Landroid/util/Range;Landroid/util/Range;)V"); + jobject jEncoderCapsImpl = env->NewObject(encoderCapsImplClazz, encoderCapsImplConstructID, + jQualityRange, jComplexityRange); + // The native EncoderCapabilities won't be destructed until process ends. + env->SetLongField(jEncoderCapsImpl, fields.encoderCapsContext, (jlong)encoderCaps.get()); + + // construct java EncoderCapabilities object + jclass encoderCapsClazz + = env->FindClass("android/media/MediaCodecInfo$EncoderCapabilities"); + CHECK(encoderCapsClazz != NULL); + jmethodID encoderCapsConstructID = env->GetMethodID(encoderCapsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$EncoderCapabilities$EncoderCapsIntf;)V"); + jobject jEncoderCaps = env->NewObject(encoderCapsClazz, encoderCapsConstructID, + jEncoderCapsImpl); + + env->DeleteLocalRef(jQualityRange); + jQualityRange = NULL; + + env->DeleteLocalRef(jComplexityRange); + jComplexityRange = NULL; + + env->DeleteLocalRef(jEncoderCapsImpl); + jEncoderCapsImpl = NULL; + + return jEncoderCaps; +} + +// Java CodecCapsNativeImpl keeps the defaultFormat, profileLevels, colorFormats, audioCapabilities, +// videoCapabilities and encoderCapabilities in it to prevent reconsturction when called by getter. +static jobject convertToJavaCodecCapsNativeImpl( + JNIEnv *env, std::shared_ptr<CodecCapabilities> codecCaps) { + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return NULL; + } + + // Construct defaultFormat + sp<AMessage> defaultFormat = codecCaps->getDefaultFormat(); + + jobject formatMap = NULL; + if (ConvertMessageToMap(env, defaultFormat, &formatMap)) { + return NULL; + } + + ScopedLocalRef<jclass> mediaFormatClass{env, env->FindClass("android/media/MediaFormat")}; + ScopedLocalRef<jobject> jDefaultFormat{env, env->NewObject( + mediaFormatClass.get(), + env->GetMethodID(mediaFormatClass.get(), "<init>", "(Ljava/util/Map;)V"), + formatMap)}; + + env->DeleteLocalRef(formatMap); + formatMap = NULL; + + // Construct Java ProfileLevelArray + std::vector<ProfileLevel> profileLevels = codecCaps->getProfileLevels(); + + jclass profileLevelClazz = + env->FindClass("android/media/MediaCodecInfo$CodecProfileLevel"); + CHECK(profileLevelClazz != NULL); + + jobjectArray profileLevelArray = + env->NewObjectArray(profileLevels.size(), profileLevelClazz, NULL); + + jfieldID profileField = + env->GetFieldID(profileLevelClazz, "profile", "I"); + jfieldID levelField = + env->GetFieldID(profileLevelClazz, "level", "I"); + + for (size_t i = 0; i < profileLevels.size(); ++i) { + const ProfileLevel &src = profileLevels.at(i); + + jobject profileLevelObj = env->AllocObject(profileLevelClazz); + + env->SetIntField(profileLevelObj, profileField, src.mProfile); + env->SetIntField(profileLevelObj, levelField, src.mLevel); + + env->SetObjectArrayElement(profileLevelArray, i, profileLevelObj); + + env->DeleteLocalRef(profileLevelObj); + profileLevelObj = NULL; + } + + // Construct ColorFormatArray + std::vector<uint32_t> colorFormats = codecCaps->getColorFormats(); + + jintArray colorFormatsArray = env->NewIntArray(colorFormats.size()); + env->SetIntArrayRegion(colorFormatsArray, 0, colorFormats.size(), + reinterpret_cast<jint*>(colorFormats.data())); + + // Construct and set AudioCapabilities + std::shared_ptr<AudioCapabilities> audioCaps = codecCaps->getAudioCapabilities(); + jobject jAudioCaps = convertToJavaAudioCapabilities(env, audioCaps); + + // Set VideoCapabilities + std::shared_ptr<VideoCapabilities> videoCaps = codecCaps->getVideoCapabilities(); + jobject jVideoCaps = convertToJavaVideoCapabilities(env, videoCaps); + + // Set EncoderCapabilities + std::shared_ptr<EncoderCapabilities> encoderCaps = codecCaps->getEncoderCapabilities(); + jobject jEncoderCaps = convertToJavaEncoderCapabilities(env, encoderCaps); + + // Construct CodecCapsNativeImpl + jclass codecCapsImplClazz = + env->FindClass("android/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl"); + CHECK(codecCapsImplClazz != NULL); + jmethodID codecCapsImplConstructID = env->GetMethodID(codecCapsImplClazz, "<init>", + "([Landroid/media/MediaCodecInfo$CodecProfileLevel;[I" + "Landroid/media/MediaFormat;" + "Landroid/media/MediaCodecInfo$AudioCapabilities;" + "Landroid/media/MediaCodecInfo$VideoCapabilities;" + "Landroid/media/MediaCodecInfo$EncoderCapabilities;)V"); + jobject javaCodecCapsImpl = env->NewObject(codecCapsImplClazz, codecCapsImplConstructID, + profileLevelArray, colorFormatsArray, jDefaultFormat.get(), + jAudioCaps, jVideoCaps, jEncoderCaps); + + // Construct JCodecCapabilities and hold the codecCaps in it + sp<JCodecCapabilities> jCodecCaps = sp<JCodecCapabilities>::make(codecCaps); + setCodecCapabilities(env, javaCodecCapsImpl, jCodecCaps); + + env->DeleteLocalRef(profileLevelArray); + profileLevelArray = NULL; + + env->DeleteLocalRef(colorFormatsArray); + colorFormatsArray = NULL; + + env->DeleteLocalRef(jAudioCaps); + jAudioCaps = NULL; + + env->DeleteLocalRef(jVideoCaps); + jVideoCaps = NULL; + + env->DeleteLocalRef(jEncoderCaps); + jEncoderCaps = NULL; + + return javaCodecCapsImpl; +} + +jobject convertToJavaCodecCapabiliites( + JNIEnv *env, std::shared_ptr<CodecCapabilities> codecCaps) { + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return NULL; + } + + jobject javaCodecCapsImpl = convertToJavaCodecCapsNativeImpl(env, codecCaps); + + // Construct CodecCapabilities + jclass codecCapsClazz = env->FindClass("android/media/MediaCodecInfo$CodecCapabilities"); + CHECK(codecCapsClazz != NULL); + + jmethodID codecCapsConstructID = env->GetMethodID(codecCapsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsIntf;)V"); + jobject javaCodecCaps = env->NewObject(codecCapsClazz, codecCapsConstructID, javaCodecCapsImpl); + + return javaCodecCaps; +} + +} // namespace android + +// ---------------------------------------------------------------------------- + +using namespace android; + +// AudioCapabilities + +static void android_media_AudioCapabilities_native_init(JNIEnv *env, jobject /* thiz */) { + jclass audioCapsImplClazz + = env->FindClass("android/media/MediaCodecInfo$AudioCapabilities$AudioCapsNativeImpl"); + if (audioCapsImplClazz == NULL) { + return; + } + + fields.audioCapsContext = env->GetFieldID(audioCapsImplClazz, "mNativeContext", "J"); + if (fields.audioCapsContext == NULL) { + return; + } + + env->DeleteLocalRef(audioCapsImplClazz); +} + +static jint android_media_AudioCapabilities_getMaxInputChannelCount(JNIEnv *env, jobject thiz) { + AudioCapabilities* const audioCaps = getAudioCapabilities(env, thiz); + if (audioCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + int32_t maxInputChannelCount = audioCaps->getMaxInputChannelCount(); + return maxInputChannelCount; +} + +static jint android_media_AudioCapabilities_getMinInputChannelCount(JNIEnv *env, jobject thiz) { + AudioCapabilities* const audioCaps = getAudioCapabilities(env, thiz); + if (audioCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + int32_t minInputChannelCount = audioCaps->getMinInputChannelCount(); + return minInputChannelCount; +} + +static jboolean android_media_AudioCapabilities_isSampleRateSupported(JNIEnv *env, jobject thiz, + int sampleRate) { + AudioCapabilities* const audioCaps = getAudioCapabilities(env, thiz); + if (audioCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + bool res = audioCaps->isSampleRateSupported(sampleRate); + return res; +} + +// PerformancePoint + +static jboolean android_media_VideoCapabilities_PerformancePoint_covers(JNIEnv *env, jobject thiz, + jobject other) { + VideoCapabilities::PerformancePoint pp0 = convertToNativePerformancePoint(env, thiz); + VideoCapabilities::PerformancePoint pp1 = convertToNativePerformancePoint(env, other); + + bool res = pp0.covers(pp1); + return res; +} + +static jboolean android_media_VideoCapabilities_PerformancePoint_equals(JNIEnv *env, jobject thiz, + jobject other) { + VideoCapabilities::PerformancePoint pp0 = convertToNativePerformancePoint(env, thiz); + VideoCapabilities::PerformancePoint pp1 = convertToNativePerformancePoint(env, other); + + bool res = pp0.equals(pp1); + return res; +} + +// VideoCapabilities + +static void android_media_VideoCapabilities_native_init(JNIEnv *env, jobject /* thiz */) { + jclass clazz + = env->FindClass("android/media/MediaCodecInfo$VideoCapabilities$VideoCapsNativeImpl"); + if (clazz == NULL) { + return; + } + + fields.videoCapsContext = env->GetFieldID(clazz, "mNativeContext", "J"); + if (fields.videoCapsContext == NULL) { + return; + } + + env->DeleteLocalRef(clazz); +} + +static jboolean android_media_VideoCapabilities_areSizeAndRateSupported(JNIEnv *env, jobject thiz, + int32_t width, int32_t height, double frameRate) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + bool res = videoCaps->areSizeAndRateSupported(width, height, frameRate); + return res; +} + +static jboolean android_media_VideoCapabilities_isSizeSupported(JNIEnv *env, jobject thiz, + int32_t width, int32_t height) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + bool res = videoCaps->isSizeSupported(width, height); + return res; +} + +static jobject android_media_VideoCapabilities_getAchievableFrameRatesFor(JNIEnv *env, jobject thiz, + int32_t width, int32_t height) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::optional<Range<double>> frameRates = videoCaps->getAchievableFrameRatesFor(width, height); + if (!frameRates) { + return NULL; + } + jobject jFrameRates = convertToJavaDoubleRange(env, frameRates.value()); + return jFrameRates; +} + +static jobject android_media_VideoCapabilities_getSupportedFrameRatesFor(JNIEnv *env, jobject thiz, + int32_t width, int32_t height) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::optional<Range<double>> frameRates = videoCaps->getSupportedFrameRatesFor(width, height); + if (!frameRates) { + return NULL; + } + jobject jFrameRates = convertToJavaDoubleRange(env, frameRates.value()); + return jFrameRates; +} + +static jobject android_media_VideoCapabilities_getSupportedWidthsFor(JNIEnv *env, jobject thiz, + int32_t height) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::optional<Range<int32_t>> supportedWidths = videoCaps->getSupportedWidthsFor(height); + if (!supportedWidths) { + return NULL; + } + jobject jSupportedWidths = convertToJavaIntRange(env, supportedWidths.value()); + + return jSupportedWidths; +} + +static jobject android_media_VideoCapabilities_getSupportedHeightsFor(JNIEnv *env, jobject thiz, + int32_t width) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::optional<Range<int32_t>> supportedHeights = videoCaps->getSupportedHeightsFor(width); + if (!supportedHeights) { + return NULL; + } + jobject jSupportedHeights = convertToJavaIntRange(env, supportedHeights.value()); + + return jSupportedHeights; +} + +static jint android_media_VideoCapabilities_getSmallerDimensionUpperLimit(JNIEnv *env, + jobject thiz) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + int smallerDimensionUpperLimit = videoCaps->getSmallerDimensionUpperLimit(); + return smallerDimensionUpperLimit; +} + +// EncoderCapabilities + +static void android_media_EncoderCapabilities_native_init(JNIEnv *env, jobject /* thiz */) { + jclass clazz = env->FindClass( + "android/media/MediaCodecInfo$EncoderCapabilities$EncoderCapsNativeImpl"); + if (clazz == NULL) { + return; + } + + fields.encoderCapsContext = env->GetFieldID(clazz, "mNativeContext", "J"); + if (fields.encoderCapsContext == NULL) { + return; + } + + env->DeleteLocalRef(clazz); +} + +static jboolean android_media_EncoderCapabilities_isBitrateModeSupported(JNIEnv *env, jobject thiz, + int mode) { + EncoderCapabilities* const encoderCaps = getEncoderCapabilities(env, thiz); + if (encoderCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + bool res = encoderCaps->isBitrateModeSupported(mode); + return res; +} + +// CodecCapabilities + +static void android_media_CodecCapabilities_native_init(JNIEnv *env, jobject /* thiz */) { + jclass codecCapsClazz + = env->FindClass("android/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl"); + if (codecCapsClazz == NULL) { + return; + } + + fields.codecCapsContext = env->GetFieldID(codecCapsClazz, "mNativeContext", "J"); + if (fields.codecCapsContext == NULL) { + return; + } + + env->DeleteLocalRef(codecCapsClazz); +} + +static jobject android_media_CodecCapabilities_createFromProfileLevel(JNIEnv *env, + jobject /* thiz */, jstring mediaType, jint profile, jint level) { + if (mediaType == NULL) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return NULL; + } + + const char *mediaTypeStr = env->GetStringUTFChars(mediaType, nullptr); + if (mediaTypeStr == nullptr) { + return NULL; + } + + std::shared_ptr<CodecCapabilities> codecCaps = CodecCapabilities::CreateFromProfileLevel( + mediaTypeStr, profile, level); + + jobject javaCodecCapsImpl = convertToJavaCodecCapsNativeImpl(env, codecCaps); + + env->ReleaseStringUTFChars(mediaType, mediaTypeStr); + + return javaCodecCapsImpl; +} + +static jobject android_media_CodecCapabilities_native_dup(JNIEnv *env, jobject thiz) { + sp<JCodecCapabilities> jCodecCaps = getCodecCapabilities(env, thiz); + + // As the CodecCaps objects are ready ony, it is ok to use the default copy constructor. + // The duplicate CodecCaps will share the same subobjects with the existing one. + // The lifetime of subobjects are managed by the shared pointer and sp. + std::shared_ptr<CodecCapabilities> duplicate + = std::make_shared<CodecCapabilities>(*(jCodecCaps->getCodecCaps())); + + jobject javaCodecCapsImpl = convertToJavaCodecCapsNativeImpl(env, duplicate); + + return javaCodecCapsImpl; +} + +static void android_media_CodecCapabilities_native_finalize(JNIEnv *env, jobject thiz) { + ALOGV("native_finalize"); + setCodecCapabilities(env, thiz, NULL); +} + +static jint android_media_CodecCapabilities_getMaxSupportedInstances(JNIEnv *env, jobject thiz) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + int maxSupportedInstances = codecCaps->getMaxSupportedInstances(); + return maxSupportedInstances; +} + +static jstring android_media_CodecCapabilities_getMimeType(JNIEnv *env, jobject thiz) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::string mediaType = codecCaps->getMediaType(); + return env->NewStringUTF(mediaType.c_str()); +} + +static jboolean android_media_CodecCapabilities_isFeatureRequired( + JNIEnv *env, jobject thiz, jstring name) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return false; + } + + if (name == NULL) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return -ENOENT; + } + + const char *nameStr = env->GetStringUTFChars(name, NULL); + if (nameStr == NULL) { + // Out of memory exception already pending. + return -ENOENT; + } + + bool isFeatureRequired = codecCaps->isFeatureRequired(nameStr); + + env->ReleaseStringUTFChars(name, nameStr); + + return isFeatureRequired; +} + +static jboolean android_media_CodecCapabilities_isFeatureSupported( + JNIEnv *env, jobject thiz, jstring name) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return false; + } + + if (name == NULL) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return -ENOENT; + } + + const char *nameStr = env->GetStringUTFChars(name, NULL); + if (nameStr == NULL) { + // Out of memory exception already pending. + return -ENOENT; + } + + bool isFeatureSupported = codecCaps->isFeatureSupported(nameStr); + + env->ReleaseStringUTFChars(name, nameStr); + + return isFeatureSupported; +} + +static jboolean android_media_CodecCapabilities_isFormatSupported(JNIEnv *env, jobject thiz, + jobjectArray keys, jobjectArray values) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return false; + } + + sp<AMessage> format; + status_t err = ConvertKeyValueArraysToMessage(env, keys, values, &format); + if (err != OK) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return -ENOENT;; + } + + return codecCaps->isFormatSupported(format); +} + +static jboolean android_media_CodecCapabilities_isRegular(JNIEnv *env, jobject thiz) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return false; + } + + bool res = codecCaps->isRegular(); + return res; +} + +// ---------------------------------------------------------------------------- + +static const JNINativeMethod gAudioCapsMethods[] = { + {"native_init", "()V", (void *)android_media_AudioCapabilities_native_init}, + {"native_getMaxInputChannelCount", "()I", (void *)android_media_AudioCapabilities_getMaxInputChannelCount}, + {"native_getMinInputChannelCount", "()I", (void *)android_media_AudioCapabilities_getMinInputChannelCount}, + {"native_isSampleRateSupported", "(I)Z", (void *)android_media_AudioCapabilities_isSampleRateSupported} +}; + +static const JNINativeMethod gPerformancePointMethods[] = { + {"native_covers", "(Landroid/media/MediaCodecInfo$VideoCapabilities$PerformancePoint;)Z", (void *)android_media_VideoCapabilities_PerformancePoint_covers}, + {"native_equals", "(Landroid/media/MediaCodecInfo$VideoCapabilities$PerformancePoint;)Z", (void *)android_media_VideoCapabilities_PerformancePoint_equals}, +}; + +static const JNINativeMethod gVideoCapsMethods[] = { + {"native_init", "()V", (void *)android_media_VideoCapabilities_native_init}, + {"native_areSizeAndRateSupported", "(IID)Z", (void *)android_media_VideoCapabilities_areSizeAndRateSupported}, + {"native_isSizeSupported", "(II)Z", (void *)android_media_VideoCapabilities_isSizeSupported}, + {"native_getAchievableFrameRatesFor", "(II)Landroid/util/Range;", (void *)android_media_VideoCapabilities_getAchievableFrameRatesFor}, + {"native_getSupportedFrameRatesFor", "(II)Landroid/util/Range;", (void *)android_media_VideoCapabilities_getSupportedFrameRatesFor}, + {"native_getSupportedWidthsFor", "(I)Landroid/util/Range;", (void *)android_media_VideoCapabilities_getSupportedWidthsFor}, + {"native_getSupportedHeightsFor", "(I)Landroid/util/Range;", (void *)android_media_VideoCapabilities_getSupportedHeightsFor}, + {"native_getSmallerDimensionUpperLimit", "()I", (void *)android_media_VideoCapabilities_getSmallerDimensionUpperLimit} +}; + +static const JNINativeMethod gEncoderCapsMethods[] = { + {"native_init", "()V", (void *)android_media_EncoderCapabilities_native_init}, + {"native_isBitrateModeSupported", "(I)Z", (void *)android_media_EncoderCapabilities_isBitrateModeSupported} +}; + +static const JNINativeMethod gCodecCapsMethods[] = { + { "native_init", "()V", (void *)android_media_CodecCapabilities_native_init }, + { "native_createFromProfileLevel", "(Ljava/lang/String;II)Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl;", (void *)android_media_CodecCapabilities_createFromProfileLevel }, + { "native_dup", "()Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl;", (void *)android_media_CodecCapabilities_native_dup }, + { "native_finalize", "()V", (void *)android_media_CodecCapabilities_native_finalize }, + { "native_getMaxSupportedInstances", "()I", (void *)android_media_CodecCapabilities_getMaxSupportedInstances }, + { "native_getMimeType", "()Ljava/lang/String;", (void *)android_media_CodecCapabilities_getMimeType }, + { "native_isFeatureRequired", "(Ljava/lang/String;)Z", (void *)android_media_CodecCapabilities_isFeatureRequired }, + { "native_isFeatureSupported", "(Ljava/lang/String;)Z", (void *)android_media_CodecCapabilities_isFeatureSupported }, + { "native_isFormatSupported", "([Ljava/lang/String;[Ljava/lang/Object;)Z", (void *)android_media_CodecCapabilities_isFormatSupported }, + { "native_isRegular", "()Z", (void *)android_media_CodecCapabilities_isRegular }, +}; + +int register_android_media_CodecCapabilities(JNIEnv *env) { + int result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$AudioCapabilities$AudioCapsNativeImpl", + gAudioCapsMethods, NELEM(gAudioCapsMethods)); + if (result != JNI_OK) { + return result; + } + + result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$VideoCapabilities$PerformancePoint", + gPerformancePointMethods, NELEM(gPerformancePointMethods)); + if (result != JNI_OK) { + return result; + } + + result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$VideoCapabilities$VideoCapsNativeImpl", + gVideoCapsMethods, NELEM(gVideoCapsMethods)); + if (result != JNI_OK) { + return result; + } + + result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$EncoderCapabilities$EncoderCapsNativeImpl", + gEncoderCapsMethods, NELEM(gEncoderCapsMethods)); + if (result != JNI_OK) { + return result; + } + + result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl", + gCodecCapsMethods, NELEM(gCodecCapsMethods)); + return result; +}
\ No newline at end of file diff --git a/media/jni/android_media_CodecCapabilities.h b/media/jni/android_media_CodecCapabilities.h new file mode 100644 index 000000000000..5cca0b503740 --- /dev/null +++ b/media/jni/android_media_CodecCapabilities.h @@ -0,0 +1,47 @@ +/* + * Copyright 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _ANDROID_MEDIA_CODECCAPABILITIES_H_ +#define _ANDROID_MEDIA_CODECCAPABILITIES_H_ + +#include "jni.h" + +#include <media/CodecCapabilities.h> + +namespace android { + +struct JCodecCapabilities : public RefBase { + JCodecCapabilities(std::shared_ptr<CodecCapabilities> codecCaps); + + std::shared_ptr<CodecCapabilities> getCodecCaps() const; + + int32_t getMaxSupportedInstances() const; + std::string getMediaType() const; + bool isFeatureRequired(const std::string& name) const; + bool isFeatureSupported(const std::string& name) const; + bool isFormatSupported(const sp<AMessage> &format) const; + bool isRegular() const; + +private: + std::shared_ptr<CodecCapabilities> mCodecCaps; +}; + +jobject convertToJavaCodecCapabiliites( + JNIEnv *env, std::shared_ptr<CodecCapabilities> codecCaps); + +} + +#endif // _ANDROID_MEDIA_CODECCAPABILITIES_H_
\ No newline at end of file diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp index 8419ce761a4a..1790670903a4 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -16,12 +16,14 @@ //#define LOG_NDEBUG 0 #define LOG_TAG "MediaCodec-JNI" +#include <android_media_codec.h> #include <utils/Log.h> #include <type_traits> #include "android_media_MediaCodec.h" +#include "android_media_CodecCapabilities.h" #include "android_media_MediaCodecLinearBlock.h" #include "android_media_MediaCrypto.h" #include "android_media_MediaDescrambler.h" @@ -138,6 +140,8 @@ static struct { static struct { jclass capsClazz; jmethodID capsCtorId; + jclass cpasImplClazz; + jmethodID capsImplCtorId; jclass profileLevelClazz; jfieldID profileField; jfieldID levelField; @@ -996,10 +1000,12 @@ static jobject getCodecCapabilitiesObject( env->SetIntArrayRegion(colorFormatsArray.get(), i, 1, &val); } - return env->NewObject( - gCodecInfo.capsClazz, gCodecInfo.capsCtorId, + jobject javaCodecCapsImpl = env->NewObject( + gCodecInfo.cpasImplClazz, gCodecInfo.capsImplCtorId, profileLevelArray.get(), colorFormatsArray.get(), isEncoder, defaultFormatRef.get(), detailsRef.get()); + + return env->NewObject(gCodecInfo.capsClazz, gCodecInfo.capsCtorId, javaCodecCapsImpl); } status_t JMediaCodec::getCodecInfo(JNIEnv *env, jobject *codecInfoObject) const { @@ -1027,11 +1033,18 @@ status_t JMediaCodec::getCodecInfo(JNIEnv *env, jobject *codecInfoObject) const env->NewObjectArray(mediaTypes.size(), gCodecInfo.capsClazz, NULL)); for (size_t i = 0; i < mediaTypes.size(); i++) { - const sp<MediaCodecInfo::Capabilities> caps = - codecInfo->getCapabilitiesFor(mediaTypes[i].c_str()); - - ScopedLocalRef<jobject> capsObj(env, getCodecCapabilitiesObject( - env, mediaTypes[i].c_str(), isEncoder, caps)); + jobject jCodecCaps = NULL; + if (android::media::codec::provider_->native_capabilites()) { + const std::shared_ptr<CodecCapabilities> codecCaps + = codecInfo->getCodecCapsFor(mediaTypes[i].c_str()); + jCodecCaps = convertToJavaCodecCapabiliites(env, codecCaps); + } else { + const sp<MediaCodecInfo::Capabilities> caps = + codecInfo->getCapabilitiesFor(mediaTypes[i].c_str()); + jCodecCaps = getCodecCapabilitiesObject( + env, mediaTypes[i].c_str(), isEncoder, caps); + } + ScopedLocalRef<jobject> capsObj(env, jCodecCaps); env->SetObjectArrayElement(capsArrayObj.get(), i, capsObj.get()); } @@ -3877,10 +3890,20 @@ static void android_media_MediaCodec_native_init(JNIEnv *env, jclass) { gCodecInfo.capsClazz = (jclass)env->NewGlobalRef(clazz.get()); method = env->GetMethodID(clazz.get(), "<init>", + "(Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsIntf;)V"); + CHECK(method != NULL); + gCodecInfo.capsCtorId = method; + + clazz.reset(env->FindClass( + "android/media/MediaCodecInfo$CodecCapabilities$CodecCapsLegacyImpl")); + CHECK(clazz.get() != NULL); + gCodecInfo.cpasImplClazz = (jclass)env->NewGlobalRef(clazz.get()); + + method = env->GetMethodID(clazz.get(), "<init>", "([Landroid/media/MediaCodecInfo$CodecProfileLevel;[IZ" "Ljava/util/Map;Ljava/util/Map;)V"); CHECK(method != NULL); - gCodecInfo.capsCtorId = method; + gCodecInfo.capsImplCtorId = method; clazz.reset(env->FindClass("android/media/MediaCodecInfo$CodecProfileLevel")); CHECK(clazz.get() != NULL); diff --git a/media/jni/android_media_MediaCodecList.cpp b/media/jni/android_media_MediaCodecList.cpp index 07866ac34e4c..3522b35539ab 100644 --- a/media/jni/android_media_MediaCodecList.cpp +++ b/media/jni/android_media_MediaCodecList.cpp @@ -16,6 +16,9 @@ //#define LOG_NDEBUG 0 #define LOG_TAG "MediaCodec-JNI" + +#include <android_media_codec.h> + #include <utils/Log.h> #include <media/stagefright/foundation/ADebug.h> @@ -32,6 +35,7 @@ #include "android_runtime/AndroidRuntime.h" #include "jni.h" #include <nativehelper/JNIHelp.h> +#include "android_media_CodecCapabilities.h" #include "android_media_Streams.h" using namespace android; @@ -245,95 +249,113 @@ static jobject android_media_MediaCodecList_getCodecCapabilities( return NULL; } - Vector<MediaCodecInfo::ProfileLevel> profileLevels; - Vector<uint32_t> colorFormats; + jobject caps; + if (android::media::codec::provider_->native_capabilites()) { + std::shared_ptr<CodecCapabilities> codecCaps = info.info->getCodecCapsFor(typeStr); + caps = android::convertToJavaCodecCapabiliites(env, codecCaps); + } else { + Vector<MediaCodecInfo::ProfileLevel> profileLevels; + Vector<uint32_t> colorFormats; + + sp<AMessage> defaultFormat = new AMessage(); + defaultFormat->setString("mime", typeStr); + + // TODO query default-format also from codec/codec list + const sp<MediaCodecInfo::Capabilities> &capabilities = + info.info->getCapabilitiesFor(typeStr); + env->ReleaseStringUTFChars(type, typeStr); + typeStr = NULL; + if (capabilities == NULL) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return NULL; + } - sp<AMessage> defaultFormat = new AMessage(); - defaultFormat->setString("mime", typeStr); + capabilities->getSupportedColorFormats(&colorFormats); + capabilities->getSupportedProfileLevels(&profileLevels); + sp<AMessage> details = capabilities->getDetails(); + bool isEncoder = info.info->isEncoder(); - // TODO query default-format also from codec/codec list - const sp<MediaCodecInfo::Capabilities> &capabilities = - info.info->getCapabilitiesFor(typeStr); - env->ReleaseStringUTFChars(type, typeStr); - typeStr = NULL; - if (capabilities == NULL) { - jniThrowException(env, "java/lang/IllegalArgumentException", NULL); - return NULL; - } + jobject defaultFormatObj = NULL; + if (ConvertMessageToMap(env, defaultFormat, &defaultFormatObj)) { + return NULL; + } - capabilities->getSupportedColorFormats(&colorFormats); - capabilities->getSupportedProfileLevels(&profileLevels); - sp<AMessage> details = capabilities->getDetails(); - bool isEncoder = info.info->isEncoder(); + jobject infoObj = NULL; + if (ConvertMessageToMap(env, details, &infoObj)) { + env->DeleteLocalRef(defaultFormatObj); + return NULL; + } - jobject defaultFormatObj = NULL; - if (ConvertMessageToMap(env, defaultFormat, &defaultFormatObj)) { - return NULL; - } + jclass capsImplClazz = env->FindClass( + "android/media/MediaCodecInfo$CodecCapabilities$CodecCapsLegacyImpl"); + CHECK(capsImplClazz != NULL); - jobject infoObj = NULL; - if (ConvertMessageToMap(env, details, &infoObj)) { - env->DeleteLocalRef(defaultFormatObj); - return NULL; - } + jclass profileLevelClazz = + env->FindClass("android/media/MediaCodecInfo$CodecProfileLevel"); + CHECK(profileLevelClazz != NULL); - jclass capsClazz = - env->FindClass("android/media/MediaCodecInfo$CodecCapabilities"); - CHECK(capsClazz != NULL); + jobjectArray profileLevelArray = + env->NewObjectArray(profileLevels.size(), profileLevelClazz, NULL); - jclass profileLevelClazz = - env->FindClass("android/media/MediaCodecInfo$CodecProfileLevel"); - CHECK(profileLevelClazz != NULL); + jfieldID profileField = + env->GetFieldID(profileLevelClazz, "profile", "I"); - jobjectArray profileLevelArray = - env->NewObjectArray(profileLevels.size(), profileLevelClazz, NULL); + jfieldID levelField = + env->GetFieldID(profileLevelClazz, "level", "I"); - jfieldID profileField = - env->GetFieldID(profileLevelClazz, "profile", "I"); + for (size_t i = 0; i < profileLevels.size(); ++i) { + const MediaCodecInfo::ProfileLevel &src = profileLevels.itemAt(i); - jfieldID levelField = - env->GetFieldID(profileLevelClazz, "level", "I"); + jobject profileLevelObj = env->AllocObject(profileLevelClazz); - for (size_t i = 0; i < profileLevels.size(); ++i) { - const MediaCodecInfo::ProfileLevel &src = profileLevels.itemAt(i); + env->SetIntField(profileLevelObj, profileField, src.mProfile); + env->SetIntField(profileLevelObj, levelField, src.mLevel); - jobject profileLevelObj = env->AllocObject(profileLevelClazz); + env->SetObjectArrayElement(profileLevelArray, i, profileLevelObj); - env->SetIntField(profileLevelObj, profileField, src.mProfile); - env->SetIntField(profileLevelObj, levelField, src.mLevel); + env->DeleteLocalRef(profileLevelObj); + profileLevelObj = NULL; + } - env->SetObjectArrayElement(profileLevelArray, i, profileLevelObj); + jintArray colorFormatsArray = env->NewIntArray(colorFormats.size()); - env->DeleteLocalRef(profileLevelObj); - profileLevelObj = NULL; - } + for (size_t i = 0; i < colorFormats.size(); ++i) { + jint val = colorFormats.itemAt(i); + env->SetIntArrayRegion(colorFormatsArray, i, 1, &val); + } - jintArray colorFormatsArray = env->NewIntArray(colorFormats.size()); + jmethodID capsImplConstructID = env->GetMethodID(capsImplClazz, "<init>", + "([Landroid/media/MediaCodecInfo$CodecProfileLevel;[IZ" + "Ljava/util/Map;Ljava/util/Map;)V"); - for (size_t i = 0; i < colorFormats.size(); ++i) { - jint val = colorFormats.itemAt(i); - env->SetIntArrayRegion(colorFormatsArray, i, 1, &val); - } + jobject capsImpl = env->NewObject(capsImplClazz, capsImplConstructID, + profileLevelArray, colorFormatsArray, isEncoder, + defaultFormatObj, infoObj); + + jclass capsClazz = env->FindClass( + "android/media/MediaCodecInfo$CodecCapabilities"); + CHECK(capsClazz != NULL); - jmethodID capsConstructID = env->GetMethodID(capsClazz, "<init>", - "([Landroid/media/MediaCodecInfo$CodecProfileLevel;[IZ" - "Ljava/util/Map;Ljava/util/Map;)V"); + jmethodID capsConstructID = env->GetMethodID(capsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsIntf;)V"); - jobject caps = env->NewObject(capsClazz, capsConstructID, - profileLevelArray, colorFormatsArray, isEncoder, - defaultFormatObj, infoObj); + caps = env->NewObject(capsClazz, capsConstructID, capsImpl); - env->DeleteLocalRef(profileLevelArray); - profileLevelArray = NULL; + env->DeleteLocalRef(profileLevelArray); + profileLevelArray = NULL; - env->DeleteLocalRef(colorFormatsArray); - colorFormatsArray = NULL; + env->DeleteLocalRef(colorFormatsArray); + colorFormatsArray = NULL; - env->DeleteLocalRef(defaultFormatObj); - defaultFormatObj = NULL; + env->DeleteLocalRef(defaultFormatObj); + defaultFormatObj = NULL; + + env->DeleteLocalRef(infoObj); + infoObj = NULL; - env->DeleteLocalRef(infoObj); - infoObj = NULL; + env->DeleteLocalRef(capsImpl); + capsImpl = NULL; + } return caps; } diff --git a/media/jni/android_media_MediaPlayer.cpp b/media/jni/android_media_MediaPlayer.cpp index a94230014437..f159282ed2c2 100644 --- a/media/jni/android_media_MediaPlayer.cpp +++ b/media/jni/android_media_MediaPlayer.cpp @@ -1479,6 +1479,7 @@ static int register_android_media_MediaPlayer(JNIEnv *env) extern int register_android_media_ImageReader(JNIEnv *env); extern int register_android_media_ImageWriter(JNIEnv *env); extern int register_android_media_JetPlayer(JNIEnv *env); +extern int register_android_media_CodecCapabilities(JNIEnv *env); extern int register_android_media_Crypto(JNIEnv *env); extern int register_android_media_Drm(JNIEnv *env); extern int register_android_media_Descrambler(JNIEnv *env); @@ -1593,6 +1594,11 @@ jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) goto bail; } + if (register_android_media_CodecCapabilities(env) < 0) { + ALOGE("ERROR: CodecCapabilities native registration failed"); + goto bail; + } + if (register_android_media_Crypto(env) < 0) { ALOGE("ERROR: MediaCodec native registration failed"); goto bail; diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto index f611793b619b..8457d6cd404f 100644 --- a/packages/SettingsLib/Graph/graph.proto +++ b/packages/SettingsLib/Graph/graph.proto @@ -108,6 +108,7 @@ message PreferenceValueProto { oneof value { bool boolean_value = 1; int32 int_value = 2; + float float_value = 3; } } @@ -116,6 +117,7 @@ message PreferenceValueDescriptorProto { oneof type { bool boolean_type = 1; RangeValueProto range_value = 2; + bool float_type = 3; } } diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt index eaa79266b194..d812fbe651e2 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt @@ -42,6 +42,7 @@ import com.android.settingslib.graph.proto.PreferenceProto.ActionTarget import com.android.settingslib.graph.proto.PreferenceScreenProto import com.android.settingslib.graph.proto.TextProto import com.android.settingslib.metadata.BooleanValue +import com.android.settingslib.metadata.FloatPersistentPreference import com.android.settingslib.metadata.PersistentPreference import com.android.settingslib.metadata.PreferenceAvailabilityProvider import com.android.settingslib.metadata.PreferenceHierarchy @@ -399,15 +400,13 @@ fun PreferenceMetadata.toProto( metadata is PersistentPreference<*> && metadata.evalReadPermit(context, callingPid, callingUid) == ReadWritePermit.ALLOW ) { + val storage = metadata.storage(context) value = preferenceValueProto { when (metadata) { - is BooleanValue -> - metadata.storage(context).getBoolean(metadata.key)?.let { - booleanValue = it - } - is RangeValue -> { - metadata.storage(context).getInt(metadata.key)?.let { intValue = it } - } + is BooleanValue -> storage.getBoolean(metadata.key)?.let { booleanValue = it } + is RangeValue -> storage.getInt(metadata.key)?.let { intValue = it } + is FloatPersistentPreference -> + storage.getFloat(metadata.key)?.let { floatValue = it } else -> {} } } @@ -421,6 +420,7 @@ fun PreferenceMetadata.toProto( max = metadata.getMaxValue(context) step = metadata.getIncrementStep(context) } + is FloatPersistentPreference -> floatType = true else -> {} } } diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt index d72ba0805db3..83c430488317 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt @@ -159,6 +159,12 @@ class PreferenceSetterApiHandler( } storage.setInt(key, intValue) return PreferenceSetterResult.OK + } else if (value.hasFloatValue()) { + val floatValue = value.floatValue + val resultCode = metadata.checkWritePermit(floatValue) + if (resultCode != PreferenceSetterResult.OK) return resultCode + storage.setFloat(key, floatValue) + return PreferenceSetterResult.OK } } catch (e: Exception) { return PreferenceSetterResult.INTERNAL_ERROR diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt index d3a731688c24..3dd6c47833fd 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt @@ -201,3 +201,6 @@ interface RangeValue : ValueDescriptor { override fun isValidValue(context: Context, index: Int) = index in getMinValue(context)..getMaxValue(context) } + +/** A persistent preference that has a float value. */ +interface FloatPersistentPreference : PersistentPreference<Float> diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java index 03a2101544be..218983a55e1b 100644 --- a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java +++ b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java @@ -31,7 +31,6 @@ import androidx.preference.CheckBoxPreference; import androidx.preference.PreferenceViewHolder; import com.android.settingslib.widget.preference.selector.R; -import com.android.settingslib.widget.selectorwithwidgetpreference.flags.Flags; /** * Selector preference (checkbox or radio button) with an optional additional widget. @@ -180,10 +179,8 @@ public class SelectorWithWidgetPreference extends CheckBoxPreference { : getContext().getString(R.string.settings_label)); } - if (Flags.allowSetTitleMaxLines()) { - TextView title = (TextView) holder.findViewById(android.R.id.title); - title.setMaxLines(mTitleMaxLines); - } + TextView title = (TextView) holder.findViewById(android.R.id.title); + title.setMaxLines(mTitleMaxLines); } /** @@ -244,16 +241,12 @@ public class SelectorWithWidgetPreference extends CheckBoxPreference { setLayoutResource(R.layout.preference_selector_with_widget); setIconSpaceReserved(false); - if (Flags.allowSetTitleMaxLines()) { - final TypedArray a = - context.obtainStyledAttributes( - attrs, R.styleable.SelectorWithWidgetPreference, defStyleAttr, - defStyleRes); - mTitleMaxLines = - a.getInt(R.styleable.SelectorWithWidgetPreference_titleMaxLines, - DEFAULT_MAX_LINES); - a.recycle(); - } + final TypedArray a = + context.obtainStyledAttributes( + attrs, R.styleable.SelectorWithWidgetPreference, defStyleAttr, defStyleRes); + mTitleMaxLines = + a.getInt(R.styleable.SelectorWithWidgetPreference_titleMaxLines, DEFAULT_MAX_LINES); + a.recycle(); } @VisibleForTesting(otherwise = VisibleForTesting.NONE) diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig index 1a043d5015b2..cc996c5a2120 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -97,6 +97,7 @@ flag { name: "settings_catalyst" namespace: "android_settings" description: "Settings catalyst project migration" + is_exported: true bug: "323791114" is_exported: true } @@ -106,6 +107,7 @@ flag { is_fixed_read_only: true namespace: "android_settings" description: "Enable WRITE_SYSTEM_PREFERENCE permission and appop" + is_exported: true bug: "375193223" is_exported: true } @@ -197,3 +199,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "disable_audio_sharing_auto_pick_fallback_in_ui" + namespace: "cross_device_experiences" + description: "Do not auto pick audio sharing fallback device in UI" + bug: "383469911" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index e1929b725a58..6cf9e83ef342 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -229,6 +229,8 @@ <string name="bluetooth_hearing_aid_right_active">Active (right only)</string> <!-- Connected device settings. Message when the left-side and right-side hearing aids device are active. [CHAR LIMIT=NONE] --> <string name="bluetooth_hearing_aid_left_and_right_active">Active (left and right)</string> + <!-- Connected device settings.: Message when changing remote ambient state failed. [CHAR LIMIT=NONE] --> + <string name="bluetooth_hearing_device_ambient_error">Couldn\u2019t update surroundings</string> <!-- Connected devices settings. Message when Bluetooth is connected and active for media only, showing remote device status and battery level. [CHAR LIMIT=NONE] --> <string name="bluetooth_active_media_only_battery_level">Active (media only). <xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery.</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUi.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUi.java new file mode 100644 index 000000000000..881a97bfadcd --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUi.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.bluetooth; + +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; +import java.util.Map; + +/** Interface for the ambient volume UI. */ +public interface AmbientVolumeUi { + + /** Interface definition for a callback to be invoked when event happens in AmbientVolumeUi. */ + interface AmbientVolumeUiListener { + /** Called when the expand icon is clicked. */ + void onExpandIconClick(); + + /** Called when the ambient volume icon is clicked. */ + void onAmbientVolumeIconClick(); + + /** Called when the slider of the specified side is changed. */ + void onSliderValueChange(int side, int value); + }; + + /** The rotation degree of the expand icon when the UI is in collapsed mode. */ + float ROTATION_COLLAPSED = 0f; + /** The rotation degree of the expand icon when the UI is in expanded mode. */ + float ROTATION_EXPANDED = 180f; + + /** + * The default ambient volume level for hearing device ambient volume icon + * + * <p> This icon visually represents the current ambient volume. It displays separate + * levels for the left and right sides, each with 5 levels ranging from 0 to 4. + * + * <p> To represent the combined left/right levels with a single value, the following + * calculation is used: + * finalLevel = (leftLevel * 5) + rightLevel + * For example: + * <ul> + * <li>If left level is 2 and right level is 3, the final level will be 13 (2 * 5 + 3)</li> + * <li>If both left and right levels are 0, the final level will be 0</li> + * <li>If both left and right levels are 4, the final level will be 24</li> + * </ul> + */ + int AMBIENT_VOLUME_LEVEL_DEFAULT = 24; + /** + * The minimum ambient volume level for hearing device ambient volume icon + * + * @see #AMBIENT_VOLUME_LEVEL_DEFAULT + */ + int AMBIENT_VOLUME_LEVEL_MIN = 0; + /** + * The maximum ambient volume level for hearing device ambient volume icon + * + * @see #AMBIENT_VOLUME_LEVEL_DEFAULT + */ + int AMBIENT_VOLUME_LEVEL_MAX = 24; + + /** + * Ths side identifier for slider in collapsed mode which can unified control the ambient + * volume of all devices in the same set. + */ + int SIDE_UNIFIED = 999; + + /** All valid side of the sliders in the UI. */ + List<Integer> VALID_SIDES = List.of(SIDE_UNIFIED, SIDE_LEFT, SIDE_RIGHT); + + /** Sets if the UI is visible. */ + void setVisible(boolean visible); + + /** + * Sets if the UI is expandable between expanded and collapsed mode. + * + * <p> If the UI is not expandable, it implies the UI will always stay in collapsed mode + */ + void setExpandable(boolean expandable); + + /** @return if the UI is expandable. */ + boolean isExpandable(); + + /** Sets if the UI is in expanded mode. */ + void setExpanded(boolean expanded); + + /** @return if the UI is in expanded mode. */ + boolean isExpanded(); + + /** + * Sets if the UI is capable to mute the ambient of the remote device. + * + * <p> If the value is {@code false}, it implies the remote device ambient will always be + * unmute and can not be mute from the UI + */ + void setMutable(boolean mutable); + + /** @return if the UI is capable to mute the ambient of remote device. */ + boolean isMutable(); + + /** Sets if the UI shows mute state. */ + void setMuted(boolean muted); + + /** @return if the UI shows mute state */ + boolean isMuted(); + + /** + * Sets listener on the UI. + * + * @see AmbientVolumeUiListener + */ + void setListener(@Nullable AmbientVolumeUiListener listener); + + /** + * Sets up sliders in the UI. + * + * <p> For each side of device, the UI should hava a corresponding slider to control it's + * ambient volume. + * <p> For all devices in the same set, the UI should have a slider to control all devices' + * ambient volume at once. + * @param sideToDeviceMap the side and device mapping of all devices in the same set + */ + void setupSliders(@NonNull Map<Integer, BluetoothDevice> sideToDeviceMap); + + /** + * Sets if the slider is enabled. + * + * @param side the side of the slider + * @param enabled the enabled state + */ + void setSliderEnabled(int side, boolean enabled); + + /** + * Sets the slider value. + * + * @param side the side of the slider + * @param value the ambient value + */ + void setSliderValue(int side, int value); + + /** + * Sets the slider's minimum and maximum value. + * + * @param side the side of the slider + * @param min the minimum ambient value + * @param max the maximum ambient value + */ + void setSliderRange(int side, int min, int max); + + /** Updates the UI according to current state. */ + void updateLayout(); +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java new file mode 100644 index 000000000000..ce392b12516f --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java @@ -0,0 +1,527 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.bluetooth; + +import static android.bluetooth.AudioInputControl.MUTE_NOT_MUTED; +import static android.bluetooth.AudioInputControl.MUTE_MUTED; +import static android.bluetooth.BluetoothDevice.BOND_BONDED; + +import static com.android.settingslib.bluetooth.AmbientVolumeUi.SIDE_UNIFIED; +import static com.android.settingslib.bluetooth.AmbientVolumeUi.VALID_SIDES; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_INVALID; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; +import static com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data.INVALID_VOLUME; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.util.ArraySet; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.settingslib.R; +import com.android.settingslib.utils.ThreadUtils; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; + +import java.util.Map; +import java.util.Set; + +/** This class controls ambient volume UI with local and remote ambient data. */ +public class AmbientVolumeUiController implements + HearingDeviceLocalDataManager.OnDeviceLocalDataChangeListener, + AmbientVolumeController.AmbientVolumeControlCallback, + AmbientVolumeUi.AmbientVolumeUiListener, BluetoothCallback, CachedBluetoothDevice.Callback { + + private static final boolean DEBUG = true; + private static final String TAG = "AmbientVolumeUiController"; + + private final Context mContext; + private final LocalBluetoothProfileManager mProfileManager; + private final BluetoothEventManager mEventManager; + private final AmbientVolumeUi mAmbientLayout; + private final AmbientVolumeController mVolumeController; + private final HearingDeviceLocalDataManager mLocalDataManager; + + private final Set<CachedBluetoothDevice> mCachedDevices = new ArraySet<>(); + private final BiMap<Integer, BluetoothDevice> mSideToDeviceMap = HashBiMap.create(); + private CachedBluetoothDevice mCachedDevice; + private boolean mShowUiWhenLocalDataExist = true; + + public AmbientVolumeUiController(@NonNull Context context, + @NonNull LocalBluetoothManager bluetoothManager, + @NonNull AmbientVolumeUi ambientLayout) { + mContext = context; + mProfileManager = bluetoothManager.getProfileManager(); + mEventManager = bluetoothManager.getEventManager(); + mAmbientLayout = ambientLayout; + mAmbientLayout.setListener(this); + mVolumeController = new AmbientVolumeController(mProfileManager, this); + mLocalDataManager = new HearingDeviceLocalDataManager(context); + mLocalDataManager.setOnDeviceLocalDataChangeListener(this, + ThreadUtils.getBackgroundExecutor()); + } + + @VisibleForTesting + public AmbientVolumeUiController(@NonNull Context context, + @NonNull LocalBluetoothManager bluetoothManager, + @NonNull AmbientVolumeUi ambientLayout, + @NonNull AmbientVolumeController volumeController, + @NonNull HearingDeviceLocalDataManager localDataManager) { + mContext = context; + mProfileManager = bluetoothManager.getProfileManager(); + mEventManager = bluetoothManager.getEventManager(); + mAmbientLayout = ambientLayout; + mVolumeController = volumeController; + mLocalDataManager = localDataManager; + } + + + @Override + public void onDeviceLocalDataChange(@NonNull String address, + @Nullable HearingDeviceLocalDataManager.Data data) { + if (data == null) { + // The local data is removed because the device is unpaired, do nothing + return; + } + if (DEBUG) { + Log.d(TAG, "onDeviceLocalDataChange, address:" + address + ", data:" + data); + } + for (BluetoothDevice device : mSideToDeviceMap.values()) { + if (device.getAnonymizedAddress().equals(address)) { + postOnMainThread(() -> loadLocalDataToUi(device)); + return; + } + } + } + + @Override + public void onVolumeControlServiceConnected() { + mCachedDevices.forEach(device -> mVolumeController.registerCallback( + ThreadUtils.getBackgroundExecutor(), device.getDevice())); + } + + @Override + public void onAmbientChanged(@NonNull BluetoothDevice device, int gainSettings) { + if (DEBUG) { + Log.d(TAG, "onAmbientChanged, value:" + gainSettings + ", device:" + device); + } + HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(device); + final boolean expanded = mAmbientLayout.isExpanded(); + final boolean isInitiatedFromUi = (expanded && data.ambient() == gainSettings) + || (!expanded && data.groupAmbient() == gainSettings); + if (isInitiatedFromUi) { + // The change is initiated from UI, no need to update UI + return; + } + + // We have to check if we need to expand the controls by getting all remote + // device's ambient value, delay for a while to wait all remote devices update + // to the latest value to avoid unnecessary expand action. + postDelayedOnMainThread(this::refresh, 1200L); + } + + @Override + public void onMuteChanged(@NonNull BluetoothDevice device, int mute) { + if (DEBUG) { + Log.d(TAG, "onMuteChanged, mute:" + mute + ", device:" + device); + } + final boolean muted = mAmbientLayout.isMuted(); + boolean isInitiatedFromUi = (muted && mute == MUTE_MUTED) + || (!muted && mute == MUTE_NOT_MUTED); + if (isInitiatedFromUi) { + // The change is initiated from UI, no need to update UI + return; + } + + // We have to check if we need to mute the devices by getting all remote + // device's mute state, delay for a while to wait all remote devices update + // to the latest value. + postDelayedOnMainThread(this::refresh, 1200L); + } + + @Override + public void onCommandFailed(@NonNull BluetoothDevice device) { + Log.w(TAG, "onCommandFailed, device:" + device); + postOnMainThread(() -> { + showErrorToast(R.string.bluetooth_hearing_device_ambient_error); + refresh(); + }); + } + + @Override + public void onExpandIconClick() { + mSideToDeviceMap.forEach((s, d) -> { + if (!mAmbientLayout.isMuted()) { + // Apply previous collapsed/expanded volume to remote device + HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(d); + int volume = mAmbientLayout.isExpanded() + ? data.ambient() : data.groupAmbient(); + mVolumeController.setAmbient(d, volume); + } + // Update new value to local data + mLocalDataManager.updateAmbientControlExpanded(d, + mAmbientLayout.isExpanded()); + }); + mLocalDataManager.flush(); + } + + @Override + public void onAmbientVolumeIconClick() { + if (!mAmbientLayout.isMuted()) { + loadLocalDataToUi(); + } + for (BluetoothDevice device : mSideToDeviceMap.values()) { + mVolumeController.setMuted(device, mAmbientLayout.isMuted()); + } + } + + @Override + public void onSliderValueChange(int side, int value) { + if (DEBUG) { + Log.d(TAG, "onSliderValueChange: side=" + side + ", value=" + value); + } + setVolumeIfValid(side, value); + + Runnable setAmbientRunnable = () -> { + if (side == SIDE_UNIFIED) { + mSideToDeviceMap.forEach((s, d) -> mVolumeController.setAmbient(d, value)); + } else { + final BluetoothDevice device = mSideToDeviceMap.get(side); + mVolumeController.setAmbient(device, value); + } + }; + + if (mAmbientLayout.isMuted()) { + // User drag on the volume slider when muted. Unmute the devices first. + mAmbientLayout.setMuted(false); + + for (BluetoothDevice device : mSideToDeviceMap.values()) { + mVolumeController.setMuted(device, false); + } + // Restore the value before muted + loadLocalDataToUi(); + // Delay set ambient on remote device since the immediately sequential command + // might get failed sometimes + postDelayedOnMainThread(setAmbientRunnable, 1000L); + } else { + setAmbientRunnable.run(); + } + } + + @Override + public void onProfileConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice, + int state, int bluetoothProfile) { + if (bluetoothProfile == BluetoothProfile.VOLUME_CONTROL + && state == BluetoothProfile.STATE_CONNECTED + && mCachedDevices.contains(cachedDevice)) { + // After VCP connected, AICS may not ready yet and still return invalid value, delay + // a while to wait AICS ready as a workaround + postDelayedOnMainThread(this::refresh, 1000L); + } + } + + @Override + public void onDeviceAttributesChanged() { + mCachedDevices.forEach(device -> { + device.unregisterCallback(this); + mVolumeController.unregisterCallback(device.getDevice()); + }); + postOnMainThread(()-> { + loadDevice(mCachedDevice); + ThreadUtils.postOnBackgroundThread(()-> { + mCachedDevices.forEach(device -> { + device.registerCallback(ThreadUtils.getBackgroundExecutor(), this); + mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(), + device.getDevice()); + }); + }); + }); + } + + /** + * Registers callbacks and listeners, this should be called when needs to start listening to + * events. + */ + public void start() { + mEventManager.registerCallback(this); + mLocalDataManager.start(); + mCachedDevices.forEach(device -> { + device.registerCallback(ThreadUtils.getBackgroundExecutor(), this); + mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(), + device.getDevice()); + }); + } + + /** + * Unregisters callbacks and listeners, this should be called when no longer needs to listen to + * events. + */ + public void stop() { + mEventManager.unregisterCallback(this); + mLocalDataManager.stop(); + mCachedDevices.forEach(device -> { + device.unregisterCallback(this); + mVolumeController.unregisterCallback(device.getDevice()); + }); + } + + /** + * Loads all devices in the same set with {@code cachedDevice} and create corresponding sliders. + * + * <p>If the devices has valid ambient control points, the ambient volume UI will be visible. + * @param cachedDevice the remote device + */ + public void loadDevice(CachedBluetoothDevice cachedDevice) { + if (DEBUG) { + Log.d(TAG, "loadDevice, device=" + cachedDevice); + } + mCachedDevice = cachedDevice; + mSideToDeviceMap.clear(); + mCachedDevices.clear(); + boolean deviceSupportVcp = + cachedDevice != null && cachedDevice.getProfiles().stream().anyMatch( + p -> p instanceof VolumeControlProfile); + if (!deviceSupportVcp) { + mAmbientLayout.setVisible(false); + return; + } + + // load devices in the same set + if (VALID_SIDES.contains(cachedDevice.getDeviceSide()) + && cachedDevice.getBondState() == BOND_BONDED) { + mSideToDeviceMap.put(cachedDevice.getDeviceSide(), cachedDevice.getDevice()); + mCachedDevices.add(cachedDevice); + } + for (CachedBluetoothDevice memberDevice : cachedDevice.getMemberDevice()) { + if (VALID_SIDES.contains(memberDevice.getDeviceSide()) + && memberDevice.getBondState() == BOND_BONDED) { + mSideToDeviceMap.put(memberDevice.getDeviceSide(), memberDevice.getDevice()); + mCachedDevices.add(memberDevice); + } + } + + mAmbientLayout.setExpandable(mSideToDeviceMap.size() > 1); + mAmbientLayout.setupSliders(mSideToDeviceMap); + refresh(); + } + + /** Refreshes the ambient volume UI. */ + public void refresh() { + if (isAmbientControlAvailable()) { + mAmbientLayout.setVisible(true); + loadRemoteDataToUi(); + } else { + mAmbientLayout.setVisible(false); + } + } + + /** Sets if the ambient volume UI should be visible when local ambient data exist. */ + public void setShowUiWhenLocalDataExist(boolean shouldShow) { + mShowUiWhenLocalDataExist = shouldShow; + } + + /** Updates the ambient sliders according to current state. */ + private void updateSliderUi() { + boolean isAnySliderEnabled = false; + for (Map.Entry<Integer, BluetoothDevice> entry : mSideToDeviceMap.entrySet()) { + final int side = entry.getKey(); + final BluetoothDevice device = entry.getValue(); + final boolean enabled = isDeviceConnectedToVcp(device) + && mVolumeController.isAmbientControlAvailable(device); + isAnySliderEnabled |= enabled; + mAmbientLayout.setSliderEnabled(side, enabled); + } + mAmbientLayout.setSliderEnabled(SIDE_UNIFIED, isAnySliderEnabled); + mAmbientLayout.updateLayout(); + } + + /** Sets the ambient to the corresponding control slider. */ + private void setVolumeIfValid(int side, int volume) { + if (volume == INVALID_VOLUME) { + return; + } + mAmbientLayout.setSliderValue(side, volume); + // Update new value to local data + if (side == SIDE_UNIFIED) { + mSideToDeviceMap.forEach((s, d) -> mLocalDataManager.updateGroupAmbient(d, volume)); + } else { + mLocalDataManager.updateAmbient(mSideToDeviceMap.get(side), volume); + } + mLocalDataManager.flush(); + } + + private void loadLocalDataToUi() { + mSideToDeviceMap.forEach((s, d) -> loadLocalDataToUi(d)); + } + + private void loadLocalDataToUi(BluetoothDevice device) { + final HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(device); + if (DEBUG) { + Log.d(TAG, "loadLocalDataToUi, data=" + data + ", device=" + device); + } + if (isDeviceConnectedToVcp(device) && !mAmbientLayout.isMuted()) { + final int side = mSideToDeviceMap.inverse().getOrDefault(device, SIDE_INVALID); + setVolumeIfValid(side, data.ambient()); + setVolumeIfValid(SIDE_UNIFIED, data.groupAmbient()); + } + setAmbientControlExpanded(data.ambientControlExpanded()); + updateSliderUi(); + } + + private void loadRemoteDataToUi() { + BluetoothDevice leftDevice = mSideToDeviceMap.get(SIDE_LEFT); + AmbientVolumeController.RemoteAmbientState leftState = + mVolumeController.refreshAmbientState(leftDevice); + BluetoothDevice rightDevice = mSideToDeviceMap.get(SIDE_RIGHT); + AmbientVolumeController.RemoteAmbientState rightState = + mVolumeController.refreshAmbientState(rightDevice); + if (DEBUG) { + Log.d(TAG, "loadRemoteDataToUi, left=" + leftState + ", right=" + rightState); + } + mSideToDeviceMap.forEach((side, device) -> { + int ambientMax = mVolumeController.getAmbientMax(device); + int ambientMin = mVolumeController.getAmbientMin(device); + if (ambientMin != ambientMax) { + mAmbientLayout.setSliderRange(side, ambientMin, ambientMax); + mAmbientLayout.setSliderRange(SIDE_UNIFIED, ambientMin, ambientMax); + } + }); + + // Update ambient volume + final int leftAmbient = leftState != null ? leftState.gainSetting() : INVALID_VOLUME; + final int rightAmbient = rightState != null ? rightState.gainSetting() : INVALID_VOLUME; + if (mAmbientLayout.isExpanded()) { + setVolumeIfValid(SIDE_LEFT, leftAmbient); + setVolumeIfValid(SIDE_RIGHT, rightAmbient); + } else { + if (leftAmbient != rightAmbient && leftAmbient != INVALID_VOLUME + && rightAmbient != INVALID_VOLUME) { + setVolumeIfValid(SIDE_LEFT, leftAmbient); + setVolumeIfValid(SIDE_RIGHT, rightAmbient); + setAmbientControlExpanded(true); + } else { + int unifiedAmbient = leftAmbient != INVALID_VOLUME ? leftAmbient : rightAmbient; + setVolumeIfValid(SIDE_UNIFIED, unifiedAmbient); + } + } + // Initialize local data between side and group value + initLocalAmbientDataIfNeeded(); + + // Update mute state + boolean mutable = true; + boolean muted = true; + if (isDeviceConnectedToVcp(leftDevice) && leftState != null) { + mutable &= leftState.isMutable(); + muted &= leftState.isMuted(); + } + if (isDeviceConnectedToVcp(rightDevice) && rightState != null) { + mutable &= rightState.isMutable(); + muted &= rightState.isMuted(); + } + mAmbientLayout.setMutable(mutable); + mAmbientLayout.setMuted(muted); + + // Ensure remote device mute state is synced + syncMuteStateIfNeeded(leftDevice, leftState, muted); + syncMuteStateIfNeeded(rightDevice, rightState, muted); + + updateSliderUi(); + } + + private void setAmbientControlExpanded(boolean expanded) { + mAmbientLayout.setExpanded(expanded); + mSideToDeviceMap.forEach((s, d) -> { + // Update new value to local data + mLocalDataManager.updateAmbientControlExpanded(d, expanded); + }); + mLocalDataManager.flush(); + } + + /** Checks if any device in the same set has valid ambient control points */ + private boolean isAmbientControlAvailable() { + for (BluetoothDevice device : mSideToDeviceMap.values()) { + if (mShowUiWhenLocalDataExist) { + // Found local ambient data + if (mLocalDataManager.get(device).hasAmbientData()) { + return true; + } + } + // Found remote ambient control points + if (mVolumeController.isAmbientControlAvailable(device)) { + return true; + } + } + return false; + } + + private void initLocalAmbientDataIfNeeded() { + int smallerVolumeAmongGroup = Integer.MAX_VALUE; + for (BluetoothDevice device : mSideToDeviceMap.values()) { + HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(device); + if (data.ambient() != INVALID_VOLUME) { + smallerVolumeAmongGroup = Math.min(data.ambient(), smallerVolumeAmongGroup); + } else if (data.groupAmbient() != INVALID_VOLUME) { + // Initialize side ambient from group ambient value + mLocalDataManager.updateAmbient(device, data.groupAmbient()); + } + } + if (smallerVolumeAmongGroup != Integer.MAX_VALUE) { + for (BluetoothDevice device : mSideToDeviceMap.values()) { + HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(device); + if (data.groupAmbient() == INVALID_VOLUME) { + // Initialize group ambient from smaller side ambient value + mLocalDataManager.updateGroupAmbient(device, smallerVolumeAmongGroup); + } + } + } + mLocalDataManager.flush(); + } + + private void syncMuteStateIfNeeded(@Nullable BluetoothDevice device, + @Nullable AmbientVolumeController.RemoteAmbientState state, boolean muted) { + if (isDeviceConnectedToVcp(device) && state != null && state.isMutable()) { + if (state.isMuted() != muted) { + mVolumeController.setMuted(device, muted); + } + } + } + + private boolean isDeviceConnectedToVcp(@Nullable BluetoothDevice device) { + return device != null && device.isConnected() + && mProfileManager.getVolumeControlProfile().getConnectionStatus(device) + == BluetoothProfile.STATE_CONNECTED; + } + + private void postOnMainThread(Runnable runnable) { + mContext.getMainThreadHandler().post(runnable); + } + + private void postDelayedOnMainThread(Runnable runnable, long delay) { + mContext.getMainThreadHandler().postDelayed(runnable, delay); + } + + private void showErrorToast(int stringResId) { + Toast.makeText(mContext, stringResId, Toast.LENGTH_SHORT).show(); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManager.java index 6725558cd2bd..3cd37320243f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManager.java @@ -148,6 +148,14 @@ public class HearingDeviceLocalDataManager { } } + /** Flushes the data into Settings . */ + public synchronized void flush() { + if (!mIsStarted) { + return; + } + putAmbientVolumeSettings(); + } + /** * Puts the local data of the corresponding hearing device. * @@ -274,9 +282,6 @@ public class HearingDeviceLocalDataManager { notifyIfDataChanged(mAddrToDataMap, updatedAddrToDataMap); mAddrToDataMap.clear(); mAddrToDataMap.putAll(updatedAddrToDataMap); - if (DEBUG) { - Log.v(TAG, "getLocalDataFromSettings, " + mAddrToDataMap + ", manager: " + this); - } } } @@ -287,12 +292,10 @@ public class HearingDeviceLocalDataManager { builder.append(KEY_ADDR).append("=").append(entry.getKey()); builder.append(entry.getValue().toSettingsFormat()).append(";"); } - if (DEBUG) { - Log.v(TAG, "putAmbientVolumeSettings, " + builder + ", manager: " + this); - } - Settings.Global.putStringForUser(mContext.getContentResolver(), - LOCAL_AMBIENT_VOLUME_SETTINGS, builder.toString(), - UserHandle.USER_SYSTEM); + ThreadUtils.postOnBackgroundThread(() -> { + Settings.Global.putStringForUser(mContext.getContentResolver(), + LOCAL_AMBIENT_VOLUME_SETTINGS, builder.toString(), UserHandle.USER_SYSTEM); + }); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index b52ed42d567f..2c99a2d4818c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -101,6 +101,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { public @interface BroadcastState {} private static final String SETTINGS_PKG = "com.android.settings"; + private static final String SYSUI_PKG = "com.android.systemui"; private static final String TAG = "LocalBluetoothLeBroadcast"; private static final boolean DEBUG = BluetoothUtils.D; @@ -216,6 +217,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } setLatestBroadcastId(broadcastId); setAppSourceName(mNewAppSourceName, /* updateContentResolver= */ true); + notifyBroadcastStateChange(BROADCAST_STATE_ON); } @Override @@ -232,7 +234,6 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId); } setLatestBluetoothLeBroadcastMetadata(metadata); - notifyBroadcastStateChange(BROADCAST_STATE_ON); } @Override @@ -1247,8 +1248,9 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } private void notifyBroadcastStateChange(@BroadcastState int state) { - if (!mContext.getPackageName().equals(SETTINGS_PKG)) { - Log.d(TAG, "Skip notifyBroadcastStateChange, not triggered by Settings."); + String packageName = mContext.getPackageName(); + if (!packageName.equals(SETTINGS_PKG) && !packageName.equals(SYSUI_PKG)) { + Log.d(TAG, "Skip notifyBroadcastStateChange, not triggered by Settings or SystemUI."); return; } if (isWorkProfile(mContext)) { @@ -1257,8 +1259,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE); intent.putExtra(EXTRA_LE_AUDIO_SHARING_STATE, state); - intent.setPackage(mContext.getPackageName()); - Log.d(TAG, "notifyBroadcastStateChange for state = " + state); + intent.setPackage(SETTINGS_PKG); + Log.d(TAG, "notifyBroadcastStateChange for state = " + state + " by pkg = " + packageName); mContext.sendBroadcast(intent); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/AmbientVolumeUiControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/AmbientVolumeUiControllerTest.java new file mode 100644 index 000000000000..8b606e299971 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/AmbientVolumeUiControllerTest.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.bluetooth; + +import static android.bluetooth.AudioInputControl.MUTE_DISABLED; +import static android.bluetooth.AudioInputControl.MUTE_MUTED; +import static android.bluetooth.AudioInputControl.MUTE_NOT_MUTED; +import static android.bluetooth.BluetoothDevice.BOND_BONDED; + +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.robolectric.Shadows.shadowOf; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; + +/** Tests for {@link AmbientVolumeUiController}. */ +@RunWith(RobolectricTestRunner.class) +public class AmbientVolumeUiControllerTest { + + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + private static final String TEST_ADDRESS = "00:00:00:00:11"; + private static final String TEST_MEMBER_ADDRESS = "00:00:00:00:22"; + + @Mock + LocalBluetoothManager mBluetoothManager; + @Mock + LocalBluetoothProfileManager mProfileManager; + @Mock + BluetoothEventManager mEventManager; + @Mock + VolumeControlProfile mVolumeControlProfile; + @Mock + AmbientVolumeUi mAmbientLayout; + @Mock + private AmbientVolumeController mVolumeController; + @Mock + private HearingDeviceLocalDataManager mLocalDataManager; + @Mock + private CachedBluetoothDevice mCachedDevice; + @Mock + private CachedBluetoothDevice mCachedMemberDevice; + @Mock + private BluetoothDevice mDevice; + @Mock + private BluetoothDevice mMemberDevice; + @Mock + private Handler mTestHandler; + + @Spy + private final Context mContext = ApplicationProvider.getApplicationContext(); + private AmbientVolumeUiController mController; + + @Before + public void setUp() { + when(mBluetoothManager.getProfileManager()).thenReturn(mProfileManager); + when(mBluetoothManager.getEventManager()).thenReturn(mEventManager); + + mController = spy(new AmbientVolumeUiController(mContext, mBluetoothManager, + mAmbientLayout, mVolumeController, mLocalDataManager)); + + when(mProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControlProfile); + when(mVolumeControlProfile.getConnectionStatus(mDevice)).thenReturn( + BluetoothProfile.STATE_CONNECTED); + when(mVolumeControlProfile.getConnectionStatus(mMemberDevice)).thenReturn( + BluetoothProfile.STATE_CONNECTED); + when(mVolumeController.isAmbientControlAvailable(mDevice)).thenReturn(true); + when(mVolumeController.isAmbientControlAvailable(mMemberDevice)).thenReturn(true); + when(mLocalDataManager.get(any(BluetoothDevice.class))).thenReturn( + new HearingDeviceLocalDataManager.Data.Builder().build()); + + when(mContext.getMainThreadHandler()).thenReturn(mTestHandler); + Answer<Object> answer = invocationOnMock -> { + invocationOnMock.getArgument(0, Runnable.class).run(); + return null; + }; + when(mTestHandler.post(any(Runnable.class))).thenAnswer(answer); + when(mTestHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer(answer); + + prepareDevice(/* hasMember= */ true); + mController.loadDevice(mCachedDevice); + Mockito.reset(mController); + Mockito.reset(mAmbientLayout); + } + + @Test + public void loadDevice_deviceWithoutMember_controlNotExpandable() { + prepareDevice(/* hasMember= */ false); + + mController.loadDevice(mCachedDevice); + + verify(mAmbientLayout).setExpandable(false); + } + + @Test + public void loadDevice_deviceWithMember_controlExpandable() { + prepareDevice(/* hasMember= */ true); + + mController.loadDevice(mCachedDevice); + + verify(mAmbientLayout).setExpandable(true); + } + + @Test + public void loadDevice_deviceNotSupportVcp_ambientLayoutGone() { + when(mCachedDevice.getProfiles()).thenReturn(List.of()); + + mController.loadDevice(mCachedDevice); + + verify(mAmbientLayout).setVisible(false); + } + + @Test + public void loadDevice_ambientControlNotAvailable_ambientLayoutGone() { + when(mVolumeController.isAmbientControlAvailable(mDevice)).thenReturn(false); + when(mVolumeController.isAmbientControlAvailable(mMemberDevice)).thenReturn(false); + + mController.loadDevice(mCachedDevice); + + verify(mAmbientLayout).setVisible(false); + } + + @Test + public void loadDevice_supportVcpAndAmbientControlAvailable_ambientLayoutVisible() { + when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); + when(mVolumeController.isAmbientControlAvailable(mDevice)).thenReturn(true); + + mController.loadDevice(mCachedDevice); + + verify(mAmbientLayout).setVisible(true); + } + + @Test + public void start_callbackRegistered() { + mController.start(); + + verify(mEventManager).registerCallback(mController); + verify(mLocalDataManager).start(); + verify(mVolumeController).registerCallback(any(Executor.class), eq(mDevice)); + verify(mVolumeController).registerCallback(any(Executor.class), eq(mMemberDevice)); + verify(mCachedDevice).registerCallback(any(Executor.class), + any(CachedBluetoothDevice.Callback.class)); + verify(mCachedMemberDevice).registerCallback(any(Executor.class), + any(CachedBluetoothDevice.Callback.class)); + } + + @Test + public void stop_callbackUnregistered() { + mController.stop(); + + verify(mEventManager).unregisterCallback(mController); + verify(mLocalDataManager).stop(); + verify(mVolumeController).unregisterCallback(mDevice); + verify(mVolumeController).unregisterCallback(mMemberDevice); + verify(mCachedDevice).unregisterCallback(any(CachedBluetoothDevice.Callback.class)); + verify(mCachedMemberDevice).unregisterCallback(any(CachedBluetoothDevice.Callback.class)); + } + + @Test + public void onDeviceLocalDataChange_verifySetExpandedAndDataUpdated() { + final boolean testExpanded = true; + HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder() + .ambient(0).groupAmbient(0).ambientControlExpanded(testExpanded).build(); + when(mLocalDataManager.get(mDevice)).thenReturn(data); + + mController.onDeviceLocalDataChange(TEST_ADDRESS, data); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mAmbientLayout).setExpanded(testExpanded); + verifyDeviceDataUpdated(mDevice); + } + + @Test + public void onAmbientChanged_refreshWhenNotInitiateFromUi() { + HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder() + .ambient(10).groupAmbient(10).ambientControlExpanded(true).build(); + when(mLocalDataManager.get(mDevice)).thenReturn(data); + when(mAmbientLayout.isExpanded()).thenReturn(true); + + mController.onAmbientChanged(mDevice, 10); + verify(mController, never()).refresh(); + + mController.onAmbientChanged(mDevice, 20); + verify(mController).refresh(); + } + + @Test + public void onMuteChanged_refreshWhenNotInitiateFromUi() { + AmbientVolumeController.RemoteAmbientState state = + new AmbientVolumeController.RemoteAmbientState(MUTE_NOT_MUTED, 0); + when(mVolumeController.refreshAmbientState(mDevice)).thenReturn(state); + when(mAmbientLayout.isExpanded()).thenReturn(false); + + mController.onMuteChanged(mDevice, MUTE_NOT_MUTED); + verify(mController, never()).refresh(); + + mController.onMuteChanged(mDevice, MUTE_MUTED); + verify(mController).refresh(); + } + + @Test + public void refresh_leftAndRightDifferentGainSetting_expandControl() { + prepareRemoteData(mDevice, 10, MUTE_NOT_MUTED); + prepareRemoteData(mMemberDevice, 20, MUTE_NOT_MUTED); + when(mAmbientLayout.isExpanded()).thenReturn(false); + + mController.refresh(); + + verify(mAmbientLayout).setExpanded(true); + } + + @Test + public void refresh_oneSideNotMutable_controlNotMutableAndNotMuted() { + prepareRemoteData(mDevice, 10, MUTE_DISABLED); + prepareRemoteData(mMemberDevice, 20, MUTE_NOT_MUTED); + + mController.refresh(); + + verify(mAmbientLayout).setMutable(false); + verify(mAmbientLayout).setMuted(false); + } + + @Test + public void refresh_oneSideNotMuted_controlNotMutedAndSyncToRemote() { + prepareRemoteData(mDevice, 10, MUTE_MUTED); + prepareRemoteData(mMemberDevice, 20, MUTE_NOT_MUTED); + + mController.refresh(); + + verify(mAmbientLayout).setMutable(true); + verify(mAmbientLayout).setMuted(false); + verify(mVolumeController).setMuted(mDevice, false); + } + + private void prepareDevice(boolean hasMember) { + when(mCachedDevice.getDeviceSide()).thenReturn(SIDE_LEFT); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + when(mCachedDevice.getBondState()).thenReturn(BOND_BONDED); + when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); + when(mDevice.getAddress()).thenReturn(TEST_ADDRESS); + when(mDevice.getAnonymizedAddress()).thenReturn(TEST_ADDRESS); + when(mDevice.isConnected()).thenReturn(true); + if (hasMember) { + when(mCachedDevice.getMemberDevice()).thenReturn(Set.of(mCachedMemberDevice)); + when(mCachedMemberDevice.getDeviceSide()).thenReturn(SIDE_RIGHT); + when(mCachedMemberDevice.getDevice()).thenReturn(mMemberDevice); + when(mCachedMemberDevice.getBondState()).thenReturn(BOND_BONDED); + when(mCachedMemberDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); + when(mMemberDevice.getAddress()).thenReturn(TEST_MEMBER_ADDRESS); + when(mMemberDevice.getAnonymizedAddress()).thenReturn(TEST_MEMBER_ADDRESS); + when(mMemberDevice.isConnected()).thenReturn(true); + } else { + when(mCachedDevice.getMemberDevice()).thenReturn(Set.of()); + } + } + + private void prepareRemoteData(BluetoothDevice device, int gainSetting, int mute) { + when(mVolumeController.refreshAmbientState(device)).thenReturn( + new AmbientVolumeController.RemoteAmbientState(gainSetting, mute)); + } + + private void verifyDeviceDataUpdated(BluetoothDevice device) { + verify(mLocalDataManager).updateAmbient(eq(device), anyInt()); + verify(mLocalDataManager).updateGroupAmbient(eq(device), anyInt()); + verify(mLocalDataManager).updateAmbientControlExpanded(eq(device), + anyBoolean()); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManagerTest.java index 6d83588e0f6e..6485636079dd 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManagerTest.java @@ -31,6 +31,8 @@ import android.provider.Settings; import androidx.test.core.app.ApplicationProvider; +import com.android.settingslib.utils.ThreadUtils; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -49,7 +51,10 @@ import java.util.Map; /** Tests for {@link HearingDeviceLocalDataManager}. */ @RunWith(RobolectricTestRunner.class) -@Config(shadows = {HearingDeviceLocalDataManagerTest.ShadowGlobal.class}) +@Config(shadows = { + HearingDeviceLocalDataManagerTest.ShadowGlobal.class, + HearingDeviceLocalDataManagerTest.ShadowThreadUtils.class, +}) public class HearingDeviceLocalDataManagerTest { private static final String TEST_ADDRESS = "XX:XX:XX:XX:11:22"; @@ -249,4 +254,12 @@ public class HearingDeviceLocalDataManagerTest { return sDataMap.computeIfAbsent(cr, k -> new HashMap<>()); } } + + @Implements(value = ThreadUtils.class) + public static class ShadowThreadUtils { + @Implementation + protected static void postOnBackgroundThread(Runnable runnable) { + runnable.run(); + } + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java index 2b8b3b74dab9..c939c770b63d 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java @@ -21,9 +21,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import android.app.Application; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -33,10 +30,8 @@ import androidx.preference.PreferenceViewHolder; import androidx.test.core.app.ApplicationProvider; import com.android.settingslib.widget.preference.selector.R; -import com.android.settingslib.widget.selectorwithwidgetpreference.flags.Flags; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; @@ -45,7 +40,6 @@ import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class SelectorWithWidgetPreferenceTest { - @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private Application mContext; private SelectorWithWidgetPreference mPreference; @@ -128,26 +122,6 @@ public class SelectorWithWidgetPreferenceTest { } @Test - @DisableFlags(Flags.FLAG_ALLOW_SET_TITLE_MAX_LINES) - public void onBindViewHolder_titleMaxLinesSet_flagOff_titleMaxLinesMatchesDefault() { - final int titleMaxLines = 5; - AttributeSet attributeSet = Robolectric.buildAttributeSet() - .addAttribute(R.attr.titleMaxLines, String.valueOf(titleMaxLines)) - .build(); - mPreference = new SelectorWithWidgetPreference(mContext, attributeSet); - View view = LayoutInflater.from(mContext) - .inflate(mPreference.getLayoutResource(), null /* root */); - PreferenceViewHolder preferenceViewHolder = - PreferenceViewHolder.createInstanceForTests(view); - - mPreference.onBindViewHolder(preferenceViewHolder); - - TextView title = (TextView) preferenceViewHolder.findViewById(android.R.id.title); - assertThat(title.getMaxLines()).isEqualTo(SelectorWithWidgetPreference.DEFAULT_MAX_LINES); - } - - @Test - @EnableFlags(Flags.FLAG_ALLOW_SET_TITLE_MAX_LINES) public void onBindViewHolder_noTitleMaxLinesSet_titleMaxLinesMatchesDefault() { AttributeSet attributeSet = Robolectric.buildAttributeSet().build(); mPreference = new SelectorWithWidgetPreference(mContext, attributeSet); @@ -163,7 +137,6 @@ public class SelectorWithWidgetPreferenceTest { } @Test - @EnableFlags(Flags.FLAG_ALLOW_SET_TITLE_MAX_LINES) public void onBindViewHolder_titleMaxLinesSet_titleMaxLinesUpdated() { final int titleMaxLines = 5; AttributeSet attributeSet = Robolectric.buildAttributeSet() diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java index 91ac34ac8233..de7c450d8d39 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java @@ -148,27 +148,7 @@ public final class DeviceConfigService extends Binder { // TODO(b/364399200): use filter to skip instead? return; } - - ArrayList<String> missingFiles = new ArrayList<String>(); - for (String fileName : sAconfigTextProtoFilesOnDevice) { - File aconfigFile = new File(fileName); - if (!aconfigFile.exists()) { - missingFiles.add(fileName); - } - } - - if (missingFiles.isEmpty()) { - pw.println("\nAconfig flags:"); - for (String name : MyShellCommand.listAllAconfigFlags(iprovider)) { - pw.println(name); - } - } else { - pw.println("\nFailed to dump aconfig flags due to missing files:"); - for (String fileName : missingFiles) { - pw.println(fileName); - } - } - } + } private static HashSet<String> getAconfigFlagNamesInDeviceConfig() { HashSet<String> nameSet = new HashSet<String>(); diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index c4d13ba6effa..5ff2d1b07347 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -496,6 +496,13 @@ flag { } flag { + name: "status_bar_popup_chips" + namespace: "systemui" + description: "Show rich ongoing processes as chips in the status bar" + bug: "372964148" +} + +flag { name: "promote_notifications_automatically" namespace: "systemui" description: "Flag to automatically turn certain notifications into promoted notifications so " @@ -1237,6 +1244,13 @@ flag { } flag { + name: "glanceable_hub_back_action" + namespace: "systemui" + description: "Support back action from glanceable hub" + bug: "382771533" +} + +flag { name: "dream_overlay_updated_font" namespace: "systemui" description: "Flag to enable updated font settings for dream overlay" diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java index ca2b9578f2be..7d27a562f536 100644 --- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java @@ -195,7 +195,10 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub { // Create the origin leash and add to the transition root leash. mOriginLeash = new SurfaceControl.Builder().setName("OriginTransition-origin-leash").build(); - mStartTransaction + + // Create temporary transaction to build + final SurfaceControl.Transaction tmpTransaction = new SurfaceControl.Transaction(); + tmpTransaction .reparent(mOriginLeash, rootLeash) .show(mOriginLeash) .setCornerRadius(mOriginLeash, windowRadius) @@ -208,14 +211,14 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub { int mode = change.getMode(); SurfaceControl leash = change.getLeash(); // Reparent leash to the transition root. - mStartTransaction.reparent(leash, rootLeash); + tmpTransaction.reparent(leash, rootLeash); if (TransitionUtil.isOpeningMode(mode)) { openingSurfaces.add(change.getLeash()); // For opening surfaces, ending bounds are base bound. Apply corner radius if // it's full screen. Rect bounds = change.getEndAbsBounds(); if (displayBounds.equals(bounds)) { - mStartTransaction + tmpTransaction .setCornerRadius(leash, windowRadius) .setWindowCrop(leash, bounds.width(), bounds.height()); } @@ -226,28 +229,53 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub { // it's full screen. Rect bounds = change.getStartAbsBounds(); if (displayBounds.equals(bounds)) { - mStartTransaction + tmpTransaction .setCornerRadius(leash, windowRadius) .setWindowCrop(leash, bounds.width(), bounds.height()); } } } + if (openingSurfaces.isEmpty() && closingSurfaces.isEmpty()) { + logD("prepareUIs: no opening/closing surfaces available, nothing to prepare."); + return false; + } + // Set relative order: // ---- App1 ---- // ---- origin ---- // ---- App2 ---- + if (mIsEntry) { - mStartTransaction - .setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1) - .setRelativeLayer( - openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1); + if (!closingSurfaces.isEmpty()) { + tmpTransaction + .setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1); + } else { + logW("Missing closing surface is entry transition"); + } + if (!openingSurfaces.isEmpty()) { + tmpTransaction + .setRelativeLayer( + openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1); + } else { + logW("Missing opening surface is entry transition"); + } + } else { - mStartTransaction - .setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1) - .setRelativeLayer( - closingSurfaces.get(closingSurfaces.size() - 1), mOriginLeash, 1); + if (!openingSurfaces.isEmpty()) { + tmpTransaction + .setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1); + } else { + logW("Missing opening surface is exit transition"); + } + if (!closingSurfaces.isEmpty()) { + tmpTransaction.setRelativeLayer( + closingSurfaces.get(closingSurfaces.size() - 1), mOriginLeash, 1); + } else { + logW("Missing closing surface is exit transition"); + } } + mStartTransaction.merge(tmpTransaction); // Attach origin UIComponent to origin leash. mOriginTransaction = mOrigin.newTransaction(); @@ -300,6 +328,7 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub { } private void cancel() { + logD("cancel()"); if (mAnimator != null) { mAnimator.cancel(); } @@ -311,6 +340,10 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub { } } + private static void logW(String msg) { + Log.w(TAG, msg); + } + private static void logE(String msg) { Log.e(TAG, msg); } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 5dbedc7045e4..bf3360f0ea14 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -931,7 +931,9 @@ private fun BoxScope.CommunalHubLazyGrid( Modifier.requiredSize(dpSize) .thenIf(!isItemDragging) { Modifier.animateItem( - placementSpec = spring(stiffness = Spring.StiffnessMediumLow) + placementSpec = spring(stiffness = Spring.StiffnessMediumLow), + // See b/376495198 - not supported with AndroidView + fadeOutSpec = null, ) } .thenIf(isItemDragging) { Modifier.zIndex(1f) }, @@ -980,11 +982,14 @@ private fun BoxScope.CommunalHubLazyGrid( size = size, selected = false, modifier = - Modifier.requiredSize(dpSize).animateItem().thenIf( - communalResponsiveGrid() - ) { - Modifier.graphicsLayer { alpha = itemAlpha?.value ?: 1f } - }, + Modifier.requiredSize(dpSize) + .animateItem( + // See b/376495198 - not supported with AndroidView + fadeOutSpec = null + ) + .thenIf(communalResponsiveGrid()) { + Modifier.graphicsLayer { alpha = itemAlpha?.value ?: 1f } + }, index = index, contentListState = contentListState, interactionHandler = interactionHandler, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt index db33e7c628d7..79cf24b9c547 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt @@ -35,9 +35,8 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.MutableSceneTransitionLayoutState -import com.android.compose.animation.scene.SceneScope -import com.android.compose.animation.scene.SceneTransitionLayout import com.android.compose.modifiers.thenIf import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.largeClockScene @@ -61,7 +60,7 @@ constructor( private val clockInteractor: KeyguardClockInteractor, ) { @Composable - fun SceneScope.DefaultClockLayout( + fun ContentScope.DefaultClockLayout( smartSpacePaddingTop: (Resources) -> Int, isShadeLayoutWide: Boolean, modifier: Modifier = Modifier, @@ -95,7 +94,7 @@ constructor( } Column(modifier) { - SceneTransitionLayout(state) { + NestedSceneTransitionLayout(state, Modifier) { scene(splitShadeLargeClockScene) { LargeClockWithSmartSpace( smartSpacePaddingTop = smartSpacePaddingTop, @@ -134,7 +133,7 @@ constructor( } @Composable - private fun SceneScope.SmallClockWithSmartSpace( + private fun ContentScope.SmallClockWithSmartSpace( smartSpacePaddingTop: (Resources) -> Int, modifier: Modifier = Modifier, ) { @@ -159,7 +158,7 @@ constructor( } @Composable - private fun SceneScope.LargeClockWithSmartSpace( + private fun ContentScope.LargeClockWithSmartSpace( smartSpacePaddingTop: (Resources) -> Int, shouldOffSetClockToOneHalf: Boolean = false, ) { @@ -200,7 +199,7 @@ constructor( } @Composable - private fun SceneScope.WeatherLargeClockWithSmartSpace( + private fun ContentScope.WeatherLargeClockWithSmartSpace( smartSpacePaddingTop: (Resources) -> Int, modifier: Modifier = Modifier, ) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt index 2af5ffaee7ed..5790c4af0d77 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt @@ -19,6 +19,7 @@ package com.android.systemui.notifications.ui.composable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.layoutId import com.android.compose.animation.scene.ContentScope @@ -84,7 +85,11 @@ constructor( viewModel.notificationsPlaceholderViewModelFactory.create() } - OverlayShade(modifier = modifier, onScrimClicked = viewModel::onScrimClicked) { + OverlayShade( + panelAlignment = Alignment.TopStart, + modifier = modifier, + onScrimClicked = viewModel::onScrimClicked, + ) { Column { if (viewModel.showHeader) { val burnIn = rememberBurnIn(clockInteractor) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt index b1a19456ab7d..f6c5f588aa95 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -99,7 +99,11 @@ constructor( val viewModel = rememberViewModel("QuickSettingsShadeOverlay") { contentViewModelFactory.create() } - OverlayShade(modifier = modifier, onScrimClicked = viewModel::onScrimClicked) { + OverlayShade( + panelAlignment = Alignment.TopEnd, + modifier = modifier, + onScrimClicked = viewModel::onScrimClicked, + ) { Column { ExpandedShadeHeader( viewModelFactory = viewModel.shadeHeaderViewModelFactory, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index 55fafd5cfeca..8907aec7fd48 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.gestures.Orientation import com.android.compose.animation.scene.ProgressConverter import com.android.compose.animation.scene.TransitionKey import com.android.compose.animation.scene.transitions -import com.android.systemui.bouncer.ui.composable.Bouncer import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes @@ -110,17 +109,13 @@ val SceneContainerTransitions = transitions { // Overlay transitions - // TODO(b/376659778): Remove this transition once nested STLs are supported. - from(Scenes.Gone, to = Overlays.NotificationsShade) { - toNotificationsShadeTransition(translateClock = true) - } to(Overlays.NotificationsShade) { toNotificationsShadeTransition() } to(Overlays.QuickSettingsShade) { toQuickSettingsShadeTransition() } from(Overlays.NotificationsShade, to = Overlays.QuickSettingsShade) { notificationsShadeToQuickSettingsShadeTransition() } from(Scenes.Gone, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) { - toNotificationsShadeTransition(translateClock = true, durationScale = 0.9) + toNotificationsShadeTransition(durationScale = 0.9) } from(Scenes.Gone, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) { toQuickSettingsShadeTransition(durationScale = 0.9) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt index 6bdb36331709..3d62151baf2f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt @@ -29,10 +29,7 @@ import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.Shade import kotlin.time.Duration.Companion.milliseconds -fun TransitionBuilder.toNotificationsShadeTransition( - translateClock: Boolean = false, - durationScale: Double = 1.0, -) { +fun TransitionBuilder.toNotificationsShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) swipeSpec = spring( @@ -45,11 +42,6 @@ fun TransitionBuilder.toNotificationsShadeTransition( elevateInContent = Overlays.NotificationsShade, ) scaleSize(OverlayShade.Elements.Panel, height = 0f) - // TODO(b/376659778): This is a temporary hack to have a shared element transition with the - // lockscreen clock. Remove once nested STLs are supported. - if (!translateClock) { - translate(ClockElementKeys.smallClockElementKey) - } // Avoid translating the status bar with the shade panel. translate(NotificationsShade.Elements.StatusBar) // Slide in the shade panel from the top edge. diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index f821e429beb5..cfbe6671db02 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -55,16 +55,17 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexContentPicker -import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.effect.rememberOffsetOverscrollEffect import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.systemui.res.R /** Renders a lightweight shade UI container, as an overlay. */ @Composable -fun SceneScope.OverlayShade( +fun ContentScope.OverlayShade( + panelAlignment: Alignment, onScrimClicked: () -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit, @@ -87,7 +88,7 @@ fun SceneScope.OverlayShade( ) { Scrim(onClicked = onScrimClicked) - Box(modifier = Modifier.fillMaxSize().panelPadding(), contentAlignment = Alignment.TopEnd) { + Box(modifier = Modifier.fillMaxSize().panelPadding(), contentAlignment = panelAlignment) { Panel( modifier = Modifier.element(OverlayShade.Elements.Panel) @@ -100,7 +101,7 @@ fun SceneScope.OverlayShade( } @Composable -private fun SceneScope.Scrim(onClicked: () -> Unit, modifier: Modifier = Modifier) { +private fun ContentScope.Scrim(onClicked: () -> Unit, modifier: Modifier = Modifier) { Spacer( modifier = modifier @@ -112,7 +113,7 @@ private fun SceneScope.Scrim(onClicked: () -> Unit, modifier: Modifier = Modifie } @Composable -private fun SceneScope.Panel(modifier: Modifier = Modifier, content: @Composable () -> Unit) { +private fun ContentScope.Panel(modifier: Modifier = Modifier, content: @Composable () -> Unit) { Box(modifier = modifier.clip(OverlayShade.Shapes.RoundedCornerPanel)) { Spacer( modifier = diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt index 599a152a23bd..167928b38e90 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt @@ -30,7 +30,8 @@ internal fun Element.shouldBeRenderedBy(content: ContentKey): Boolean { // the transition is running. If the [renderAuthority.size] is 1 it means that that this element // is currently composed only in one nesting level, which means that the render authority // is determined by "classic" shared element code. - return renderAuthority.size == 1 || renderAuthority.first() == content + return renderAuthority.size > 0 && + (renderAuthority.size == 1 || renderAuthority.first() == content) } /** diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt index 85bdf9264467..cea1e9600741 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt @@ -163,6 +163,16 @@ class KeyguardDisplayManagerTest : SysuiTestCase() { } @Test + fun testShow_rearDisplayOuterDefaultActive_occluded() { + displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay) + + whenever(deviceStateHelper.isRearDisplayOuterDefaultActive(secondaryDisplay)) + .thenReturn(true) + whenever(keyguardStateController.isOccluded).thenReturn(true) + verify(presentationFactory, never()).create(eq(secondaryDisplay)) + } + + @Test fun testShow_presentationCreated() { displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayoutTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayoutTest.java new file mode 100644 index 000000000000..455329f54864 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayoutTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.hearingaid; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; +import static com.android.systemui.accessibility.hearingaid.AmbientVolumeLayout.ROTATION_COLLAPSED; +import static com.android.systemui.accessibility.hearingaid.AmbientVolumeLayout.ROTATION_EXPANDED; +import static com.android.systemui.accessibility.hearingaid.AmbientVolumeLayout.SIDE_UNIFIED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; + +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.util.ArrayMap; +import android.view.View; +import android.widget.ImageView; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.settingslib.bluetooth.AmbientVolumeUi; +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.Map; + +/** Tests for {@link AmbientVolumeLayout}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class AmbientVolumeLayoutTest extends SysuiTestCase { + + private static final int TEST_LEFT_VOLUME_LEVEL = 1; + private static final int TEST_RIGHT_VOLUME_LEVEL = 2; + private static final int TEST_UNIFIED_VOLUME_LEVEL = 3; + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Spy + private Context mContext = ApplicationProvider.getApplicationContext(); + @Mock + private AmbientVolumeUi.AmbientVolumeUiListener mListener; + + private AmbientVolumeLayout mLayout; + private ImageView mExpandIcon; + private ImageView mVolumeIcon; + private final Map<Integer, BluetoothDevice> mSideToDeviceMap = new ArrayMap<>(); + + @Before + public void setUp() { + mLayout = new AmbientVolumeLayout(mContext); + mLayout.setListener(mListener); + mLayout.setExpandable(true); + mLayout.setMutable(true); + + prepareDevices(); + mLayout.setupSliders(mSideToDeviceMap); + mLayout.getSliders().forEach((side, slider) -> { + slider.setMin(0); + slider.setMax(4); + if (side == SIDE_LEFT) { + slider.setValue(TEST_LEFT_VOLUME_LEVEL); + } else if (side == SIDE_RIGHT) { + slider.setValue(TEST_RIGHT_VOLUME_LEVEL); + } else if (side == SIDE_UNIFIED) { + slider.setValue(TEST_UNIFIED_VOLUME_LEVEL); + } + }); + + mExpandIcon = mLayout.getExpandIcon(); + mVolumeIcon = mLayout.getVolumeIcon(); + } + + @Test + public void setExpandable_expandable_expandIconVisible() { + mLayout.setExpandable(true); + + assertThat(mExpandIcon.getVisibility()).isEqualTo(VISIBLE); + } + + @Test + public void setExpandable_notExpandable_expandIconGone() { + mLayout.setExpandable(false); + + assertThat(mExpandIcon.getVisibility()).isEqualTo(View.GONE); + } + + @Test + public void setExpanded_expanded_assertControlUiCorrect() { + mLayout.setExpanded(true); + + assertControlUiCorrect(); + } + + @Test + public void setExpanded_notExpanded_assertControlUiCorrect() { + mLayout.setExpanded(false); + + assertControlUiCorrect(); + } + + @Test + public void setMutable_mutable_clickOnMuteIconChangeMuteState() { + mLayout.setMutable(true); + mLayout.setMuted(false); + + mVolumeIcon.callOnClick(); + + assertThat(mLayout.isMuted()).isTrue(); + } + + @Test + public void setMutable_notMutable_clickOnMuteIconWontChangeMuteState() { + mLayout.setMutable(false); + mLayout.setMuted(false); + + mVolumeIcon.callOnClick(); + + assertThat(mLayout.isMuted()).isFalse(); + } + + @Test + public void updateLayout_mute_volumeIconIsCorrect() { + mLayout.setMuted(true); + mLayout.updateLayout(); + + assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(0); + } + + @Test + public void updateLayout_unmuteAndExpanded_volumeIconIsCorrect() { + mLayout.setMuted(false); + mLayout.setExpanded(true); + mLayout.updateLayout(); + + int expectedLevel = calculateVolumeLevel(TEST_LEFT_VOLUME_LEVEL, TEST_RIGHT_VOLUME_LEVEL); + assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(expectedLevel); + } + + @Test + public void updateLayout_unmuteAndNotExpanded_volumeIconIsCorrect() { + mLayout.setMuted(false); + mLayout.setExpanded(false); + mLayout.updateLayout(); + + int expectedLevel = calculateVolumeLevel(TEST_UNIFIED_VOLUME_LEVEL, + TEST_UNIFIED_VOLUME_LEVEL); + assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(expectedLevel); + } + + @Test + public void setSliderEnabled_expandedAndLeftIsDisabled_volumeIconIsCorrect() { + mLayout.setExpanded(true); + mLayout.setSliderEnabled(SIDE_LEFT, false); + + int expectedLevel = calculateVolumeLevel(0, TEST_RIGHT_VOLUME_LEVEL); + assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(expectedLevel); + } + + @Test + public void setSliderValue_expandedAndLeftValueChanged_volumeIconIsCorrect() { + mLayout.setExpanded(true); + mLayout.setSliderValue(SIDE_LEFT, 4); + + int expectedLevel = calculateVolumeLevel(4, TEST_RIGHT_VOLUME_LEVEL); + assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(expectedLevel); + } + + private int calculateVolumeLevel(int left, int right) { + return left * 5 + right; + } + + private void assertControlUiCorrect() { + final boolean expanded = mLayout.isExpanded(); + final Map<Integer, AmbientVolumeSlider> sliders = mLayout.getSliders(); + if (expanded) { + assertThat(sliders.get(SIDE_UNIFIED).getVisibility()).isEqualTo(GONE); + assertThat(sliders.get(SIDE_LEFT).getVisibility()).isEqualTo(VISIBLE); + assertThat(sliders.get(SIDE_RIGHT).getVisibility()).isEqualTo(VISIBLE); + assertThat(mExpandIcon.getRotation()).isEqualTo(ROTATION_EXPANDED); + } else { + assertThat(sliders.get(SIDE_UNIFIED).getVisibility()).isEqualTo(VISIBLE); + assertThat(sliders.get(SIDE_LEFT).getVisibility()).isEqualTo(GONE); + assertThat(sliders.get(SIDE_RIGHT).getVisibility()).isEqualTo(GONE); + assertThat(mExpandIcon.getRotation()).isEqualTo(ROTATION_COLLAPSED); + } + } + + private void prepareDevices() { + mSideToDeviceMap.put(SIDE_LEFT, mock(BluetoothDevice.class)); + mSideToDeviceMap.put(SIDE_RIGHT, mock(BluetoothDevice.class)); + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSliderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSliderTest.java new file mode 100644 index 000000000000..78dfda88a526 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSliderTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.hearingaid; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Tests for {@link AmbientVolumeLayout}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class AmbientVolumeSliderTest extends SysuiTestCase { + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Spy + private Context mContext = ApplicationProvider.getApplicationContext(); + + private AmbientVolumeSlider mSlider; + + @Before + public void setUp() { + mSlider = new AmbientVolumeSlider(mContext); + } + + @Test + public void setTitle_titleCorrect() { + final String testTitle = "test"; + mSlider.setTitle(testTitle); + + assertThat(mSlider.getTitle()).isEqualTo(testTitle); + } + + @Test + public void getVolumeLevel_valueMin_volumeLevelIsZero() { + prepareSlider(/* min= */ 0, /* max= */ 100, /* value= */ 0); + + // The volume level is divided into 5 levels: + // Level 0 corresponds to the minimum volume value. The range between the minimum and + // maximum volume is divided into 4 equal intervals, represented by levels 1 to 4. + assertThat(mSlider.getVolumeLevel()).isEqualTo(0); + } + + @Test + public void getVolumeLevel_valueMax_volumeLevelIsFour() { + prepareSlider(/* min= */ 0, /* max= */ 100, /* value= */ 100); + + assertThat(mSlider.getVolumeLevel()).isEqualTo(4); + } + + @Test + public void getVolumeLevel_volumeLevelIsCorrect() { + prepareSlider(/* min= */ 0, /* max= */ 100, /* value= */ 73); + + assertThat(mSlider.getVolumeLevel()).isEqualTo(3); + } + + private void prepareSlider(float min, float max, float value) { + mSlider.setMin(min); + mSlider.setMax(max); + mSlider.setValue(value); + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java index ad12c61ab5d1..43d0d69c428f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java @@ -16,8 +16,11 @@ package com.android.systemui.accessibility.hearingaid; +import static android.bluetooth.BluetoothDevice.BOND_BONDED; import static android.bluetooth.BluetoothHapClient.PRESET_INDEX_UNAVAILABLE; +import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; import static com.android.systemui.accessibility.hearingaid.HearingDevicesDialogDelegate.LIVE_CAPTION_INTENT; import static com.google.common.truth.Truth.assertThat; @@ -31,6 +34,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.bluetooth.AudioInputControl; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHapPresetInfo; import android.bluetooth.BluetoothProfile; @@ -61,6 +65,7 @@ import com.android.settingslib.bluetooth.HapClientProfile; import com.android.settingslib.bluetooth.LocalBluetoothAdapter; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.bluetooth.VolumeControlProfile; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogTransitionAnimator; @@ -90,6 +95,7 @@ import java.util.List; @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest public class HearingDevicesDialogDelegateTest extends SysuiTestCase { + @Rule public MockitoRule mockito = MockitoJUnit.rule(); @@ -120,6 +126,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { @Mock private HapClientProfile mHapClientProfile; @Mock + private VolumeControlProfile mVolumeControlProfile; + @Mock private CachedBluetoothDeviceManager mCachedDeviceManager; @Mock private BluetoothEventManager mBluetoothEventManager; @@ -151,21 +159,25 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { when(mLocalBluetoothManager.getBluetoothAdapter()).thenReturn(mLocalBluetoothAdapter); when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager); when(mProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile); + when(mProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControlProfile); when(mLocalBluetoothAdapter.isEnabled()).thenReturn(true); when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager); when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(List.of(mCachedDevice)); when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager); when(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState); - when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + when(mDevice.getBondState()).thenReturn(BOND_BONDED); when(mDevice.isConnected()).thenReturn(true); when(mCachedDevice.getDevice()).thenReturn(mDevice); when(mCachedDevice.getAddress()).thenReturn(DEVICE_ADDRESS); when(mCachedDevice.getName()).thenReturn(DEVICE_NAME); - when(mCachedDevice.getProfiles()).thenReturn(List.of(mHapClientProfile)); + when(mCachedDevice.getProfiles()).thenReturn( + List.of(mHapClientProfile, mVolumeControlProfile)); when(mCachedDevice.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true); when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(true); when(mCachedDevice.isConnectedHapClientDevice()).thenReturn(true); when(mCachedDevice.getDrawableWithDescription()).thenReturn(new Pair<>(mDrawable, "")); + when(mCachedDevice.getBondState()).thenReturn(BOND_BONDED); + when(mCachedDevice.getDeviceSide()).thenReturn(SIDE_LEFT); when(mHearingDeviceItem.getCachedBluetoothDevice()).thenReturn(mCachedDevice); mContext.setMockPackageManager(mPackageManager); @@ -292,6 +304,46 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { } @Test + @EnableFlags(com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_AMBIENT_VOLUME_CONTROL) + public void showDialog_deviceNotSupportVcp_ambientLayoutGone() { + when(mCachedDevice.getProfiles()).thenReturn(List.of()); + + setUpDeviceDialogWithoutPairNewDeviceButton(); + mDialog.show(); + + ViewGroup ambientLayout = getAmbientLayout(mDialog); + assertThat(ambientLayout.getVisibility()).isEqualTo(View.GONE); + } + + @Test + @EnableFlags(com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_AMBIENT_VOLUME_CONTROL) + public void showDialog_ambientControlNotAvailable_ambientLayoutGone() { + when(mVolumeControlProfile.getAudioInputControlServices(mDevice)).thenReturn(List.of()); + + setUpDeviceDialogWithoutPairNewDeviceButton(); + mDialog.show(); + + ViewGroup ambientLayout = getAmbientLayout(mDialog); + assertThat(ambientLayout.getVisibility()).isEqualTo(View.GONE); + } + + @Test + @EnableFlags(com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_AMBIENT_VOLUME_CONTROL) + public void showDialog_supportVcpAndAmbientControlAvailable_ambientLayoutVisible() { + when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); + AudioInputControl audioInputControl = prepareAudioInputControl(); + when(mVolumeControlProfile.getAudioInputControlServices(mDevice)).thenReturn( + List.of(audioInputControl)); + when(mVolumeControlProfile.getConnectionStatus(mDevice)).thenReturn(STATE_CONNECTED); + + setUpDeviceDialogWithoutPairNewDeviceButton(); + mDialog.show(); + + ViewGroup ambientLayout = getAmbientLayout(mDialog); + assertThat(ambientLayout.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test public void onActiveDeviceChanged_presetExist_presetSelected() { setUpDeviceDialogWithoutPairNewDeviceButton(); mDialog.show(); @@ -368,6 +420,10 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { return dialog.requireViewById(R.id.preset_layout); } + private ViewGroup getAmbientLayout(SystemUIDialog dialog) { + return dialog.requireViewById(R.id.ambient_layout); + } + private int countChildWithoutSpace(ViewGroup viewGroup) { int spaceCount = 0; @@ -388,6 +444,16 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { assertThat(toolsLayout.getVisibility()).isEqualTo(targetVisibility); } + private AudioInputControl prepareAudioInputControl() { + AudioInputControl audioInputControl = mock(AudioInputControl.class); + when(audioInputControl.getAudioInputType()).thenReturn( + AudioInputControl.AUDIO_INPUT_TYPE_AMBIENT); + when(audioInputControl.getGainMode()).thenReturn(AudioInputControl.GAIN_MODE_MANUAL); + when(audioInputControl.getAudioInputStatus()).thenReturn( + AudioInputControl.AUDIO_INPUT_STATUS_ACTIVE); + return audioInputControl; + } + @After public void reset() { if (mDialogDelegate != null) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt index 41cc6ee182cf..271cd3a4f202 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.back.domain.interactor +import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.RequiresFlagsDisabled import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.DeviceFlagsValueProvider @@ -31,6 +32,7 @@ import androidx.test.filters.SmallTest import com.android.internal.statusbar.IStatusBarService import com.android.systemui.Flags import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.domain.interactor.CommunalBackActionInteractor import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope @@ -93,6 +95,7 @@ class BackActionInteractorTest : SysuiTestCase() { @Mock private lateinit var onBackInvokedDispatcher: WindowOnBackInvokedDispatcher @Mock private lateinit var iStatusBarService: IStatusBarService @Mock private lateinit var headsUpManager: HeadsUpManager + @Mock private lateinit var communalBackActionInteractor: CommunalBackActionInteractor private val keyguardRepository = FakeKeyguardRepository() private val windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor by lazy { @@ -117,6 +120,7 @@ class BackActionInteractorTest : SysuiTestCase() { windowRootViewVisibilityInteractor, shadeBackActionInteractor, qsController, + communalBackActionInteractor, ) } @@ -306,6 +310,19 @@ class BackActionInteractorTest : SysuiTestCase() { verify(shadeBackActionInteractor).onBackProgressed(0.4f) } + @Test + @EnableFlags(Flags.FLAG_GLANCEABLE_HUB_BACK_ACTION) + fun onBackAction_communalCanBeDismissed_communalBackActionInteractorCalled() { + backActionInteractor.start() + windowRootViewVisibilityInteractor.setIsLockscreenOrShadeVisible(true) + powerInteractor.setAwakeForTest() + val callback = getBackInvokedCallback() + whenever(communalBackActionInteractor.canBeDismissed()).thenReturn(true) + callback.onBackInvoked() + + verify(communalBackActionInteractor).onBackPressed() + } + private fun getBackInvokedCallback(): OnBackInvokedCallback { testScope.runCurrent() val captor = argumentCaptor<OnBackInvokedCallback>() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorTest.kt new file mode 100644 index 000000000000..c365f1cb3872 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.domain.interactor + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class CommunalBackActionInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + + private var Kosmos.underTest by Fixture { communalBackActionInteractor } + + @Test + @EnableFlags(FLAG_COMMUNAL_HUB) + fun communalShowing_canBeDismissed() = + kosmos.runTest { + setCommunalAvailable(true) + assertThat(underTest.canBeDismissed()).isEqualTo(false) + communalInteractor.changeScene(CommunalScenes.Communal, "test") + runCurrent() + assertThat(underTest.canBeDismissed()).isEqualTo(true) + } + + @Test + @EnableFlags(FLAG_COMMUNAL_HUB) + fun onBackPressed_invokesSceneChange() = + kosmos.runTest { + underTest.onBackPressed() + runCurrent() + assertThat(communalSceneRepository.currentScene.value).isEqualTo(CommunalScenes.Blank) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt index 0bfcd242828d..8a9c42d9b64e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt @@ -130,19 +130,6 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { } @Test - fun tutorialState_startedAndCommunalSceneShowing_stateWillNotUpdate() = - testScope.runTest { - val tutorialSettingState by - collectLastValue(communalTutorialRepository.tutorialSettingState) - - communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED) - - goToCommunal() - - assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_STARTED) - } - - @Test fun tutorialState_completedAndCommunalSceneShowing_stateWillNotUpdate() = testScope.runTest { val tutorialSettingState by diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt index ee3e241b5754..56e8185ab580 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt @@ -81,7 +81,7 @@ class CameraQuickAffordanceConfigTest : SysuiTestCase() { // Then verify(cameraGestureHelper) .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(true), result) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt index 50ac26196978..fde9b8ce6a50 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt @@ -197,7 +197,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { val dndMode = currentModes!!.single() assertThat(dndMode.isActive).isFalse() - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) } @Test @@ -222,7 +222,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { ) // then - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) assertEquals(ZEN_MODE_OFF, spyZenMode.value) assertNull(spyConditionId.value) } @@ -244,7 +244,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { val dndMode = currentModes!!.single() assertThat(dndMode.isActive).isTrue() assertThat(zenModeRepository.getModeActiveDuration(dndMode.id)).isNull() - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) } @Test @@ -268,7 +268,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { ) // then - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value) assertNull(spyConditionId.value) } @@ -285,7 +285,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { val result = underTest.onTriggered(null) runCurrent() - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) val dndMode = currentModes!!.single() assertThat(dndMode.isActive).isTrue() assertThat(zenModeRepository.getModeActiveDuration(dndMode.id)) @@ -313,7 +313,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { ) // then - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value) assertEquals(conditionUri, spyConditionId.value) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceHapticViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceHapticViewModelTest.kt new file mode 100644 index 000000000000..18946f9d7e07 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceHapticViewModelTest.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.domain.interactor.keyguardQuickAffordanceHapticViewModelFactory +import com.android.systemui.keyguard.domain.interactor.keyguardQuickAffordanceInteractor +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceHapticViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel +import com.android.systemui.kosmos.testScope +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class KeyguardQuickAffordanceHapticViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + private val configKey = "$slotId::home" + private val keyguardQuickAffordanceInteractor = kosmos.keyguardQuickAffordanceInteractor + private val viewModelFlow = + MutableStateFlow(KeyguardQuickAffordanceViewModel(configKey = configKey, slotId = slotId)) + + private val underTest = + kosmos.keyguardQuickAffordanceHapticViewModelFactory.create(viewModelFlow) + + @Test + fun whenLaunchingFromTriggeredResult_hapticStateIsLaunch() = + testScope.runTest { + // GIVEN that the result from triggering the affordance launched an activity or dialog + val hapticState by collectLastValue(underTest.quickAffordanceHapticState) + keyguardQuickAffordanceInteractor.setLaunchingFromTriggeredResult( + KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult(true, configKey) + ) + runCurrent() + + // THEN the haptic state indicates that a launch haptics must play + assertThat(hapticState) + .isEqualTo(KeyguardQuickAffordanceHapticViewModel.HapticState.LAUNCH) + } + + @Test + fun whenNotLaunchFromTriggeredResult_hapticStateDoesNotEmit() = + testScope.runTest { + // GIVEN that the result from triggering the affordance did not launch an activity or + // dialog + val hapticState by collectLastValue(underTest.quickAffordanceHapticState) + keyguardQuickAffordanceInteractor.setLaunchingFromTriggeredResult( + KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult(false, configKey) + ) + runCurrent() + + // THEN there is no haptic state to play any feedback + assertThat(hapticState) + .isEqualTo(KeyguardQuickAffordanceHapticViewModel.HapticState.NO_HAPTICS) + } + + @Test + fun onQuickAffordanceTogglesToActivated_hapticStateIsToggleOn() = + testScope.runTest { + // GIVEN that an affordance toggles from deactivated to activated + val hapticState by collectLastValue(underTest.quickAffordanceHapticState) + toggleQuickAffordance(on = true) + + // THEN the haptic state reflects that a toggle on haptics should play + assertThat(hapticState) + .isEqualTo(KeyguardQuickAffordanceHapticViewModel.HapticState.TOGGLE_ON) + } + + @Test + fun onQuickAffordanceTogglesToDeactivated_hapticStateIsToggleOff() = + testScope.runTest { + // GIVEN that an affordance toggles from activated to deactivated + val hapticState by collectLastValue(underTest.quickAffordanceHapticState) + toggleQuickAffordance(on = false) + + // THEN the haptic state reflects that a toggle off haptics should play + assertThat(hapticState) + .isEqualTo(KeyguardQuickAffordanceHapticViewModel.HapticState.TOGGLE_OFF) + } + + private fun TestScope.toggleQuickAffordance(on: Boolean) { + underTest.updateActivatedHistory(!on) + runCurrent() + underTest.updateActivatedHistory(on) + runCurrent() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt index b15352bfe6ab..173b4e56075c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt @@ -49,14 +49,10 @@ import org.mockito.MockitoAnnotations class MuteQuickAffordanceConfigTest : SysuiTestCase() { private lateinit var underTest: MuteQuickAffordanceConfig - @Mock - private lateinit var ringerModeTracker: RingerModeTracker - @Mock - private lateinit var audioManager: AudioManager - @Mock - private lateinit var userTracker: UserTracker - @Mock - private lateinit var userFileManager: UserFileManager + @Mock private lateinit var ringerModeTracker: RingerModeTracker + @Mock private lateinit var audioManager: AudioManager + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var userFileManager: UserFileManager private lateinit var testDispatcher: TestDispatcher private lateinit var testScope: TestScope @@ -70,9 +66,12 @@ class MuteQuickAffordanceConfigTest : SysuiTestCase() { whenever(userTracker.userContext).thenReturn(context) whenever(userFileManager.getSharedPreferences(any(), any(), any())) - .thenReturn(context.getSharedPreferences("mutequickaffordancetest", Context.MODE_PRIVATE)) + .thenReturn( + context.getSharedPreferences("mutequickaffordancetest", Context.MODE_PRIVATE) + ) - underTest = MuteQuickAffordanceConfig( + underTest = + MuteQuickAffordanceConfig( context, userTracker, userFileManager, @@ -81,64 +80,71 @@ class MuteQuickAffordanceConfigTest : SysuiTestCase() { testScope.backgroundScope, testDispatcher, testDispatcher, - ) + ) } @Test - fun pickerState_volumeFixed_notAvailable() = testScope.runTest { - //given - whenever(audioManager.isVolumeFixed).thenReturn(true) + fun pickerState_volumeFixed_notAvailable() = + testScope.runTest { + // given + whenever(audioManager.isVolumeFixed).thenReturn(true) - //when - val result = underTest.getPickerScreenState() + // when + val result = underTest.getPickerScreenState() - //then - assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice, result) - } + // then + assertEquals( + KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice, + result, + ) + } @Test - fun pickerState_volumeNotFixed_available() = testScope.runTest { - //given - whenever(audioManager.isVolumeFixed).thenReturn(false) + fun pickerState_volumeNotFixed_available() = + testScope.runTest { + // given + whenever(audioManager.isVolumeFixed).thenReturn(false) - //when - val result = underTest.getPickerScreenState() + // when + val result = underTest.getPickerScreenState() - //then - assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.Default(), result) - } + // then + assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.Default(), result) + } @Test - fun triggered_stateWasPreviouslyNORMAL_currentlySILENT_moveToPreviousState() = testScope.runTest { - //given - val ringerModeCapture = argumentCaptor<Int>() - whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL) - underTest.onTriggered(null) - whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_SILENT) - - //when - val result = underTest.onTriggered(null) - runCurrent() - verify(audioManager, times(2)).ringerModeInternal = ringerModeCapture.capture() - - //then - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) - assertEquals(AudioManager.RINGER_MODE_NORMAL, ringerModeCapture.value) - } + fun triggered_stateWasPreviouslyNORMAL_currentlySILENT_moveToPreviousState() = + testScope.runTest { + // given + val ringerModeCapture = argumentCaptor<Int>() + whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL) + underTest.onTriggered(null) + whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_SILENT) + + // when + val result = underTest.onTriggered(null) + runCurrent() + verify(audioManager, times(2)).ringerModeInternal = ringerModeCapture.capture() + + // then + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) + assertEquals(AudioManager.RINGER_MODE_NORMAL, ringerModeCapture.value) + } @Test - fun triggered_stateIsNotSILENT_moveToSILENTringer() = testScope.runTest { - //given - val ringerModeCapture = argumentCaptor<Int>() - whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL) - - //when - val result = underTest.onTriggered(null) - runCurrent() - verify(audioManager).ringerModeInternal = ringerModeCapture.capture() - - //then - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) - assertEquals(AudioManager.RINGER_MODE_SILENT, ringerModeCapture.value) - } -}
\ No newline at end of file + fun triggered_stateIsNotSILENT_moveToSILENTringer() = + testScope.runTest { + // given + val ringerModeCapture = argumentCaptor<Int>() + whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL) + + // when + val result = underTest.onTriggered(null) + runCurrent() + verify(audioManager).ringerModeInternal = ringerModeCapture.capture() + + // then + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) + assertEquals(AudioManager.RINGER_MODE_SILENT, ringerModeCapture.value) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt index e9b36b8b3b57..9bdc363b3a38 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt @@ -88,9 +88,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { Icon.Loaded( drawable = ICON, contentDescription = - ContentDescription.Resource( - res = R.string.accessibility_wallet_button, - ), + ContentDescription.Resource(res = R.string.accessibility_wallet_button), ) ) } @@ -118,9 +116,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { Icon.Loaded( drawable = ICON, contentDescription = - ContentDescription.Resource( - res = R.string.accessibility_wallet_button, - ), + ContentDescription.Resource(res = R.string.accessibility_wallet_button), ) ) } @@ -163,13 +159,9 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { } assertThat(underTest.onTriggered(expandable)) - .isEqualTo(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled) + .isEqualTo(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(true)) verify(walletController) - .startQuickAccessUiIntent( - activityStarter, - animationController, - /* hasCard= */ true, - ) + .startQuickAccessUiIntent(activityStarter, animationController, /* hasCard= */ true) } @Test @@ -184,9 +176,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun getPickerScreenState_unavailable() = testScope.runTest { - setUpState( - isWalletServiceAvailable = false, - ) + setUpState(isWalletServiceAvailable = false) assertThat(underTest.getPickerScreenState()) .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice) @@ -195,9 +185,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun getPickerScreenState_disabledWhenTheFeatureIsNotEnabled() = testScope.runTest { - setUpState( - isWalletFeatureAvailable = false, - ) + setUpState(isWalletFeatureAvailable = false) assertThat(underTest.getPickerScreenState()) .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java) @@ -206,9 +194,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun getPickerScreenState_disabledWhenThereIsNoCard() = testScope.runTest { - setUpState( - hasSelectedCard = false, - ) + setUpState(hasSelectedCard = false) assertThat(underTest.getPickerScreenState()) .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java) @@ -219,7 +205,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { isWalletServiceAvailable: Boolean = true, isWalletQuerySuccessful: Boolean = true, hasSelectedCard: Boolean = true, - cardType: Int = WalletCard.CARD_TYPE_UNKNOWN + cardType: Int = WalletCard.CARD_TYPE_UNKNOWN, ) { val walletClient: QuickAccessWalletClient = mock() whenever(walletClient.tileIcon).thenReturn(ICON) @@ -242,11 +228,11 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { /*cardType= */ cardType, /*cardImage= */ mock(), /*contentDescription= */ CARD_DESCRIPTION, - /*pendingIntent= */ mock() + /*pendingIntent= */ mock(), ) .build() ), - 0 + 0, ) } else { GetWalletCardsResponse(emptyList(), 0) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 46d1ebe75899..9de0215022e9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -764,6 +764,28 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { assertThat(launchingAffordance).isFalse() } + @Test + fun onQuickAffordanceTriggered_updatesLaunchingFromTriggeredResult() = + testScope.runTest { + // WHEN selecting and triggering a quick affordance at a slot + val key = homeControls.key + val slot = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + val encodedKey = "$slot::$key" + val actionLaunched = true + homeControls.onTriggeredResult = + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(actionLaunched) + underTest.select(slot, key) + runCurrent() + underTest.onQuickAffordanceTriggered(encodedKey, expandable = null, slot) + + // THEN the latest triggered result shows that an action launched for the same key and + // slot + val launchingFromTriggeredResult by + collectLastValue(underTest.launchingFromTriggeredResult) + assertThat(launchingFromTriggeredResult?.launched).isEqualTo(actionLaunched) + assertThat(launchingFromTriggeredResult?.configKey).isEqualTo(encodedKey) + } + companion object { private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 private val ICON: Icon = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt index ad5eeabf83d2..26fe379f00bf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope import com.android.systemui.scene.data.repository.sceneContainerRepository +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -118,6 +119,50 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { ) } + /** STL: Ls -> overlay, then settle with Idle(overlay). */ + @Test + fun transition_overlay_from_ls_scene_end_in_gone() = + testScope.runTest { + sceneTransitions.value = + ObservableTransitionState.Transition.ShowOrHideOverlay( + overlay = Overlays.NotificationsShade, + fromContent = Scenes.Lockscreen, + toContent = Overlays.NotificationsShade, + currentScene = Scenes.Lockscreen, + currentOverlays = flowOf(emptySet()), + progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0f, + ) + + progress.value = 0.4f + assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f) + + sceneTransitions.value = + ObservableTransitionState.Idle( + Scenes.Lockscreen, + setOf(Overlays.NotificationsShade), + ) + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + /** * STL: Ls -> Gone, then settle with Idle(Ls). KTF in this scenario needs to invert the * transition LS -> UNDEFINED to UNDEFINED -> LS as there is no mechanism in KTF to @@ -259,6 +304,47 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { ) } + /** STL: Ls with overlay, then settle with Idle(Ls). */ + @Test + fun transition_overlay_to_ls_scene_end_in_ls() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = + ObservableTransitionState.Transition.ShowOrHideOverlay( + overlay = Overlays.NotificationsShade, + fromContent = Overlays.NotificationsShade, + toContent = Scenes.Lockscreen, + currentScene = Scenes.Lockscreen, + currentOverlays = flowOf(setOf(Overlays.NotificationsShade)), + progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0f, + ) + + progress.value = 0.4f + assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f) + + sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen) + + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + /** STL: Gone -> Ls (AOD), will transition to AOD once */ @Test fun transition_to_ls_scene_with_changed_next_scene_is_respected_just_once() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt index 0e3b03f74c02..be504cc0f704 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt @@ -18,11 +18,8 @@ package com.android.systemui.keyguard.ui.viewmodel -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags as AConfigFlags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.DisableSceneContainer @@ -75,7 +72,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { private val burnInFlow = MutableStateFlow(BurnInModel()) @Before - @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) @DisableSceneContainer fun setUp() { MockitoAnnotations.initMocks(this) @@ -219,50 +215,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun translationAndScale_whenFullyDozing_MigrationFlagOff_staysOutOfTopInset() = - testScope.runTest { - underTest.updateBurnInParams(burnInParameters.copy(minViewY = 100, topInset = 80)) - val movement by collectLastValue(underTest.movement) - assertThat(movement?.translationX).isEqualTo(0) - - // Set to dozing (on AOD) - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.GONE, - to = KeyguardState.AOD, - value = 1f, - transitionState = TransitionState.FINISHED, - ), - validateStep = false, - ) - - // Trigger a change to the burn-in model - burnInFlow.value = BurnInModel(translationX = 20, translationY = -30, scale = 0.5f) - assertThat(movement?.translationX).isEqualTo(20) - // -20 instead of -30, due to inset of 80 - assertThat(movement?.translationY).isEqualTo(-20) - assertThat(movement?.scale).isEqualTo(0.5f) - assertThat(movement?.scaleClockOnly).isEqualTo(true) - - // Set to the beginning of GONE->AOD transition - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.GONE, - to = KeyguardState.AOD, - value = 0f, - transitionState = TransitionState.STARTED, - ), - validateStep = false, - ) - assertThat(movement?.translationX).isEqualTo(0) - assertThat(movement?.translationY).isEqualTo(0) - assertThat(movement?.scale).isEqualTo(1f) - assertThat(movement?.scaleClockOnly).isEqualTo(true) - } - - @Test - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_whenFullyDozing_MigrationFlagOn_staysOutOfTopInset() = testScope.runTest { underTest.updateBurnInParams(burnInParameters.copy(minViewY = 100, topInset = 80)) @@ -334,7 +286,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { @Test @DisableSceneContainer - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_sceneContainerOff_weatherLargeClock() = testBurnInViewModelForClocks( isSmallClock = false, @@ -344,7 +295,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { @Test @DisableSceneContainer - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_sceneContainerOff_weatherSmallClock() = testBurnInViewModelForClocks( isSmallClock = true, @@ -354,7 +304,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { @Test @DisableSceneContainer - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_sceneContainerOff_nonWeatherLargeClock() = testBurnInViewModelForClocks( isSmallClock = false, @@ -364,7 +313,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { @Test @DisableSceneContainer - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_sceneContainerOff_nonWeatherSmallClock() = testBurnInViewModelForClocks( isSmallClock = true, @@ -373,7 +321,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) @EnableSceneContainer fun translationAndScale_sceneContainerOn_weatherLargeClock() = testBurnInViewModelForClocks( @@ -383,7 +330,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) @EnableSceneContainer fun translationAndScale_sceneContainerOn_weatherSmallClock() = testBurnInViewModelForClocks( @@ -393,7 +339,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) @EnableSceneContainer fun translationAndScale_sceneContainerOn_nonWeatherLargeClock() = testBurnInViewModelForClocks( @@ -403,7 +348,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) @EnableSceneContainer @Ignore("b/367659687") fun translationAndScale_sceneContainerOn_nonWeatherSmallClock() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index 95ffc962797d..789477e38b55 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -19,12 +19,10 @@ package com.android.systemui.keyguard.ui.viewmodel -import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.view.View import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState -import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.shared.model.CommunalScenes @@ -73,7 +71,6 @@ import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) -@EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt index 056efb34a0b1..c47a412e226a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt @@ -117,7 +117,6 @@ class QSTileViewModelImplTest : SysuiTestCase() { "test_spec:\n" + " QSTileState(" + "icon=Resource(res=0, contentDescription=Resource(res=0)), " + - "iconRes=null, " + "label=test_data, " + "activationState=INACTIVE, " + "secondaryLabel=null, " + diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt index 00460bfe83b2..557f4ea262a3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt @@ -93,8 +93,7 @@ class AirplaneModeMapperTest : SysuiTestCase() { ): QSTileState { val label = context.getString(R.string.airplane_mode) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt index 632aae035ede..24e46686e23d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt @@ -178,8 +178,7 @@ class AlarmTileMapperTest : SysuiTestCase() { ): QSTileState { val label = context.getString(R.string.status_bar_alarm) return QSTileState( - Icon.Loaded(context.getDrawable(R.drawable.ic_alarm)!!, null), - R.drawable.ic_alarm, + Icon.Loaded(context.getDrawable(R.drawable.ic_alarm)!!, null, R.drawable.ic_alarm), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt index 5385f945946c..2ddaddd5ad35 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt @@ -253,8 +253,7 @@ class BatterySaverTileMapperTest : SysuiTestCase() { ): QSTileState { val label = context.getString(R.string.battery_detail_switch_title) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt index 356b98eb192e..a3c159820a94 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt @@ -77,8 +77,11 @@ class ColorCorrectionTileMapperTest : SysuiTestCase() { ): QSTileState { val label = context.getString(R.string.quick_settings_color_correction_label) return QSTileState( - Icon.Loaded(context.getDrawable(R.drawable.ic_qs_color_correction)!!, null), - R.drawable.ic_qs_color_correction, + Icon.Loaded( + context.getDrawable(R.drawable.ic_qs_color_correction)!!, + null, + R.drawable.ic_qs_color_correction, + ), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt index 8236c4c1e638..608adf183163 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt @@ -253,7 +253,6 @@ class CustomTileMapperTest : SysuiTestCase() { ): QSTileState { return QSTileState( icon?.let { com.android.systemui.common.shared.model.Icon.Loaded(icon, null) }, - null, "test label", activationState, "test subtitle", diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt index 587585ccee2e..a115c1235210 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt @@ -73,7 +73,11 @@ class FlashlightMapperTest : SysuiTestCase() { mapper.map(qsTileConfig, FlashlightTileModel.FlashlightAvailable(true)) val expectedIcon = - Icon.Loaded(context.getDrawable(R.drawable.qs_flashlight_icon_on)!!, null) + Icon.Loaded( + context.getDrawable(R.drawable.qs_flashlight_icon_on)!!, + null, + R.drawable.qs_flashlight_icon_on, + ) val actualIcon = tileState.icon assertThat(actualIcon).isEqualTo(expectedIcon) } @@ -84,7 +88,11 @@ class FlashlightMapperTest : SysuiTestCase() { mapper.map(qsTileConfig, FlashlightTileModel.FlashlightAvailable(false)) val expectedIcon = - Icon.Loaded(context.getDrawable(R.drawable.qs_flashlight_icon_off)!!, null) + Icon.Loaded( + context.getDrawable(R.drawable.qs_flashlight_icon_off)!!, + null, + R.drawable.qs_flashlight_icon_off, + ) val actualIcon = tileState.icon assertThat(actualIcon).isEqualTo(expectedIcon) } @@ -95,7 +103,11 @@ class FlashlightMapperTest : SysuiTestCase() { mapper.map(qsTileConfig, FlashlightTileModel.FlashlightTemporarilyUnavailable) val expectedIcon = - Icon.Loaded(context.getDrawable(R.drawable.qs_flashlight_icon_off)!!, null) + Icon.Loaded( + context.getDrawable(R.drawable.qs_flashlight_icon_off)!!, + null, + R.drawable.qs_flashlight_icon_off, + ) val actualIcon = tileState.icon assertThat(actualIcon).isEqualTo(expectedIcon) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt index e81771ec38d5..8f8f7105415f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt @@ -58,8 +58,11 @@ class FontScalingTileMapperTest : SysuiTestCase() { private fun createFontScalingTileState(): QSTileState = QSTileState( - Icon.Loaded(context.getDrawable(R.drawable.ic_qs_font_scaling)!!, null), - R.drawable.ic_qs_font_scaling, + Icon.Loaded( + context.getDrawable(R.drawable.ic_qs_font_scaling)!!, + null, + R.drawable.ic_qs_font_scaling, + ), context.getString(R.string.quick_settings_font_scaling_label), QSTileState.ActivationState.ACTIVE, null, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapperTest.kt index 12d604ff6a7c..3d3447da15a1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapperTest.kt @@ -102,8 +102,7 @@ class HearingDevicesTileMapperTest : SysuiTestCase() { val label = context.getString(R.string.quick_settings_hearing_devices_label) val iconRes = R.drawable.qs_hearing_devices_icon return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt index 9dcf49e02697..b087bbc29bf7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt @@ -82,7 +82,6 @@ class InternetTileMapperTest : SysuiTestCase() { QSTileState.ActivationState.ACTIVE, context.getString(R.string.quick_settings_networks_available), Icon.Loaded(signalDrawable, null), - null, context.getString(R.string.quick_settings_internet_label), ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) @@ -120,8 +119,11 @@ class InternetTileMapperTest : SysuiTestCase() { createInternetTileState( QSTileState.ActivationState.ACTIVE, inputModel.secondaryLabel.loadText(context).toString(), - Icon.Loaded(context.getDrawable(expectedSatIcon!!.res)!!, null), - expectedSatIcon.res, + Icon.Loaded( + context.getDrawable(expectedSatIcon!!.res)!!, + null, + expectedSatIcon.res, + ), expectedSatIcon.contentDescription.loadContentDescription(context).toString(), ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) @@ -144,8 +146,7 @@ class InternetTileMapperTest : SysuiTestCase() { createInternetTileState( QSTileState.ActivationState.ACTIVE, context.getString(R.string.quick_settings_networks_available), - Icon.Loaded(context.getDrawable(wifiRes)!!, contentDescription = null), - wifiRes, + Icon.Loaded(context.getDrawable(wifiRes)!!, null, wifiRes), context.getString(R.string.quick_settings_internet_label), ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) @@ -171,8 +172,8 @@ class InternetTileMapperTest : SysuiTestCase() { Icon.Loaded( context.getDrawable(R.drawable.ic_qs_no_internet_unavailable)!!, contentDescription = null, + R.drawable.ic_qs_no_internet_unavailable, ), - R.drawable.ic_qs_no_internet_unavailable, context.getString(R.string.quick_settings_networks_unavailable), ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) @@ -182,13 +183,11 @@ class InternetTileMapperTest : SysuiTestCase() { activationState: QSTileState.ActivationState, secondaryLabel: String, icon: Icon, - iconRes: Int? = null, contentDescription: String, ): QSTileState { val label = context.getString(R.string.quick_settings_internet_label) return QSTileState( icon, - iconRes, label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt index 30fce73e04da..780d58c83e5b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt @@ -90,8 +90,7 @@ class ColorInversionTileMapperTest : SysuiTestCase() { ): QSTileState { val label = context.getString(R.string.quick_settings_inversion_label) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapperTest.kt index 37e8a6053682..4ebe23dcdef1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapperTest.kt @@ -69,7 +69,12 @@ class LocationTileMapperTest : SysuiTestCase() { fun mapsEnabledDataToOnIconState() { val tileState: QSTileState = mapper.map(qsTileConfig, LocationTileModel(true)) - val expectedIcon = Icon.Loaded(context.getDrawable(R.drawable.qs_location_icon_on)!!, null) + val expectedIcon = + Icon.Loaded( + context.getDrawable(R.drawable.qs_location_icon_on)!!, + null, + R.drawable.qs_location_icon_on, + ) val actualIcon = tileState.icon Truth.assertThat(actualIcon).isEqualTo(expectedIcon) } @@ -78,7 +83,12 @@ class LocationTileMapperTest : SysuiTestCase() { fun mapsDisabledDataToOffIconState() { val tileState: QSTileState = mapper.map(qsTileConfig, LocationTileModel(false)) - val expectedIcon = Icon.Loaded(context.getDrawable(R.drawable.qs_location_icon_off)!!, null) + val expectedIcon = + Icon.Loaded( + context.getDrawable(R.drawable.qs_location_icon_off)!!, + null, + R.drawable.qs_location_icon_off, + ) val actualIcon = tileState.icon Truth.assertThat(actualIcon).isEqualTo(expectedIcon) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt index de3dc5730421..44e6b4d2d0f6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt @@ -28,6 +28,7 @@ import com.android.internal.R import com.android.settingslib.notification.modes.TestModeBuilder import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestableContext +import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.asIcon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues @@ -63,7 +64,7 @@ class ModesTileDataInteractorTest : SysuiTestCase() { fun setUp() { context.orCreateTestableResources.apply { addOverride(MODES_DRAWABLE_ID, MODES_DRAWABLE) - addOverride(R.drawable.ic_zen_mode_type_bedtime, BEDTIME_DRAWABLE) + addOverride(BEDTIME_DRAWABLE_ID, BEDTIME_DRAWABLE) } val customPackageContext = SysuiTestableContext(context) @@ -145,24 +146,24 @@ class ModesTileDataInteractorTest : SysuiTestCase() { // Tile starts with the generic Modes icon. runCurrent() assertThat(tileData?.icon).isEqualTo(MODES_ICON) - assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) + assertThat(tileData?.icon!!.res).isEqualTo(MODES_DRAWABLE_ID) // Add an inactive mode -> Still modes icon zenModeRepository.addMode(id = "Mode", active = false) runCurrent() assertThat(tileData?.icon).isEqualTo(MODES_ICON) - assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) + assertThat(tileData?.icon!!.res).isEqualTo(MODES_DRAWABLE_ID) // Add an active mode with a default icon: icon should be the mode icon, and the // iconResId is also populated, because we know it's a system icon. zenModeRepository.addMode( id = "Bedtime with default icon", type = AutomaticZenRule.TYPE_BEDTIME, - active = true + active = true, ) runCurrent() assertThat(tileData?.icon).isEqualTo(BEDTIME_ICON) - assertThat(tileData?.iconResId).isEqualTo(R.drawable.ic_zen_mode_type_bedtime) + assertThat(tileData?.icon!!.res).isEqualTo(BEDTIME_DRAWABLE_ID) // Add another, less-prioritized mode that has a *custom* icon: for now, icon should // remain the first mode icon @@ -177,20 +178,20 @@ class ModesTileDataInteractorTest : SysuiTestCase() { ) runCurrent() assertThat(tileData?.icon).isEqualTo(BEDTIME_ICON) - assertThat(tileData?.iconResId).isEqualTo(R.drawable.ic_zen_mode_type_bedtime) + assertThat(tileData?.icon!!.res).isEqualTo(BEDTIME_DRAWABLE_ID) // Deactivate more important mode: icon should be the less important, still active mode // And because it's a package-provided icon, iconResId is not populated. zenModeRepository.deactivateMode("Bedtime with default icon") runCurrent() assertThat(tileData?.icon).isEqualTo(CUSTOM_ICON) - assertThat(tileData?.iconResId).isNull() + assertThat(tileData?.icon!!.res).isNull() // Deactivate remaining mode: back to the default modes icon zenModeRepository.deactivateMode("Driving with custom icon") runCurrent() assertThat(tileData?.icon).isEqualTo(MODES_ICON) - assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) + assertThat(tileData?.icon!!.res).isEqualTo(MODES_DRAWABLE_ID) } @Test @@ -205,18 +206,18 @@ class ModesTileDataInteractorTest : SysuiTestCase() { runCurrent() assertThat(tileData?.icon).isEqualTo(MODES_ICON) - assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) + assertThat(tileData?.icon!!.res).isEqualTo(MODES_DRAWABLE_ID) // Activate a Mode -> Icon doesn't change. zenModeRepository.addMode(id = "Mode", active = true) runCurrent() assertThat(tileData?.icon).isEqualTo(MODES_ICON) - assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) + assertThat(tileData?.icon!!.res).isEqualTo(MODES_DRAWABLE_ID) zenModeRepository.deactivateMode(id = "Mode") runCurrent() assertThat(tileData?.icon).isEqualTo(MODES_ICON) - assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) + assertThat(tileData?.icon!!.res).isEqualTo(MODES_DRAWABLE_ID) } @EnableFlags(Flags.FLAG_MODES_UI) @@ -256,15 +257,17 @@ class ModesTileDataInteractorTest : SysuiTestCase() { val TEST_USER = UserHandle.of(1)!! const val CUSTOM_PACKAGE = "com.some.mode.owner.package" - val MODES_DRAWABLE_ID = R.drawable.ic_zen_priority_modes + const val MODES_DRAWABLE_ID = R.drawable.ic_zen_priority_modes const val CUSTOM_DRAWABLE_ID = 12345 + const val BEDTIME_DRAWABLE_ID = R.drawable.ic_zen_mode_type_bedtime + val MODES_DRAWABLE = TestStubDrawable("modes_icon") val BEDTIME_DRAWABLE = TestStubDrawable("bedtime") val CUSTOM_DRAWABLE = TestStubDrawable("custom") - val MODES_ICON = MODES_DRAWABLE.asIcon() - val BEDTIME_ICON = BEDTIME_DRAWABLE.asIcon() + val MODES_ICON = Icon.Loaded(MODES_DRAWABLE, null, MODES_DRAWABLE_ID) + val BEDTIME_ICON = Icon.Loaded(BEDTIME_DRAWABLE, null, BEDTIME_DRAWABLE_ID) val CUSTOM_ICON = CUSTOM_DRAWABLE.asIcon() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt index 88b00468573f..04e094f25f5d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt @@ -156,6 +156,10 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { } private fun modelOf(isActivated: Boolean, activeModeNames: List<String>): ModesTileModel { - return ModesTileModel(isActivated, activeModeNames, TestStubDrawable("icon").asIcon(), 123) + return ModesTileModel( + isActivated, + activeModeNames, + TestStubDrawable("icon").asIcon(res = 123), + ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt index 4e91d16bf1ec..d73044f6b479 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt @@ -99,18 +99,11 @@ class ModesTileMapperTest : SysuiTestCase() { @Test fun state_modelHasIconResId_includesIconResId() { - val icon = TestStubDrawable("res123").asIcon() - val model = - ModesTileModel( - isActivated = false, - activeModes = emptyList(), - icon = icon, - iconResId = 123, - ) + val icon = TestStubDrawable("res123").asIcon(res = 123) + val model = ModesTileModel(isActivated = false, activeModes = emptyList(), icon = icon) val state = underTest.map(config, model) assertThat(state.icon).isEqualTo(icon) - assertThat(state.iconRes).isEqualTo(123) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt index 1457f533f5ec..7c853261aa1c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt @@ -289,8 +289,7 @@ class NightDisplayTileMapperTest : SysuiTestCase() { if (TextUtils.isEmpty(secondaryLabel)) label else TextUtils.concat(label, ", ", secondaryLabel) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapperTest.kt index 2ac3e081b8f4..b6caa22a3012 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapperTest.kt @@ -58,8 +58,11 @@ class NotesTileMapperTest : SysuiTestCase() { private fun createNotesTileState(): QSTileState = QSTileState( - Icon.Loaded(context.getDrawable(R.drawable.ic_qs_notes)!!, null), - R.drawable.ic_qs_notes, + Icon.Loaded( + context.getDrawable(R.drawable.ic_qs_notes)!!, + null, + R.drawable.ic_qs_notes, + ), context.getString(R.string.quick_settings_notes_label), QSTileState.ActivationState.INACTIVE, /* secondaryLabel= */ null, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt index 7782d2b279a8..5b39810e3477 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt @@ -66,11 +66,7 @@ class OneHandedModeTileMapperTest : SysuiTestCase() { val outputState = mapper.map(config, inputModel) val expectedState = - createOneHandedModeTileState( - QSTileState.ActivationState.INACTIVE, - subtitleArray[1], - com.android.internal.R.drawable.ic_qs_one_handed_mode, - ) + createOneHandedModeTileState(QSTileState.ActivationState.INACTIVE, subtitleArray[1]) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } @@ -81,23 +77,21 @@ class OneHandedModeTileMapperTest : SysuiTestCase() { val outputState = mapper.map(config, inputModel) val expectedState = - createOneHandedModeTileState( - QSTileState.ActivationState.ACTIVE, - subtitleArray[2], - com.android.internal.R.drawable.ic_qs_one_handed_mode, - ) + createOneHandedModeTileState(QSTileState.ActivationState.ACTIVE, subtitleArray[2]) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } private fun createOneHandedModeTileState( activationState: QSTileState.ActivationState, secondaryLabel: String, - iconRes: Int, ): QSTileState { val label = context.getString(R.string.quick_settings_onehanded_label) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded( + context.getDrawable(com.android.internal.R.drawable.ic_qs_one_handed_mode)!!, + null, + com.android.internal.R.drawable.ic_qs_one_handed_mode, + ), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapperTest.kt index ed33250a3392..c572ff60b61a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapperTest.kt @@ -93,8 +93,8 @@ class QRCodeScannerTileMapperTest : SysuiTestCase() { Icon.Loaded( context.getDrawable(com.android.systemui.res.R.drawable.ic_qr_code_scanner)!!, null, + com.android.systemui.res.R.drawable.ic_qr_code_scanner, ), - com.android.systemui.res.R.drawable.ic_qr_code_scanner, label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapperTest.kt index 85111fd07663..00017f9059de 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapperTest.kt @@ -85,8 +85,7 @@ class ReduceBrightColorsTileMapperTest : SysuiTestCase() { R.drawable.qs_extra_dim_icon_on else R.drawable.qs_extra_dim_icon_off return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, context.resources diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapperTest.kt index 53671ba38eb6..74010143166b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapperTest.kt @@ -180,8 +180,7 @@ class RotationLockTileMapperTest : SysuiTestCase() { ): QSTileState { val label = context.getString(R.string.quick_settings_rotation_unlocked_label) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt index 9a450653aa8f..1fb5048dd4c9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt @@ -91,8 +91,7 @@ class DataSaverTileMapperTest : SysuiTestCase() { else context.resources.getStringArray(R.array.tile_states_saver)[0] return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/ui/ScreenRecordTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/ui/ScreenRecordTileMapperTest.kt index cd683c44a59c..363255695131 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/ui/ScreenRecordTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/ui/ScreenRecordTileMapperTest.kt @@ -110,8 +110,7 @@ class ScreenRecordTileMapperTest : SysuiTestCase() { val label = context.getString(R.string.quick_settings_screen_record_label) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapperTest.kt index c569403960d0..e4cd0e0ec215 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapperTest.kt @@ -146,8 +146,7 @@ class SensorPrivacyToggleTileMapperTest : SysuiTestCase() { else context.getString(R.string.quick_settings_mic_label) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt index 0d2ebe42b7ad..8f5f2d3e6689 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt @@ -69,8 +69,7 @@ class UiModeNightTileMapperTest : SysuiTestCase() { expandedAccessibilityClass: KClass<out View>? = Switch::class, ): QSTileState { return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt index 86321ea04703..2c81f39a03ec 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt @@ -109,8 +109,7 @@ class WorkModeTileMapperTest : SysuiTestCase() { val label = testLabel val iconRes = com.android.internal.R.drawable.stat_sys_managed_profile_status return QSTileState( - icon = Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes = iconRes, + icon = Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label = label, activationState = activationState, secondaryLabel = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index 165e943a0cc0..40f13bbbf908 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -16,10 +16,12 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository @@ -71,6 +73,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_noNotifs_empty() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -81,6 +84,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_notifMissingStatusBarChipIconView_empty() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -99,6 +103,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_onePromotedNotif_statusBarIconViewMatches() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -122,6 +127,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_onePromotedNotif_connectedDisplaysFlagEnabled_statusBarIconMatches() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -145,6 +151,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_onePromotedNotif_colorMatches() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -175,6 +182,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_onlyForPromotedNotifs() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -208,6 +216,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_connectedDisplaysFlagEnabled_onlyForPromotedNotifs() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -242,6 +251,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_hasShortCriticalText_usesTextInsteadOfTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -272,6 +282,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_noTime_isIconOnly() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -294,6 +305,36 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_basicTime_hiddenIfAutomaticallyPromoted() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = 6543L, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = mock<StatusBarIconView>(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_basicTime_isShortTimeDelta() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -322,6 +363,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_countUpTime_isTimer() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -349,6 +391,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_countDownTime_isTimer() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -376,6 +419,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_noHeadsUp_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -407,6 +451,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_hasHeadsUpByUser_onlyShowsIcon() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -442,6 +487,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_clickingChipNotifiesInteractor() = kosmos.runTest { val latest by collectLastValue(underTest.chips) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt new file mode 100644 index 000000000000..14787e169979 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.featurepods.popups.ui.viewmodel + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.chips.notification.shared.StatusBarPopupChips +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@EnableFlags(StatusBarPopupChips.FLAG_NAME) +@RunWith(AndroidJUnit4::class) +class StatusBarPopupChipsViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest = kosmos.statusBarPopupChipsViewModel + + @Test + fun popupChips_allHidden_empty() = + testScope.runTest { + val latest by collectLastValue(underTest.popupChips) + assertThat(latest).isEmpty() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index cdc8bc1e6cbb..a49a66fe26b2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -16,12 +16,18 @@ package com.android.systemui.statusbar.notification.row; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -36,7 +42,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Notification; +import android.app.Person; import android.content.Context; +import android.graphics.drawable.Icon; import android.os.AsyncTask; import android.os.CancellationSignal; import android.os.Handler; @@ -66,6 +74,7 @@ import com.android.systemui.statusbar.notification.promoted.shared.model.Promote import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; +import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction; import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor; import com.android.systemui.statusbar.policy.InflatedSmartReplyState; import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder; @@ -155,8 +164,8 @@ public class NotificationContentInflaterTest extends SysuiTestCase { @Test public void testIncreasedHeadsUpBeingUsed() { - BindParams params = new BindParams(); - params.usesIncreasedHeadsUpHeight = true; + BindParams params = new BindParams(false, false, /* usesIncreasedHeadsUpHeight */ true, + REDACTION_TYPE_NONE); Notification.Builder builder = spy(mBuilder); mNotificationInflater.inflateNotificationViews( mRow.getEntry(), @@ -166,14 +175,15 @@ public class NotificationContentInflaterTest extends SysuiTestCase { FLAG_CONTENT_VIEW_ALL, builder, mContext, + mContext, mSmartReplyStateInflater); verify(builder).createHeadsUpContentView(true); } @Test public void testIncreasedHeightBeingUsed() { - BindParams params = new BindParams(); - params.usesIncreasedHeight = true; + BindParams params = new BindParams(false, /* usesIncreasedHeight */ true, false, + REDACTION_TYPE_NONE); Notification.Builder builder = spy(mBuilder); mNotificationInflater.inflateNotificationViews( mRow.getEntry(), @@ -183,6 +193,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { FLAG_CONTENT_VIEW_ALL, builder, mContext, + mContext, mSmartReplyStateInflater); verify(builder).createContentView(true); } @@ -207,7 +218,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { mRow.getEntry().getSbn().getNotification().contentView = new RemoteViews(mContext.getPackageName(), com.android.systemui.res.R.layout.status_bar); inflateAndWait(true /* expectingException */, mNotificationInflater, FLAG_CONTENT_VIEW_ALL, - mRow); + REDACTION_TYPE_NONE, mRow); assertTrue(mRow.getPrivateLayout().getChildCount() == 0); verify(mRow, times(0)).onNotificationUpdated(); } @@ -227,7 +238,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { mRow.getEntry(), mRow, FLAG_CONTENT_VIEW_ALL, - new BindParams(), + new BindParams(false, false, false, REDACTION_TYPE_NONE), false /* forceInflate */, null /* callback */); Assert.assertNull(mRow.getEntry().getRunningTask()); @@ -287,7 +298,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { mBuilder.setCustomContentView(new RemoteViews(getContext().getPackageName(), R.layout.custom_view_dark)); RemoteViews decoratedMediaView = mBuilder.createContentView(); - Assert.assertFalse("The decorated media style doesn't allow a view to be reapplied!", + assertFalse("The decorated media style doesn't allow a view to be reapplied!", NotificationContentInflater.canReapplyRemoteView(mediaView, decoratedMediaView)); } @@ -385,7 +396,8 @@ public class NotificationContentInflaterTest extends SysuiTestCase { mRow.getPrivateLayout().removeAllViews(); mRow.getEntry().getSbn().getNotification().contentView = new RemoteViews(mContext.getPackageName(), R.layout.invalid_notification_height); - inflateAndWait(true, mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); + inflateAndWait(true, mNotificationInflater, FLAG_CONTENT_VIEW_ALL, REDACTION_TYPE_NONE, + mRow); assertEquals(0, mRow.getPrivateLayout().getChildCount()); verify(mRow, times(0)).onNotificationUpdated(); } @@ -455,16 +467,88 @@ public class NotificationContentInflaterTest extends SysuiTestCase { assertNull(mRow.getEntry().getPromotedNotificationContentModel()); } + @Test + @EnableFlags(LockscreenOtpRedaction.FLAG_NAME) + public void testSensitiveContentPublicView_messageStyle() throws Exception { + String displayName = "Display Name"; + String messageText = "Message Text"; + String contentText = "Content Text"; + Icon personIcon = Icon.createWithResource(mContext, + com.android.systemui.res.R.drawable.ic_person); + Person testPerson = new Person.Builder() + .setName(displayName) + .setIcon(personIcon) + .build(); + Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle(testPerson); + messagingStyle.addMessage(new Notification.MessagingStyle.Message(messageText, + System.currentTimeMillis(), testPerson)); + messagingStyle.setConversationType(Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL); + messagingStyle.setShortcutIcon(personIcon); + Notification messageNotif = new Notification.Builder(mContext).setSmallIcon( + com.android.systemui.res.R.drawable.ic_person).setStyle(messagingStyle).build(); + ExpandableNotificationRow row = mHelper.createRow(messageNotif); + inflateAndWait(false, mNotificationInflater, FLAG_CONTENT_VIEW_PUBLIC, + REDACTION_TYPE_SENSITIVE_CONTENT, row); + NotificationContentView publicView = row.getPublicLayout(); + assertNotNull(publicView); + // The display name should be included, but not the content or message text + assertFalse(hasText(publicView, messageText)); + assertFalse(hasText(publicView, contentText)); + assertTrue(hasText(publicView, displayName)); + } + + @Test + @EnableFlags(LockscreenOtpRedaction.FLAG_NAME) + public void testSensitiveContentPublicView_nonMessageStyle() throws Exception { + String contentTitle = "Content Title"; + String contentText = "Content Text"; + Notification notif = new Notification.Builder(mContext).setSmallIcon( + com.android.systemui.res.R.drawable.ic_person) + .setContentTitle(contentTitle) + .setContentText(contentText) + .build(); + ExpandableNotificationRow row = mHelper.createRow(notif); + inflateAndWait(false, mNotificationInflater, FLAG_CONTENT_VIEW_PUBLIC, + REDACTION_TYPE_SENSITIVE_CONTENT, row); + NotificationContentView publicView = row.getPublicLayout(); + assertNotNull(publicView); + assertFalse(hasText(publicView, contentText)); + assertTrue(hasText(publicView, contentTitle)); + + // The standard public view should not use the content title or text + inflateAndWait(false, mNotificationInflater, FLAG_CONTENT_VIEW_PUBLIC, + REDACTION_TYPE_PUBLIC, row); + publicView = row.getPublicLayout(); + assertFalse(hasText(publicView, contentText)); + assertFalse(hasText(publicView, contentTitle)); + } + + private static boolean hasText(ViewGroup parent, CharSequence text) { + for (int i = 0; i < parent.getChildCount(); i++) { + View child = parent.getChildAt(i); + if (child instanceof ViewGroup) { + if (hasText((ViewGroup) child, text)) { + return true; + } + } else if (child instanceof TextView) { + return ((TextView) child).getText().toString().contains(text); + } + } + return false; + } + private static void inflateAndWait(NotificationContentInflater inflater, @InflationFlag int contentToInflate, ExpandableNotificationRow row) throws Exception { - inflateAndWait(false /* expectingException */, inflater, contentToInflate, row); + inflateAndWait(false /* expectingException */, inflater, contentToInflate, + REDACTION_TYPE_NONE, row); } private static void inflateAndWait(boolean expectingException, NotificationContentInflater inflater, @InflationFlag int contentToInflate, + @RedactionType int redactionType, ExpandableNotificationRow row) throws Exception { CountDownLatch countDownLatch = new CountDownLatch(1); final ExceptionHolder exceptionHolder = new ExceptionHolder(); @@ -492,7 +576,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { row.getEntry(), row, contentToInflate, - new BindParams(), + new BindParams(false, false, false, redactionType), false /* forceInflate */, callback /* callback */); assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS)); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt index 9fb72fba4d71..f25ba2c93c65 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.row import android.app.Notification import android.app.Person import android.content.Context +import android.graphics.drawable.Icon import android.os.AsyncTask import android.os.Build import android.os.CancellationSignal @@ -34,6 +35,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.res.R +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT +import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.ConversationNotificationProcessor import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -45,6 +50,7 @@ import com.android.systemui.statusbar.notification.row.NotificationRowContentBin import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag @@ -110,6 +116,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { } } private val promotedNotificationContentExtractor = FakePromotedNotificationContentExtractor() + private val conversationNotificationProcessor: ConversationNotificationProcessor = mock() @Before fun setUp() { @@ -126,7 +133,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { NotificationRowContentBinderImpl( cache, mock(), - mock<ConversationNotificationProcessor>(), + conversationNotificationProcessor, mock(), smartReplyStateInflater, layoutInflaterFactoryProvider, @@ -138,14 +145,14 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @Test fun testIncreasedHeadsUpBeingUsed() { - val params = BindParams() - params.usesIncreasedHeadsUpHeight = true + val params = + BindParams(false, false, /* usesIncreasedHeadsUpHeight */ true, REDACTION_TYPE_NONE) val builder = spy(builder) notificationInflater.inflateNotificationViews( row.entry, row, params, - true /* inflateSynchronously */, + true, /* inflateSynchronously */ FLAG_CONTENT_VIEW_ALL, builder, mContext, @@ -157,14 +164,13 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @Test fun testIncreasedHeightBeingUsed() { - val params = BindParams() - params.usesIncreasedHeight = true + val params = BindParams(false, /* usesIncreasedHeight */ true, false, REDACTION_TYPE_NONE) val builder = spy(builder) notificationInflater.inflateNotificationViews( row.entry, row, params, - true /* inflateSynchronously */, + true, /* inflateSynchronously */ FLAG_CONTENT_VIEW_ALL, builder, mContext, @@ -193,15 +199,18 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { row.entry.sbn.notification.contentView = RemoteViews(mContext.packageName, R.layout.status_bar) inflateAndWait( - true /* expectingException */, + true, /* expectingException */ notificationInflater, FLAG_CONTENT_VIEW_ALL, + REDACTION_TYPE_NONE, row, ) Assert.assertTrue(row.privateLayout.childCount == 0) verify(row, times(0)).onNotificationUpdated() } + @Test fun testInflationOfSensitiveContentPublicView() {} + @Test fun testAsyncTaskRemoved() { row.entry.abortTask() @@ -217,8 +226,8 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { row.entry, row, FLAG_CONTENT_VIEW_ALL, - BindParams(), - false /* forceInflate */, + BindParams(false, false, false, REDACTION_TYPE_NONE), + false, /* forceInflate */ null, /* callback */ ) Assert.assertNull(row.entry.runningTask) @@ -431,7 +440,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { mContext.packageName, com.android.systemui.tests.R.layout.invalid_notification_height, ) - inflateAndWait(true, notificationInflater, FLAG_CONTENT_VIEW_ALL, row) + inflateAndWait(true, notificationInflater, FLAG_CONTENT_VIEW_ALL, REDACTION_TYPE_NONE, row) Assert.assertEquals(0, row.privateLayout.childCount.toLong()) verify(row, times(0)).onNotificationUpdated() } @@ -440,7 +449,13 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @Test fun testInflatePublicSingleLineView() { row.publicLayout.removeAllViews() - inflateAndWait(false, notificationInflater, FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE, row) + inflateAndWait( + false, + notificationInflater, + FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE, + REDACTION_TYPE_NONE, + row, + ) Assert.assertNotNull(row.publicLayout.mSingleLineView) Assert.assertTrue(row.publicLayout.mSingleLineView is HybridNotificationView) } @@ -448,12 +463,15 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @Test fun testInflatePublicSingleLineConversationView() { val testPerson = Person.Builder().setName("Person").build() + val style = Notification.MessagingStyle(testPerson) val messagingBuilder = Notification.Builder(mContext, "no-id") .setSmallIcon(R.drawable.ic_person) .setContentTitle("Title") .setContentText("Text") - .setStyle(Notification.MessagingStyle(testPerson)) + .setStyle(style) + whenever(conversationNotificationProcessor.processNotification(any(), any(), any())) + .thenReturn(style) val messagingRow = spy(testHelper.createRow(messagingBuilder.build())) messagingRow.publicLayout.removeAllViews() @@ -461,6 +479,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { false, notificationInflater, FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE, + REDACTION_TYPE_NONE, messagingRow, ) Assert.assertNotNull(messagingRow.publicLayout.mSingleLineView) @@ -530,6 +549,80 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { Assert.assertNull(row.entry.promotedNotificationContentModel) } + @Test + @Throws(java.lang.Exception::class) + @EnableFlags(LockscreenOtpRedaction.FLAG_NAME) + fun testSensitiveContentPublicView_messageStyle() { + val displayName = "Display Name" + val messageText = "Message Text" + val contentText = "Content Text" + val personIcon = Icon.createWithResource(mContext, R.drawable.ic_person) + val testPerson = Person.Builder().setName(displayName).setIcon(personIcon).build() + val messagingStyle = Notification.MessagingStyle(testPerson) + messagingStyle.addMessage( + Notification.MessagingStyle.Message(messageText, System.currentTimeMillis(), testPerson) + ) + messagingStyle.setConversationType(Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL) + messagingStyle.setShortcutIcon(personIcon) + val messageNotif = + Notification.Builder(mContext) + .setSmallIcon(R.drawable.ic_person) + .setStyle(messagingStyle) + .build() + val newRow: ExpandableNotificationRow = testHelper.createRow(messageNotif) + inflateAndWait( + false, + notificationInflater, + FLAG_CONTENT_VIEW_PUBLIC, + REDACTION_TYPE_SENSITIVE_CONTENT, + newRow, + ) + // The display name should be included, but not the content or message text + val publicView = newRow.publicLayout + Assert.assertNotNull(publicView) + Assert.assertFalse(hasText(publicView, messageText)) + Assert.assertFalse(hasText(publicView, contentText)) + Assert.assertTrue(hasText(publicView, displayName)) + } + + @Test + @Throws(java.lang.Exception::class) + @EnableFlags(LockscreenOtpRedaction.FLAG_NAME) + fun testSensitiveContentPublicView_nonMessageStyle() { + val contentTitle = "Content Title" + val contentText = "Content Text" + val notif = + Notification.Builder(mContext) + .setSmallIcon(R.drawable.ic_person) + .setContentTitle(contentTitle) + .setContentText(contentText) + .build() + val newRow: ExpandableNotificationRow = testHelper.createRow(notif) + inflateAndWait( + false, + notificationInflater, + FLAG_CONTENT_VIEW_PUBLIC, + REDACTION_TYPE_SENSITIVE_CONTENT, + newRow, + ) + var publicView = newRow.publicLayout + Assert.assertNotNull(publicView) + Assert.assertFalse(hasText(publicView, contentText)) + Assert.assertTrue(hasText(publicView, contentTitle)) + + // The standard public view should not use the content title or text + inflateAndWait( + false, + notificationInflater, + FLAG_CONTENT_VIEW_PUBLIC, + REDACTION_TYPE_PUBLIC, + newRow, + ) + publicView = newRow.publicLayout + Assert.assertFalse(hasText(publicView, contentText)) + Assert.assertFalse(hasText(publicView, contentTitle)) + } + private class ExceptionHolder { var exception: Exception? = null } @@ -568,13 +661,20 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @InflationFlag contentToInflate: Int, row: ExpandableNotificationRow, ) { - inflateAndWait(false /* expectingException */, inflater, contentToInflate, row) + inflateAndWait( + false /* expectingException */, + inflater, + contentToInflate, + REDACTION_TYPE_NONE, + row, + ) } private fun inflateAndWait( expectingException: Boolean, inflater: NotificationRowContentBinderImpl, @InflationFlag contentToInflate: Int, + @RedactionType redactionType: Int, row: ExpandableNotificationRow, ) { val countDownLatch = CountDownLatch(1) @@ -603,12 +703,26 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { row.entry, row, contentToInflate, - BindParams(), - false /* forceInflate */, + BindParams(false, false, false, redactionType), + false, /* forceInflate */ callback, /* callback */ ) Assert.assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS)) exceptionHolder.exception?.let { throw it } } + + fun hasText(parent: ViewGroup, text: CharSequence): Boolean { + for (i in 0 until parent.childCount) { + val child = parent.getChildAt(i) + if (child is ViewGroup) { + if (hasText(child, text)) { + return true + } + } else if (child is TextView) { + return child.text.toString().contains(text) + } + } + return false + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index f48fd3c998b1..6bdd86efa8c0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -241,7 +241,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S shadeTestUtil.setSplitShade(true) val horizontalPosition = checkNotNull(dimens).horizontalPosition - assertIs<HorizontalPosition.FloatAtEnd>(horizontalPosition) + assertIs<HorizontalPosition.FloatAtStart>(horizontalPosition) assertThat(horizontalPosition.width).isEqualTo(200) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt index b560c591af1e..1ee8005fb7ab 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt @@ -20,7 +20,9 @@ import android.view.WindowManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any @@ -31,7 +33,6 @@ import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.runner.RunWith @@ -43,7 +44,7 @@ import org.mockito.Mockito.verify @RunWithLooper(setAsMainLooper = true) class SystemUIBottomSheetDialogTest : SysuiTestCase() { - private val kosmos = testKosmos() + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val configurationController = mock<ConfigurationController>() private val config = mock<Configuration>() private val delegate = mock<DialogDelegate<Dialog>>() @@ -67,21 +68,17 @@ class SystemUIBottomSheetDialogTest : SysuiTestCase() { @Test fun onStart_registersConfigCallback() { - kosmos.testScope.runTest { + kosmos.runTest { dialog.show() - runCurrent() - verify(configurationController).addCallback(any()) } } @Test fun onStop_unregisterConfigCallback() { - kosmos.testScope.runTest { + kosmos.runTest { dialog.show() - runCurrent() dialog.dismiss() - runCurrent() verify(configurationController).removeCallback(any()) } @@ -89,14 +86,12 @@ class SystemUIBottomSheetDialogTest : SysuiTestCase() { @Test fun onConfigurationChanged_calledInDelegate() { - kosmos.testScope.runTest { + kosmos.runTest { dialog.show() - runCurrent() val captor = argumentCaptor<ConfigurationController.ConfigurationListener>() verify(configurationController).addCallback(capture(captor)) captor.value.onConfigChanged(config) - runCurrent() verify(delegate).onConfigurationChanged(any(), any()) } diff --git a/packages/SystemUI/res/drawable/hearing_devices_spinner_background.xml b/packages/SystemUI/res/drawable/hearing_devices_spinner_background.xml index dfefb9d166af..4b7be3512f53 100644 --- a/packages/SystemUI/res/drawable/hearing_devices_spinner_background.xml +++ b/packages/SystemUI/res/drawable/hearing_devices_spinner_background.xml @@ -27,18 +27,7 @@ <solid android:color="@android:color/transparent"/> </shape> </item> - <item - android:end="20dp" - android:gravity="end|center_vertical"> - <vector - android:width="@dimen/hearing_devices_preset_spinner_icon_size" - android:height="@dimen/hearing_devices_preset_spinner_icon_size" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?androidprv:attr/colorControlNormal"> - <path - android:fillColor="#FF000000" - android:pathData="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z" /> - </vector> - </item> + <item android:end="20dp" + android:gravity="end|center_vertical" + android:drawable="@drawable/ic_hearing_device_expand" /> </layer-list>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_hearing_device_expand.xml b/packages/SystemUI/res/drawable/ic_hearing_device_expand.xml new file mode 100644 index 000000000000..fdfe7134a748 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_hearing_device_expand.xml @@ -0,0 +1,27 @@ +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="@androidprv:color/materialColorOnSurface"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/hearing_device_ambient_volume_layout.xml b/packages/SystemUI/res/layout/hearing_device_ambient_volume_layout.xml new file mode 100644 index 000000000000..fd409a5a8bb1 --- /dev/null +++ b/packages/SystemUI/res/layout/hearing_device_ambient_volume_layout.xml @@ -0,0 +1,67 @@ +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/ambient_header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/bluetooth_dialog_layout_margin" + android:layout_marginEnd="@dimen/bluetooth_dialog_layout_margin" + android:gravity="center_vertical" + android:orientation="horizontal"> + <ImageView + android:id="@+id/ambient_volume_icon" + android:layout_width="48dp" + android:layout_height="48dp" + android:padding="12dp" + android:contentDescription="@string/hearing_devices_ambient_unmute" + android:src="@drawable/ic_ambient_volume" + android:tint="@androidprv:color/materialColorOnSurface" /> + <TextView + android:id="@+id/ambient_title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingStart="10dp" + android:text="@string/hearing_devices_ambient_label" + android:textAppearance="@style/TextAppearance.Dialog.Title" + android:textDirection="locale" + android:textSize="16sp" + android:gravity="center_vertical" + android:fontFamily="@*android:string/config_headlineFontFamilyMedium" /> + <ImageView + android:id="@+id/ambient_expand_icon" + android:layout_width="48dp" + android:layout_height="48dp" + android:padding="10dp" + android:contentDescription="@string/hearing_devices_ambient_expand_controls" + android:src="@drawable/ic_hearing_device_expand" + android:tint="@androidprv:color/materialColorOnSurface" /> + </LinearLayout> + <LinearLayout + android:id="@+id/ambient_control_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" /> + +</LinearLayout> diff --git a/packages/SystemUI/res/layout/hearing_device_ambient_volume_slider.xml b/packages/SystemUI/res/layout/hearing_device_ambient_volume_slider.xml new file mode 100644 index 000000000000..44ada8943b12 --- /dev/null +++ b/packages/SystemUI/res/layout/hearing_device_ambient_volume_slider.xml @@ -0,0 +1,46 @@ +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/bluetooth_dialog_layout_margin" + android:layout_marginEnd="@dimen/bluetooth_dialog_layout_margin" + android:orientation="vertical"> + + <TextView + android:id="@+id/ambient_volume_slider_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:paddingStart="@dimen/hearing_devices_small_title_padding_horizontal" + android:textAppearance="@style/TextAppearance.Dialog.Title" + android:textDirection="locale" + android:textSize="14sp" + android:labelFor="@+id/ambient_volume_slider" + android:gravity="center_vertical" /> + <com.google.android.material.slider.Slider + style="@style/SystemUI.Material3.Slider" + android:id="@+id/ambient_volume_slider" + android:layout_width="match_parent" + android:layout_height="@dimen/bluetooth_dialog_device_height" + android:layout_gravity="center_vertical" + android:theme="@style/Theme.Material3.DayNight" + app:labelBehavior="gone" /> + +</LinearLayout> diff --git a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml index bf04a6f64d6a..949a6abb9b9d 100644 --- a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml +++ b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml @@ -85,13 +85,22 @@ android:longClickable="false"/> </LinearLayout> + <com.android.systemui.accessibility.hearingaid.AmbientVolumeLayout + android:id="@+id/ambient_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/preset_layout" + android:layout_marginTop="@dimen/hearing_devices_layout_margin" /> + <LinearLayout android:id="@+id/tools_layout" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@id/preset_layout" + app:layout_constraintTop_toBottomOf="@id/ambient_layout" android:layout_marginTop="@dimen/hearing_devices_layout_margin" android:orientation="vertical"> <TextView diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml index ab0f788dbb13..ec24c3df36a8 100644 --- a/packages/SystemUI/res/values-sw600dp/config.xml +++ b/packages/SystemUI/res/values-sw600dp/config.xml @@ -23,6 +23,9 @@ <!-- The maximum number of rows in the QuickSettings --> <integer name="quick_settings_max_rows">4</integer> + <!-- The number of columns in the Split Shade QuickSettings --> + <integer name="quick_settings_split_shade_num_columns">6</integer> + <!-- Use collapsed layout for media player in landscape QQS --> <bool name="config_quickSettingsMediaLandscapeCollapsed">false</bool> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index d3ee63ba0dd1..c3d84ff39485 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1004,6 +1004,20 @@ <string name="hearing_devices_preset_label">Preset</string> <!-- QuickSettings: Content description for the icon that indicates the item is selected [CHAR LIMIT=NONE]--> <string name="hearing_devices_spinner_item_selected">Selected</string> + <!-- QuickSettings: Title for ambient controls. [CHAR LIMIT=40]--> + <string name="hearing_devices_ambient_label">Surroundings</string> + <!-- QuickSettings: The text to show the control is for left side device. [CHAR LIMIT=30] --> + <string name="hearing_devices_ambient_control_left">Left</string> + <!-- QuickSettings: The text to show the control is for right side device. [CHAR LIMIT=30] --> + <string name="hearing_devices_ambient_control_right">Right</string> + <!-- QuickSettings: Content description for a button, that expands ambient volume sliders [CHAR_LIMIT=NONE] --> + <string name="hearing_devices_ambient_expand_controls">Expand to left and right separated controls</string> + <!-- QuickSettings: Content description for a button, that collapses ambient volume sliders [CHAR LIMIT=NONE] --> + <string name="hearing_devices_ambient_collapse_controls">Collapse to unified control</string> + <!-- QuickSettings: Content description for a button, that mute ambient volume [CHAR_LIMIT=NONE] --> + <string name="hearing_devices_ambient_mute">Mute surroundings</string> + <!-- QuickSettings: Content description for a button, that unmute ambient volume [CHAR LIMIT=NONE] --> + <string name="hearing_devices_ambient_unmute">Unmute surroundings</string> <!-- QuickSettings: Title for related tools of hearing. [CHAR LIMIT=40]--> <string name="hearing_devices_tools_label">Tools</string> <!-- QuickSettings: Tool name for hearing devices dialog related tools [CHAR LIMIT=40] [BACKUP_MESSAGE_ID=8916875614623730005]--> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index fc536bdb126b..6f13d637d5c5 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -20,6 +20,7 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; +import static com.android.systemui.Flags.glanceableHubBackAction; import static com.android.systemui.shared.Flags.shadeAllowBackGesture; import android.annotation.LongDef; @@ -352,6 +353,10 @@ public class QuickStepContract { } // Disable back gesture on the hub, but not when the shade is showing. if ((sysuiStateFlags & SYSUI_STATE_COMMUNAL_HUB_SHOWING) != 0) { + // Allow back gesture on Glanceable Hub with back action support. + if (glanceableHubBackAction()) { + return false; + } // Use QS expanded signal as the notification panel is always considered visible // expanded when on the lock screen and when opening hub over lock screen. This does // mean that back gesture is disabled when opening shade over hub while in portrait diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java index 1083136b570a..acfa08643b63 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java @@ -26,11 +26,9 @@ import android.hardware.display.DisplayManager; import android.media.MediaRouter; import android.media.MediaRouter.RouteInfo; import android.os.Trace; -import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import android.view.Display; -import android.view.DisplayAddress; import android.view.DisplayInfo; import android.view.View; import android.view.WindowManager; @@ -58,6 +56,9 @@ import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Provider; +/** + * Manages Keyguard Presentations for non-primary display(s). + */ @SysUISingleton public class KeyguardDisplayManager { protected static final String TAG = "KeyguardDisplayManager"; @@ -170,14 +171,17 @@ public class KeyguardDisplayManager { } return false; } - if (mKeyguardStateController.isOccluded() - && mDeviceStateHelper.isConcurrentDisplayActive(display)) { + + final boolean deviceStateOccludesKeyguard = + mDeviceStateHelper.isConcurrentDisplayActive(display) + || mDeviceStateHelper.isRearDisplayOuterDefaultActive(display); + if (mKeyguardStateController.isOccluded() && deviceStateOccludesKeyguard) { if (DEBUG) { // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the // Keyguard state becomes "occluded". In this case, we should not show the // KeyguardPresentation, since the activity is presenting content onto the // non-default display. - Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent" + Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent or rear" + " display is active"); } return false; @@ -326,44 +330,45 @@ public class KeyguardDisplayManager { public static class DeviceStateHelper implements DeviceStateManager.DeviceStateCallback { @Nullable - private final DisplayAddress.Physical mRearDisplayPhysicalAddress; - - // TODO(b/271317597): These device states should be defined in DeviceStateManager - private final int mConcurrentState; - private boolean mIsInConcurrentDisplayState; + private DeviceState mDeviceState; @Inject DeviceStateHelper( - @ShadeDisplayAware Context context, DeviceStateManager deviceStateManager, @Main Executor mainExecutor) { - - final String rearDisplayPhysicalAddress = context.getResources().getString( - com.android.internal.R.string.config_rearDisplayPhysicalAddress); - if (TextUtils.isEmpty(rearDisplayPhysicalAddress)) { - mRearDisplayPhysicalAddress = null; - } else { - mRearDisplayPhysicalAddress = DisplayAddress - .fromPhysicalDisplayId(Long.parseLong(rearDisplayPhysicalAddress)); - } - - mConcurrentState = context.getResources().getInteger( - com.android.internal.R.integer.config_deviceStateConcurrentRearDisplay); deviceStateManager.registerCallback(mainExecutor, this); } @Override public void onDeviceStateChanged(@NonNull DeviceState state) { - // When concurrent state ends, the display also turns off. This is enforced in various - // ExtensionRearDisplayPresentationTest CTS tests. So, we don't need to invoke - // hide() since that will happen through the onDisplayRemoved callback. - mIsInConcurrentDisplayState = state.getIdentifier() == mConcurrentState; + // When dual display or rear display mode ends, the display also turns off. This is + // enforced in various ExtensionRearDisplayPresentationTest CTS tests. So, we don't need + // to invoke hide() since that will happen through the onDisplayRemoved callback. + mDeviceState = state; + } + + /** + * @return true if the device is in Dual Display mode, and the specified display is the + * rear facing (outer) display. + */ + boolean isConcurrentDisplayActive(@NonNull Display display) { + return mDeviceState != null + && mDeviceState.hasProperty( + DeviceState.PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT) + && (display.getFlags() & Display.FLAG_REAR) != 0; } - boolean isConcurrentDisplayActive(Display display) { - return mIsInConcurrentDisplayState - && mRearDisplayPhysicalAddress != null - && mRearDisplayPhysicalAddress.equals(display.getAddress()); + /** + * @return true if the device is the updated Rear Display mode, and the specified display is + * the inner display. See {@link DeviceState.PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT}. + * Note that in this state, the outer display is the default display, while the inner + * display is the "rear" display. + */ + boolean isRearDisplayOuterDefaultActive(@NonNull Display display) { + return mDeviceState != null + && mDeviceState.hasProperty( + DeviceState.PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT) + && (display.getFlags() & Display.FLAG_REAR) != 0; } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt index 07bd813c2420..40a86dc3713e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt @@ -19,13 +19,12 @@ package com.android.keyguard import android.content.Context import android.view.View import com.android.systemui.customization.R as customR -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.ui.view.KeyguardRootView import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R -import com.android.systemui.shared.R as sharedR import com.android.systemui.shade.NotificationShadeWindowView import com.android.systemui.shade.ShadeDisplayAware +import com.android.systemui.shared.R as sharedR import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.END import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.START @@ -55,16 +54,17 @@ constructor( var statusViewCentered = false private val filterKeyguardAndSplitShadeOnly: () -> Boolean = { - statusBarStateController.getState() == KEYGUARD && !statusViewCentered } + statusBarStateController.getState() == KEYGUARD && !statusViewCentered + } private val filterKeyguard: () -> Boolean = { statusBarStateController.getState() == KEYGUARD } private val translateAnimator by lazy { - val smartSpaceViews = if (MigrateClocksToBlueprint.isEnabled) { - // Use scrollX instead of translationX as translation is already set by [AodBurnInLayer] - val scrollXTranslation = { view: View, translation: Float -> - view.scrollX = -translation.toInt() - } + // Use scrollX instead of translationX as translation is already set by [AodBurnInLayer] + val scrollXTranslation = { view: View, translation: Float -> + view.scrollX = -translation.toInt() + } + val smartSpaceViews = setOf( ViewIdToTranslate( viewId = sharedR.id.date_smartspace_view, @@ -83,18 +83,8 @@ constructor( direction = START, shouldBeAnimated = filterKeyguard, translateFunc = scrollXTranslation, - ) + ), ) - } else { - setOf(ViewIdToTranslate( - viewId = R.id.keyguard_status_area, - direction = START, - shouldBeAnimated = filterKeyguard, - translateFunc = { view, value -> - (view as? KeyguardStatusAreaView)?.translateXFromUnfold = value - } - )) - } UnfoldConstantTranslateAnimator( viewsIdToTranslate = @@ -102,39 +92,39 @@ constructor( ViewIdToTranslate( viewId = customR.id.lockscreen_clock_view_large, direction = START, - shouldBeAnimated = filterKeyguardAndSplitShadeOnly + shouldBeAnimated = filterKeyguardAndSplitShadeOnly, ), ViewIdToTranslate( viewId = customR.id.lockscreen_clock_view, direction = START, - shouldBeAnimated = filterKeyguard + shouldBeAnimated = filterKeyguard, ), ViewIdToTranslate( viewId = R.id.notification_stack_scroller, direction = END, - shouldBeAnimated = filterKeyguardAndSplitShadeOnly - ) + shouldBeAnimated = filterKeyguardAndSplitShadeOnly, + ), ) + smartSpaceViews, - progressProvider = unfoldProgressProvider + progressProvider = unfoldProgressProvider, ) } private val shortcutButtonsAnimator by lazy { UnfoldConstantTranslateAnimator( viewsIdToTranslate = - setOf( - ViewIdToTranslate( - viewId = R.id.start_button, - direction = START, - shouldBeAnimated = filterKeyguard + setOf( + ViewIdToTranslate( + viewId = R.id.start_button, + direction = START, + shouldBeAnimated = filterKeyguard, + ), + ViewIdToTranslate( + viewId = R.id.end_button, + direction = END, + shouldBeAnimated = filterKeyguard, + ), ), - ViewIdToTranslate( - viewId = R.id.end_button, - direction = END, - shouldBeAnimated = filterKeyguard - ) - ), - progressProvider = unfoldProgressProvider + progressProvider = unfoldProgressProvider, ) } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayout.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayout.java new file mode 100644 index 000000000000..7c141c1b561e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayout.java @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.hearingaid; + +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; + +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.settingslib.bluetooth.AmbientVolumeUi; +import com.android.systemui.res.R; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.primitives.Ints; + +import java.util.Map; + +/** + * A view of ambient volume controls. + * + * <p> It consists of a header with an expand icon and volume sliders for unified control and + * separated control for devices in the same set. Toggle the expand icon will make the UI switch + * between unified and separated control. + */ +public class AmbientVolumeLayout extends LinearLayout implements AmbientVolumeUi { + + @Nullable + private AmbientVolumeUiListener mListener; + private ImageView mExpandIcon; + private ImageView mVolumeIcon; + private boolean mExpandable = true; + private boolean mExpanded = false; + private boolean mMutable = false; + private boolean mMuted = false; + private final BiMap<Integer, AmbientVolumeSlider> mSideToSliderMap = HashBiMap.create(); + private int mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT; + + private final AmbientVolumeSlider.OnChangeListener mSliderOnChangeListener = + (slider, value) -> { + if (mListener != null) { + final int side = mSideToSliderMap.inverse().get(slider); + mListener.onSliderValueChange(side, value); + } + }; + + public AmbientVolumeLayout(@Nullable Context context) { + this(context, /* attrs= */ null); + } + + public AmbientVolumeLayout(@Nullable Context context, @Nullable AttributeSet attrs) { + this(context, attrs, /* defStyleAttr= */ 0); + } + + public AmbientVolumeLayout(@Nullable Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + this(context, attrs, defStyleAttr, /* defStyleRes= */ 0); + } + + public AmbientVolumeLayout(@Nullable Context context, @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + inflate(context, R.layout.hearing_device_ambient_volume_layout, /* root= */ this); + init(); + } + + private void init() { + mVolumeIcon = requireViewById(R.id.ambient_volume_icon); + mVolumeIcon.setImageResource(com.android.settingslib.R.drawable.ic_ambient_volume); + mVolumeIcon.setOnClickListener(v -> { + if (!mMutable) { + return; + } + setMuted(!mMuted); + if (mListener != null) { + mListener.onAmbientVolumeIconClick(); + } + }); + updateVolumeIcon(); + + mExpandIcon = requireViewById(R.id.ambient_expand_icon); + mExpandIcon.setOnClickListener(v -> { + setExpanded(!mExpanded); + if (mListener != null) { + mListener.onExpandIconClick(); + } + }); + updateExpandIcon(); + } + + @Override + public void setVisible(boolean visible) { + setVisibility(visible ? VISIBLE : GONE); + } + + @Override + public void setExpandable(boolean expandable) { + mExpandable = expandable; + if (!mExpandable) { + setExpanded(false); + } + updateExpandIcon(); + } + + @Override + public boolean isExpandable() { + return mExpandable; + } + + @Override + public void setExpanded(boolean expanded) { + if (!mExpandable && expanded) { + return; + } + mExpanded = expanded; + updateExpandIcon(); + updateLayout(); + } + + @Override + public boolean isExpanded() { + return mExpanded; + } + + @Override + public void setMutable(boolean mutable) { + mMutable = mutable; + if (!mMutable) { + mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT; + setMuted(false); + } + updateVolumeIcon(); + } + + @Override + public boolean isMutable() { + return mMutable; + } + + @Override + public void setMuted(boolean muted) { + if (!mMutable && muted) { + return; + } + mMuted = muted; + if (mMutable && mMuted) { + for (AmbientVolumeSlider slider : mSideToSliderMap.values()) { + slider.setValue(slider.getMin()); + } + } + updateVolumeIcon(); + } + + @Override + public boolean isMuted() { + return mMuted; + } + + @Override + public void setListener(@Nullable AmbientVolumeUiListener listener) { + mListener = listener; + } + + @Override + public void setupSliders(@NonNull Map<Integer, BluetoothDevice> sideToDeviceMap) { + sideToDeviceMap.forEach((side, device) -> createSlider(side)); + createSlider(SIDE_UNIFIED); + + LinearLayout controlContainer = requireViewById(R.id.ambient_control_container); + controlContainer.removeAllViews(); + if (!mSideToSliderMap.isEmpty()) { + for (int side : VALID_SIDES) { + final AmbientVolumeSlider slider = mSideToSliderMap.get(side); + if (slider != null) { + controlContainer.addView(slider); + } + } + } + updateLayout(); + } + + @Override + public void setSliderEnabled(int side, boolean enabled) { + AmbientVolumeSlider slider = mSideToSliderMap.get(side); + if (slider != null && slider.isEnabled() != enabled) { + slider.setEnabled(enabled); + updateLayout(); + } + } + + @Override + public void setSliderValue(int side, int value) { + AmbientVolumeSlider slider = mSideToSliderMap.get(side); + if (slider != null && slider.getValue() != value) { + slider.setValue(value); + updateVolumeLevel(); + } + } + + @Override + public void setSliderRange(int side, int min, int max) { + AmbientVolumeSlider slider = mSideToSliderMap.get(side); + if (slider != null) { + slider.setMin(min); + slider.setMax(max); + } + } + + @Override + public void updateLayout() { + mSideToSliderMap.forEach((side, slider) -> { + if (side == SIDE_UNIFIED) { + slider.setVisibility(mExpanded ? GONE : VISIBLE); + } else { + slider.setVisibility(mExpanded ? VISIBLE : GONE); + } + if (!slider.isEnabled()) { + slider.setValue(slider.getMin()); + } + }); + updateVolumeLevel(); + } + + private void updateVolumeLevel() { + int leftLevel, rightLevel; + if (mExpanded) { + leftLevel = getVolumeLevel(SIDE_LEFT); + rightLevel = getVolumeLevel(SIDE_RIGHT); + } else { + final int unifiedLevel = getVolumeLevel(SIDE_UNIFIED); + leftLevel = unifiedLevel; + rightLevel = unifiedLevel; + } + mVolumeLevel = Ints.constrainToRange(leftLevel * 5 + rightLevel, + AMBIENT_VOLUME_LEVEL_MIN, AMBIENT_VOLUME_LEVEL_MAX); + updateVolumeIcon(); + } + + private int getVolumeLevel(int side) { + AmbientVolumeSlider slider = mSideToSliderMap.get(side); + if (slider == null || !slider.isEnabled()) { + return 0; + } + return slider.getVolumeLevel(); + } + + private void updateExpandIcon() { + mExpandIcon.setVisibility(mExpandable ? VISIBLE : GONE); + mExpandIcon.setRotation(mExpanded ? ROTATION_EXPANDED : ROTATION_COLLAPSED); + if (mExpandable) { + final int stringRes = mExpanded ? R.string.hearing_devices_ambient_collapse_controls + : R.string.hearing_devices_ambient_expand_controls; + mExpandIcon.setContentDescription(mContext.getString(stringRes)); + } else { + mExpandIcon.setContentDescription(null); + } + } + + private void updateVolumeIcon() { + mVolumeIcon.setImageLevel(mMuted ? 0 : mVolumeLevel); + if (mMutable) { + final int stringRes = mMuted ? R.string.hearing_devices_ambient_unmute + : R.string.hearing_devices_ambient_mute; + mVolumeIcon.setContentDescription(mContext.getString(stringRes)); + mVolumeIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } else { + mVolumeIcon.setContentDescription(null); + mVolumeIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + } + } + + private void createSlider(int side) { + if (mSideToSliderMap.containsKey(side)) { + return; + } + AmbientVolumeSlider slider = new AmbientVolumeSlider(mContext); + slider.addOnChangeListener(mSliderOnChangeListener); + if (side == SIDE_LEFT) { + slider.setTitle(mContext.getString(R.string.hearing_devices_ambient_control_left)); + } else if (side == SIDE_RIGHT) { + slider.setTitle(mContext.getString(R.string.hearing_devices_ambient_control_right)); + } + mSideToSliderMap.put(side, slider); + } + + @VisibleForTesting + ImageView getVolumeIcon() { + return mVolumeIcon; + } + + @VisibleForTesting + ImageView getExpandIcon() { + return mExpandIcon; + } + + @VisibleForTesting + Map<Integer, AmbientVolumeSlider> getSliders() { + return mSideToSliderMap; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSlider.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSlider.java new file mode 100644 index 000000000000..92338ef3773c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSlider.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.hearingaid; + +import android.content.Context; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.systemui.res.R; + +import com.google.android.material.slider.Slider; + +import java.util.ArrayList; +import java.util.List; + +/** + * A view of ambient volume slider. + * <p> It consists by a title {@link TextView} with a volume control {@link Slider}. + */ +public class AmbientVolumeSlider extends LinearLayout { + + private final TextView mTitle; + private final Slider mSlider; + private final List<OnChangeListener> mChangeListeners = new ArrayList<>(); + private final Slider.OnSliderTouchListener mSliderTouchListener = + new Slider.OnSliderTouchListener() { + @Override + public void onStartTrackingTouch(@NonNull Slider slider) { + } + + @Override + public void onStopTrackingTouch(@NonNull Slider slider) { + final int value = Math.round(slider.getValue()); + for (OnChangeListener listener : mChangeListeners) { + listener.onValueChange(AmbientVolumeSlider.this, value); + } + } + }; + public AmbientVolumeSlider(@Nullable Context context) { + this(context, /* attrs= */ null); + } + + public AmbientVolumeSlider(@Nullable Context context, @Nullable AttributeSet attrs) { + this(context, attrs, /* defStyleAttr= */ 0); + } + + public AmbientVolumeSlider(@Nullable Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + this(context, attrs, defStyleAttr, /* defStyleRes= */ 0); + } + + public AmbientVolumeSlider(@Nullable Context context, @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + inflate(context, R.layout.hearing_device_ambient_volume_slider, /* root= */ this); + mTitle = requireViewById(R.id.ambient_volume_slider_title); + mSlider = requireViewById(R.id.ambient_volume_slider); + mSlider.addOnSliderTouchListener(mSliderTouchListener); + } + + /** + * Sets title for the ambient volume slider. + * <p> If text is null or empty, then {@link TextView} is hidden. + */ + public void setTitle(@Nullable String text) { + mTitle.setText(text); + mTitle.setVisibility(TextUtils.isEmpty(text) ? GONE : VISIBLE); + } + + /** Gets title for the ambient volume slider. */ + public CharSequence getTitle() { + return mTitle.getText(); + } + + /** + * Adds the callback to the ambient volume slider to get notified when the value is changed by + * user. + * <p> Note: The {@link OnChangeListener#onValueChange(AmbientVolumeSlider, int)} will be + * called when user's finger take off from the slider. + */ + public void addOnChangeListener(@Nullable OnChangeListener listener) { + if (listener == null) { + return; + } + mChangeListeners.add(listener); + } + + /** Sets max value to the ambient volume slider. */ + public void setMax(float max) { + mSlider.setValueTo(max); + } + + /** Gets max value from the ambient volume slider. */ + public float getMax() { + return mSlider.getValueTo(); + } + + /** Sets min value to the ambient volume slider. */ + public void setMin(float min) { + mSlider.setValueFrom(min); + } + + /** Gets min value from the ambient volume slider. */ + public float getMin() { + return mSlider.getValueFrom(); + } + + /** Sets value to the ambient volume slider. */ + public void setValue(float value) { + mSlider.setValue(value); + } + + /** Gets value from the ambient volume slider. */ + public float getValue() { + return mSlider.getValue(); + } + + /** Sets the enable state to the ambient volume slider. */ + public void setEnabled(boolean enabled) { + mSlider.setEnabled(enabled); + } + + /** Gets the enable state of the ambient volume slider. */ + public boolean isEnabled() { + return mSlider.isEnabled(); + } + + /** + * Gets the volume value of the ambient volume slider. + * <p> The volume level is divided into 5 levels: + * Level 0 corresponds to the minimum volume value. The range between the minimum and maximum + * volume is divided into 4 equal intervals, represented by levels 1 to 4. + */ + public int getVolumeLevel() { + if (!mSlider.isEnabled()) { + return 0; + } + final double min = mSlider.getValueFrom(); + final double max = mSlider.getValueTo(); + final double levelGap = (max - min) / 4.0; + final double value = mSlider.getValue(); + return (int) Math.ceil((value - min) / levelGap); + } + + /** Interface definition for a callback invoked when a slider's value is changed. */ + public interface OnChangeListener { + /** Called when the finger is take off from the slider. */ + void onValueChange(@NonNull AmbientVolumeSlider slider, int value); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java index 56435df1ad2c..73aabc3cf95a 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -52,10 +52,12 @@ import androidx.annotation.VisibleForTesting; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.android.settingslib.bluetooth.AmbientVolumeUiController; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.accessibility.hearingaid.HearingDevicesListAdapter.HearingDeviceItemCallback; import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.bluetooth.qsdialog.ActiveHearingDeviceItemFactory; @@ -108,7 +110,6 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, private SystemUIDialog mDialog; - private RecyclerView mDeviceList; private List<DeviceItem> mHearingDeviceItemList; private HearingDevicesListAdapter mDeviceListAdapter; @@ -134,6 +135,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, } }; + private AmbientVolumeUiController mAmbientController; + private final List<DeviceItemFactory> mHearingDeviceItemFactoryList = List.of( new ActiveHearingDeviceItemFactory(), new AvailableHearingDeviceItemFactory(), @@ -225,13 +228,17 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, public void onActiveDeviceChanged(@Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) { refreshDeviceUi(); - if (mPresetController != null) { - mPresetController.setDevice(getActiveHearingDevice()); - mMainHandler.post(() -> { + mMainHandler.post(() -> { + CachedBluetoothDevice device = getActiveHearingDevice(); + if (mPresetController != null) { + mPresetController.setDevice(device); mPresetLayout.setVisibility( mPresetController.isPresetControlAvailable() ? VISIBLE : GONE); - }); - } + } + if (mAmbientController != null) { + mAmbientController.loadDevice(device); + } + }); } @Override @@ -272,13 +279,13 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, } mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DIALOG_SHOW, mLaunchSourceId); - mDeviceList = dialog.requireViewById(R.id.device_list); - mPresetLayout = dialog.requireViewById(R.id.preset_layout); - mPresetSpinner = dialog.requireViewById(R.id.preset_spinner); setupDeviceListView(dialog); - setupPresetSpinner(dialog); setupPairNewDeviceButton(dialog); + setupPresetSpinner(dialog); + if (com.android.settingslib.flags.Flags.hearingDevicesAmbientVolumeControl()) { + setupAmbientControls(); + } if (com.android.systemui.Flags.hearingDevicesDialogRelatedTools()) { setupRelatedToolsView(dialog); } @@ -286,41 +293,50 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, @Override public void onStart(@NonNull SystemUIDialog dialog) { - if (mLocalBluetoothManager == null) { - return; - } - mLocalBluetoothManager.getEventManager().registerCallback(this); - if (mPresetController != null) { - mPresetController.registerHapCallback(); - } + ThreadUtils.postOnBackgroundThread(() -> { + if (mLocalBluetoothManager != null) { + mLocalBluetoothManager.getEventManager().registerCallback(this); + } + if (mPresetController != null) { + mPresetController.registerHapCallback(); + } + if (mAmbientController != null) { + mAmbientController.start(); + } + }); } @Override public void onStop(@NonNull SystemUIDialog dialog) { - if (mLocalBluetoothManager == null) { - return; - } - - if (mPresetController != null) { - mPresetController.unregisterHapCallback(); - } - mLocalBluetoothManager.getEventManager().unregisterCallback(this); + ThreadUtils.postOnBackgroundThread(() -> { + if (mLocalBluetoothManager != null) { + mLocalBluetoothManager.getEventManager().unregisterCallback(this); + } + if (mPresetController != null) { + mPresetController.unregisterHapCallback(); + } + if (mAmbientController != null) { + mAmbientController.stop(); + } + }); } private void setupDeviceListView(SystemUIDialog dialog) { - mDeviceList.setLayoutManager(new LinearLayoutManager(dialog.getContext())); + final RecyclerView deviceList = dialog.requireViewById(R.id.device_list); + deviceList.setLayoutManager(new LinearLayoutManager(dialog.getContext())); mHearingDeviceItemList = getHearingDeviceItemList(); mDeviceListAdapter = new HearingDevicesListAdapter(mHearingDeviceItemList, this); - mDeviceList.setAdapter(mDeviceListAdapter); + deviceList.setAdapter(mDeviceListAdapter); } private void setupPresetSpinner(SystemUIDialog dialog) { mPresetController = new HearingDevicesPresetsController(mProfileManager, mPresetCallback); mPresetController.setDevice(getActiveHearingDevice()); + mPresetSpinner = dialog.requireViewById(R.id.preset_spinner); mPresetInfoAdapter = new HearingDevicesSpinnerAdapter(dialog.getContext()); mPresetSpinner.setAdapter(mPresetInfoAdapter); - // disable redundant Touch & Hold accessibility action for Switch Access + // Disable redundant Touch & Hold accessibility action for Switch Access mPresetSpinner.setAccessibilityDelegate(new View.AccessibilityDelegate() { @Override public void onInitializeAccessibilityNodeInfo(@NonNull View host, @@ -349,12 +365,20 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, } }); + mPresetLayout = dialog.requireViewById(R.id.preset_layout); mPresetLayout.setVisibility(mPresetController.isPresetControlAvailable() ? VISIBLE : GONE); } + private void setupAmbientControls() { + final AmbientVolumeLayout ambientLayout = mDialog.requireViewById(R.id.ambient_layout); + mAmbientController = new AmbientVolumeUiController( + mDialog.getContext(), mLocalBluetoothManager, ambientLayout); + mAmbientController.setShowUiWhenLocalDataExist(false); + mAmbientController.loadDevice(getActiveHearingDevice()); + } + private void setupPairNewDeviceButton(SystemUIDialog dialog) { final Button pairButton = dialog.requireViewById(R.id.pair_new_device_button); - pairButton.setVisibility(mShowPairNewDevice ? VISIBLE : GONE); if (mShowPairNewDevice) { pairButton.setOnClickListener(v -> { diff --git a/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt index 232b62985ad0..47910f3d25bc 100644 --- a/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt @@ -21,8 +21,11 @@ import android.window.OnBackAnimationCallback import android.window.OnBackInvokedCallback import android.window.OnBackInvokedDispatcher import android.window.WindowOnBackInvokedDispatcher +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable +import com.android.systemui.Flags.glanceableHubBackAction import com.android.systemui.Flags.predictiveBackAnimateShade +import com.android.systemui.communal.domain.interactor.CommunalBackActionInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -35,7 +38,6 @@ import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch /** Handles requests to go back either from a button or gesture. */ @SysUISingleton @@ -50,6 +52,7 @@ constructor( private val windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor, private val shadeBackActionInteractor: ShadeBackActionInteractor, private val qsController: QuickSettingsController, + private val communalBackActionInteractor: CommunalBackActionInteractor, ) : CoreStartable { private var isCallbackRegistered = false @@ -114,6 +117,12 @@ constructor( if (shadeBackActionInteractor.closeUserSwitcherIfOpen()) { return true } + if (glanceableHubBackAction()) { + if (communalBackActionInteractor.canBeDismissed()) { + communalBackActionInteractor.onBackPressed() + return true + } + } if (shouldBackBeHandled()) { if (shadeBackActionInteractor.canBeCollapsed()) { // this is the Shade dismiss animation, so make sure QQS closes when it ends. diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt index aef5f1f422d1..e6f02457d320 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt @@ -21,14 +21,17 @@ import android.graphics.drawable.Drawable /** * Models an icon, that can either be already [loaded][Icon.Loaded] or be a [reference] - * [Icon.Resource] to a resource. + * [Icon.Resource] to a resource. In case of [Loaded], the resource ID [res] is optional. */ sealed class Icon { abstract val contentDescription: ContentDescription? - data class Loaded( + data class Loaded + @JvmOverloads + constructor( val drawable: Drawable, override val contentDescription: ContentDescription?, + @DrawableRes val res: Int? = null, ) : Icon() data class Resource( @@ -37,6 +40,11 @@ sealed class Icon { ) : Icon() } -/** Creates [Icon.Loaded] for a given drawable with an optional [contentDescription]. */ -fun Drawable.asIcon(contentDescription: ContentDescription? = null): Icon.Loaded = - Icon.Loaded(this, contentDescription) +/** + * Creates [Icon.Loaded] for a given drawable with an optional [contentDescription] and an optional + * [res]. + */ +fun Drawable.asIcon( + contentDescription: ContentDescription? = null, + @DrawableRes res: Int? = null, +): Icon.Loaded = Icon.Loaded(this, contentDescription, res) diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractor.kt new file mode 100644 index 000000000000..2ccf96abff79 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractor.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.domain.interactor + +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.scene.shared.model.Scenes +import javax.inject.Inject + +/** + * {@link CommunalBackActionInteractor} is responsible for handling back gestures on the glanceable + * hub. When invoked SystemUI should navigate back to the lockscreen. + */ +@SysUISingleton +class CommunalBackActionInteractor +@Inject +constructor( + private val communalInteractor: CommunalInteractor, + private val communalSceneInteractor: CommunalSceneInteractor, + private val sceneInteractor: SceneInteractor, +) { + fun canBeDismissed(): Boolean { + return communalInteractor.isCommunalShowing.value + } + + fun onBackPressed() { + if (SceneContainerFlag.isEnabled) { + // TODO(b/384610333): Properly determine whether to go to dream or lockscreen on back. + sceneInteractor.changeScene( + toScene = Scenes.Lockscreen, + loggingReason = "CommunalBackActionInteractor", + ) + } else { + communalSceneInteractor.changeScene( + newScene = CommunalScenes.Blank, + loggingReason = "CommunalBackActionInteractor", + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index ea428698e476..947113da0e60 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -285,7 +285,7 @@ constructor( * use [isIdleOnCommunal]. */ // TODO(b/323215860): rename to something more appropriate after cleaning up usages - val isCommunalShowing: Flow<Boolean> = + val isCommunalShowing: StateFlow<Boolean> = flow { emit(SceneContainerFlag.isEnabled) } .flatMapLatest { sceneContainerEnabled -> if (sceneContainerEnabled) { @@ -304,10 +304,10 @@ constructor( columnName = "isCommunalShowing", initialValue = false, ) - .shareIn( + .stateIn( scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - replay = 1, + started = SharingStarted.Eagerly, + initialValue = false, ) /** diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 9ae106c3ab39..014c0db618e1 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -267,6 +267,7 @@ public class FrameworkServicesModule { } @Provides + @Nullable @Singleton static VirtualDeviceManager provideVirtualDeviceManager(Context context) { return context.getSystemService(VirtualDeviceManager.class); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 571b37f43fd4..b272d65a8a11 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -54,6 +54,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.policy.PhoneWindow; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.systemui.Flags; import com.android.systemui.ambient.touch.TouchHandler; import com.android.systemui.ambient.touch.TouchMonitor; import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent; @@ -210,6 +211,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mCommunalVisible = communalVisible; updateLifecycleStateLocked(); + updateGestureBlockingLocked(); }); } }; @@ -585,7 +587,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private void updateGestureBlockingLocked() { final boolean shouldBlock = mStarted && !mShadeExpanded && !mBouncerShowing - && !isDreamInPreviewMode(); + && !isDreamInPreviewMode() + && !(Flags.glanceableHubBackAction() && mCommunalVisible); if (shouldBlock) { mGestureInteractor.addGestureBlockedMatcher(DREAM_TYPE_MATCHER, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index f549e64ca853..d0065c8b06c6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -39,7 +39,6 @@ import androidx.annotation.VisibleForTesting import androidx.core.math.MathUtils import com.android.app.animation.Interpolators import com.android.internal.R -import com.android.keyguard.KeyguardClockSwitchController import com.android.keyguard.KeyguardViewController import com.android.systemui.Flags.fasterUnlockTransition import com.android.systemui.dagger.SysUISingleton @@ -206,7 +205,7 @@ constructor( fun onUnlockAnimationFinished() {} } - /** The SmartSpace view on the lockscreen, provided by [KeyguardClockSwitchController]. */ + /** The SmartSpace view on the lockscreen. */ var lockscreenSmartspace: View? = null /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt index 74ee052f12b9..57f06fbd3bb5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt @@ -21,13 +21,13 @@ import android.app.StatusBarManager import android.app.admin.DevicePolicyManager import android.content.Context import android.content.pm.PackageManager -import com.android.systemui.res.R import com.android.systemui.animation.Expandable import com.android.systemui.camera.CameraGestureHelper import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.res.R import com.android.systemui.settings.UserTracker import com.android.systemui.shade.ShadeDisplayAware import dagger.Lazy @@ -65,7 +65,7 @@ constructor( icon = Icon.Resource( R.drawable.ic_camera, - ContentDescription.Resource(R.string.accessibility_camera_button) + ContentDescription.Resource(R.string.accessibility_camera_button), ) ) } else { @@ -88,7 +88,7 @@ constructor( cameraGestureHelper .get() .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(true) } private suspend fun isLaunchable(): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt index e8d3bfac6361..1b8baf657948 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt @@ -210,16 +210,16 @@ constructor( ): KeyguardQuickAffordanceConfig.OnTriggeredResult { return if (ModesUi.isEnabled) { if (!isAvailable.value) { - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } else { val dnd = interactor.dndMode.value if (dnd == null) { Log.wtf(TAG, "Triggered DND but it's null!?") - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } if (dnd.isActive) { interactor.deactivateMode(dnd) - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } else { if (interactor.shouldAskForZenDuration(dnd)) { // NOTE: The dialog handles turning on the mode itself. @@ -229,16 +229,16 @@ constructor( ) } else { interactor.activateMode(dnd) - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } } } } else { when { - !oldIsAvailable -> KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + !oldIsAvailable -> KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) zenMode != ZEN_MODE_OFF -> { controller.setZen(ZEN_MODE_OFF, null, TAG) - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } settingsValue == ZEN_DURATION_PROMPT -> @@ -249,12 +249,12 @@ constructor( settingsValue == ZEN_DURATION_FOREVER -> { controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG) - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } else -> { controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, conditionUri, TAG) - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt index 480ef5e19d8e..e2642a0964c1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt @@ -18,15 +18,14 @@ package com.android.systemui.keyguard.data.quickaffordance import android.content.Context -import com.android.systemui.res.R import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.shared.quickaffordance.ActivationState +import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.policy.FlashlightController import javax.inject.Inject @@ -50,9 +49,9 @@ constructor( KeyguardQuickAffordanceConfig.LockScreenState.Visible( Icon.Resource( R.drawable.qs_flashlight_icon_on, - ContentDescription.Resource(R.string.quick_settings_flashlight_label) + ContentDescription.Resource(R.string.quick_settings_flashlight_label), ), - ActivationState.Active + ActivationState.Active, ) } @@ -61,9 +60,9 @@ constructor( KeyguardQuickAffordanceConfig.LockScreenState.Visible( Icon.Resource( R.drawable.qs_flashlight_icon_off, - ContentDescription.Resource(R.string.quick_settings_flashlight_label) + ContentDescription.Resource(R.string.quick_settings_flashlight_label), ), - ActivationState.Inactive + ActivationState.Inactive, ) } @@ -92,14 +91,14 @@ constructor( } else { FlashlightState.OffAvailable.toLockScreenState() }, - TAG + TAG, ) } override fun onFlashlightError() { trySendWithFailureLogging( FlashlightState.OffAvailable.toLockScreenState(), - TAG + TAG, ) } @@ -114,7 +113,7 @@ constructor( FlashlightState.OffAvailable.toLockScreenState() } }, - TAG + TAG, ) } } @@ -130,7 +129,7 @@ constructor( flashlightController.setFlashlight( flashlightController.isAvailable && !flashlightController.isEnabled ) - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt index d335a1806a6d..06da281648a7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt @@ -111,7 +111,7 @@ constructor( transitionKey = CommunalTransitionKeys.SimpleFade, ) } - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(true) } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt index 1cf6183fec6c..ade65c38ff3c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt @@ -21,10 +21,10 @@ import android.app.AlertDialog import android.content.Context import android.content.Intent import android.net.Uri -import com.android.systemui.res.R import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon import com.android.systemui.keyguard.shared.quickaffordance.ActivationState +import com.android.systemui.res.R import kotlinx.coroutines.flow.Flow /** Defines interface that can act as data source for a single quick affordance model. */ @@ -71,7 +71,7 @@ interface KeyguardQuickAffordanceConfig { /** The picker shows the item for selecting this affordance as it normally would. */ data class Default( /** Optional [Intent] to use to start an activity to configure this affordance. */ - val configureIntent: Intent? = null, + val configureIntent: Intent? = null ) : PickerScreenState() /** @@ -134,34 +134,39 @@ interface KeyguardQuickAffordanceConfig { ) : LockScreenState() } - sealed class OnTriggeredResult { + sealed class OnTriggeredResult() { /** * Returning this as a result from the [onTriggered] method means that the implementation * has taken care of the action, the system will do nothing. + * + * @param[actionLaunched] Whether the implementation handled the action by launching a + * dialog or an activity. */ - object Handled : OnTriggeredResult() + data class Handled(val actionLaunched: Boolean) : OnTriggeredResult() /** * Returning this as a result from the [onTriggered] method means that the implementation * has _not_ taken care of the action and the system should start an activity using the * given [Intent]. */ - data class StartActivity( - val intent: Intent, - val canShowWhileLocked: Boolean, - ) : OnTriggeredResult() + data class StartActivity(val intent: Intent, val canShowWhileLocked: Boolean) : + OnTriggeredResult() /** * Returning this as a result from the [onTriggered] method means that the implementation * has _not_ taken care of the action and the system should show a Dialog using the given * [AlertDialog] and [Expandable]. */ - data class ShowDialog( - val dialog: AlertDialog, - val expandable: Expandable?, - ) : OnTriggeredResult() + data class ShowDialog(val dialog: AlertDialog, val expandable: Expandable?) : + OnTriggeredResult() } + /** + * Models an [OnTriggeredResult] that did or did not launch a dialog or activity for a given + * config key. + */ + data class LaunchingFromTriggeredResult(val launched: Boolean, val configKey: String) + companion object { /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt index 1358634a55f8..1c9bc9f39663 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt @@ -21,6 +21,7 @@ import android.content.Context import android.media.AudioManager import androidx.lifecycle.LiveData import androidx.lifecycle.Observer +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.common.shared.model.ContentDescription @@ -45,7 +46,6 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.withContext @SysUISingleton @@ -118,7 +118,7 @@ constructor( audioManager.ringerModeInternal = newRingerMode } } - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState = @@ -140,11 +140,11 @@ constructor( .getSharedPreferences( MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME, Context.MODE_PRIVATE, - userTracker.userId + userTracker.userId, ) .getInt( LAST_NON_SILENT_RINGER_MODE_KEY, - ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE + ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt index eafa1cea59f3..cb7702e090d0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt @@ -30,7 +30,6 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R @@ -72,21 +71,15 @@ constructor( override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) { val hasCards = getPaymentCards(response.walletCards)?.isNotEmpty() == true - trySendWithFailureLogging( - hasCards, - TAG, - ) + trySendWithFailureLogging(hasCards, TAG) } override fun onWalletCardRetrievalError(error: GetWalletCardsError) { Log.e( TAG, - "Wallet card retrieval error, message: \"${error?.message}\"" - ) - trySendWithFailureLogging( - null, - TAG, + "Wallet card retrieval error, message: \"${error?.message}\"", ) + trySendWithFailureLogging(null, TAG) } } @@ -94,7 +87,7 @@ constructor( callback, QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE, - QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE + QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE, ) withContext(backgroundDispatcher) { @@ -107,7 +100,7 @@ constructor( walletController.unregisterWalletChangeObservers( QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE, - QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE + QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE, ) } } @@ -117,11 +110,7 @@ constructor( if (hasCards == null) { KeyguardQuickAffordanceConfig.LockScreenState.Hidden } else { - state( - isWalletAvailable(), - hasCards, - walletController.walletClient.tileIcon, - ) + state(isWalletAvailable(), hasCards, walletController.walletClient.tileIcon) } flowOf(state) } @@ -135,28 +124,28 @@ constructor( explanation = context.getString( R.string.wallet_quick_affordance_unavailable_install_the_app - ), + ) ) queryCards().isEmpty() -> KeyguardQuickAffordanceConfig.PickerScreenState.Disabled( explanation = context.getString( R.string.wallet_quick_affordance_unavailable_configure_the_app - ), + ) ) else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default() } } override fun onTriggered( - expandable: Expandable?, + expandable: Expandable? ): KeyguardQuickAffordanceConfig.OnTriggeredResult { walletController.startQuickAccessUiIntent( activityStarter, expandable?.activityTransitionController(), /* hasCard= */ true, ) - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(true) } private suspend fun queryCards(): List<WalletCard> { @@ -199,10 +188,8 @@ constructor( Icon.Loaded( drawable = tileIcon, contentDescription = - ContentDescription.Resource( - res = R.string.accessibility_wallet_button, - ), - ), + ContentDescription.Resource(res = R.string.accessibility_wallet_button), + ) ) } else { KeyguardQuickAffordanceConfig.LockScreenState.Hidden diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index ae55825c9842..9c2daf52c5df 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -29,7 +29,6 @@ import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled import com.android.systemui.dock.DockManager @@ -62,6 +61,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine @@ -101,6 +101,14 @@ constructor( val launchingAffordance: StateFlow<Boolean> = repository.get().launchingAffordance.asStateFlow() /** + * Whether a [KeyguardQuickAffordanceConfig.OnTriggeredResult] indicated that the system + * launched an activity or showed a dialog. + */ + private val _launchingFromTriggeredResult = + MutableStateFlow<KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult?>(null) + val launchingFromTriggeredResult = _launchingFromTriggeredResult.asStateFlow() + + /** * Whether the UI should use the long press gesture to activate quick affordances. * * If `false`, the UI goes back to using single taps. @@ -187,18 +195,45 @@ constructor( metricsLogger.logOnShortcutTriggered(slotId, configKey) when (val result = config.onTriggered(expandable)) { - is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity -> + is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity -> { + setLaunchingFromTriggeredResult( + KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult( + launched = true, + configKey, + ) + ) launchQuickAffordance( intent = result.intent, canShowWhileLocked = result.canShowWhileLocked, expandable = expandable, ) - is KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled -> Unit - is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog -> + } + is KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled -> { + setLaunchingFromTriggeredResult( + KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult( + result.actionLaunched, + configKey, + ) + ) + } + is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog -> { + setLaunchingFromTriggeredResult( + KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult( + launched = true, + configKey, + ) + ) showDialog(result.dialog, result.expandable) + } } } + fun setLaunchingFromTriggeredResult( + launchingResult: KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult? + ) { + _launchingFromTriggeredResult.value = launchingResult + } + /** * Selects an affordance with the given ID on the slot with the given ID. * diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt index aa44b6d46289..382436cf9397 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.domain.interactor.scenetransition +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.systemui.CoreStartable @@ -38,7 +39,6 @@ import java.util.UUID import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import com.android.app.tracing.coroutines.launchTraced as launch /** * This class listens to scene framework scene transitions and manages keyguard transition framework @@ -111,7 +111,10 @@ constructor( if (currentTransitionId == null) return if (prevTransition !is ObservableTransitionState.Transition) return - if (idle.currentScene == prevTransition.toContent) { + if ( + idle.currentScene == prevTransition.toContent || + idle.currentOverlays.contains(prevTransition.toContent) + ) { finishCurrentTransition() } else { val targetState = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt index e7803c5e964c..a4a5ba691965 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt @@ -17,12 +17,23 @@ package com.android.systemui.keyguard.ui.binder import android.os.VibrationEffect +import com.android.systemui.Flags import kotlin.time.Duration.Companion.milliseconds object KeyguardBottomAreaVibrations { - val ShakeAnimationDuration = 300.milliseconds - const val ShakeAnimationCycles = 5f + val ShakeAnimationDuration = + if (Flags.msdlFeedback()) { + 285.milliseconds + } else { + 300.milliseconds + } + val ShakeAnimationCycles = + if (Flags.msdlFeedback()) { + 3f + } else { + 5f + } private const val SmallVibrationScale = 0.3f private const val BigVibrationScale = 0.6f @@ -32,7 +43,7 @@ object KeyguardBottomAreaVibrations { .apply { val vibrationDelayMs = (ShakeAnimationDuration.inWholeMilliseconds / (ShakeAnimationCycles * 2)) - .toInt() + .toInt() val vibrationCount = ShakeAnimationCycles.toInt() * 2 repeat(vibrationCount) { @@ -47,29 +58,13 @@ object KeyguardBottomAreaVibrations { val Activated = VibrationEffect.startComposition() - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_TICK, - BigVibrationScale, - 0, - ) - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, - 0.1f, - 0, - ) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, BigVibrationScale, 0) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.1f, 0) .compose() val Deactivated = VibrationEffect.startComposition() - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_TICK, - BigVibrationScale, - 0, - ) - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, - 0.1f, - 0, - ) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, BigVibrationScale, 0) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, 0.1f, 0) .compose() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt index 8725cdd273df..8a2e3dd791c2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt @@ -20,6 +20,7 @@ package com.android.systemui.keyguard.ui.binder import android.annotation.SuppressLint import android.content.res.ColorStateList import android.graphics.drawable.Animatable2 +import android.os.VibrationEffect import android.util.Size import android.view.View import android.view.ViewGroup @@ -33,25 +34,27 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launchTraced as launch import com.android.keyguard.logging.KeyguardQuickAffordancesLogger +import com.android.systemui.Flags import com.android.systemui.animation.Expandable import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceHapticViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.util.doOnEnd +import com.google.android.msdl.data.model.MSDLToken +import com.google.android.msdl.domain.MSDLPlayer import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map -import com.android.app.tracing.coroutines.launchTraced as launch /** This is only for a SINGLE Quick affordance */ @SysUISingleton @@ -60,8 +63,9 @@ class KeyguardQuickAffordanceViewBinder constructor( private val falsingManager: FalsingManager?, private val vibratorHelper: VibratorHelper?, + private val msdlPlayer: MSDLPlayer, private val logger: KeyguardQuickAffordancesLogger, - @Main private val mainImmediateDispatcher: CoroutineDispatcher, + private val hapticsViewModelFactory: KeyguardQuickAffordanceHapticViewModel.Factory, ) { private val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L @@ -88,6 +92,12 @@ constructor( ): Binding { val button = view as ImageView val configurationBasedDimensions = MutableStateFlow(loadFromResources(view)) + val hapticsViewModel = + if (Flags.msdlFeedback()) { + hapticsViewModelFactory.create(viewModel) + } else { + null + } val disposableHandle = view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -98,15 +108,12 @@ constructor( viewModel = buttonModel, messageDisplayer = messageDisplayer, ) + hapticsViewModel?.updateActivatedHistory(buttonModel.isActivated) } } launch { - updateButtonAlpha( - view = button, - viewModel = viewModel, - alphaFlow = alpha, - ) + updateButtonAlpha(view = button, viewModel = viewModel, alphaFlow = alpha) } launch { @@ -117,6 +124,32 @@ constructor( } } } + + if (Flags.msdlFeedback()) { + launch { + hapticsViewModel + ?.quickAffordanceHapticState + ?.filter { + it != + KeyguardQuickAffordanceHapticViewModel.HapticState + .NO_HAPTICS + } + ?.collect { state -> + when (state) { + KeyguardQuickAffordanceHapticViewModel.HapticState + .TOGGLE_ON -> msdlPlayer.playToken(MSDLToken.SWITCH_ON) + KeyguardQuickAffordanceHapticViewModel.HapticState + .TOGGLE_OFF -> + msdlPlayer.playToken(MSDLToken.SWITCH_OFF) + KeyguardQuickAffordanceHapticViewModel.HapticState.LAUNCH -> + msdlPlayer.playToken(MSDLToken.LONG_PRESS) + KeyguardQuickAffordanceHapticViewModel.HapticState + .NO_HAPTICS -> Unit + } + hapticsViewModel.resetLaunchingFromTriggeredResult() + } + } + } } } @@ -178,7 +211,7 @@ constructor( com.android.internal.R.color.materialColorOnPrimaryFixed } else { com.android.internal.R.color.materialColorOnSurface - }, + } ) ) @@ -221,12 +254,7 @@ constructor( .getDimensionPixelSize(R.dimen.keyguard_affordance_shake_amplitude) .toFloat() val shakeAnimator = - ObjectAnimator.ofFloat( - view, - "translationX", - -amplitude / 2, - amplitude / 2, - ) + ObjectAnimator.ofFloat(view, "translationX", -amplitude / 2, amplitude / 2) shakeAnimator.duration = KeyguardBottomAreaVibrations.ShakeAnimationDuration.inWholeMilliseconds shakeAnimator.interpolator = @@ -234,11 +262,17 @@ constructor( shakeAnimator.doOnEnd { view.translationX = 0f } shakeAnimator.start() - vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Shake) + vibratorHelper?.playFeedback(KeyguardBottomAreaVibrations.Shake, msdlPlayer) logger.logQuickAffordanceTapped(viewModel.configKey) } view.onLongClickListener = - OnLongClickListener(falsingManager, viewModel, vibratorHelper, onTouchListener) + OnLongClickListener( + falsingManager, + viewModel, + vibratorHelper, + onTouchListener, + msdlPlayer, + ) } else { view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager))) } @@ -268,7 +302,7 @@ constructor( Size( view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width), view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height), - ), + ) ) } @@ -297,7 +331,8 @@ constructor( private val falsingManager: FalsingManager?, private val viewModel: KeyguardQuickAffordanceViewModel, private val vibratorHelper: VibratorHelper?, - private val onTouchListener: KeyguardQuickAffordanceOnTouchListener + private val onTouchListener: KeyguardQuickAffordanceOnTouchListener, + private val msdlPlayer: MSDLPlayer, ) : View.OnLongClickListener { override fun onLongClick(view: View): Boolean { if (falsingManager?.isFalseLongTap(FalsingManager.MODERATE_PENALTY) == true) { @@ -312,12 +347,13 @@ constructor( slotId = viewModel.slotId, ) ) - vibratorHelper?.vibrate( + vibratorHelper?.playFeedback( if (viewModel.isActivated) { KeyguardBottomAreaVibrations.Activated } else { KeyguardBottomAreaVibrations.Deactivated - } + }, + msdlPlayer, ) } @@ -328,7 +364,15 @@ constructor( override fun onLongClickUseDefaultHapticFeedback(view: View) = false } - private data class ConfigurationBasedDimensions( - val buttonSizePx: Size, - ) + private data class ConfigurationBasedDimensions(val buttonSizePx: Size) +} + +private fun VibratorHelper.playFeedback(effect: VibrationEffect, msdlPlayer: MSDLPlayer) { + if (!Flags.msdlFeedback()) { + vibrate(effect) + } else { + if (effect == KeyguardBottomAreaVibrations.Shake) { + msdlPlayer.playToken(MSDLToken.FAILURE) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index a2ce4ec5ce9b..6d270b219c81 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -127,21 +127,18 @@ object KeyguardRootViewBinder { if (Flags.nonTouchscreenDevicesBypassFalsing()) { if ( event.action == MotionEvent.ACTION_DOWN && - event.buttonState == MotionEvent.BUTTON_PRIMARY && - !event.isTouchscreenSource() + event.buttonState == MotionEvent.BUTTON_PRIMARY && + !event.isTouchscreenSource() ) { consumed = true } else if ( - event.action == MotionEvent.ACTION_UP && - !event.isTouchscreenSource() + event.action == MotionEvent.ACTION_UP && !event.isTouchscreenSource() ) { statusBarKeyguardViewManager?.showBouncer(true) consumed = true } } - viewModel.setRootViewLastTapPosition( - Point(event.x.toInt(), event.y.toInt()) - ) + viewModel.setRootViewLastTapPosition(Point(event.x.toInt(), event.y.toInt())) } consumed } @@ -172,7 +169,6 @@ object KeyguardRootViewBinder { launch("$TAG#alpha") { viewModel.alpha(viewState).collect { alpha -> view.alpha = alpha - childViews[statusViewId]?.alpha = alpha childViews[burnInLayerId]?.alpha = alpha } } @@ -253,18 +249,6 @@ object KeyguardRootViewBinder { } launch { - viewModel.burnInLayerAlpha.collect { alpha -> - childViews[statusViewId]?.alpha = alpha - } - } - - launch { - viewModel.lockscreenStateAlpha(viewState).collect { alpha -> - childViews[statusViewId]?.alpha = alpha - } - } - - launch { viewModel.scale.collect { scaleViewModel -> if (scaleViewModel.scaleClockOnly) { // For clocks except weather clock, we have scale transition besides @@ -553,7 +537,6 @@ object KeyguardRootViewBinder { return device?.supportsSource(InputDevice.SOURCE_TOUCHSCREEN) == true } - private val statusViewId = R.id.keyguard_status_view private val burnInLayerId = R.id.burn_in_layer private val aodNotificationIconContainerId = R.id.aod_notification_icon_container private val largeClockId = customR.id.lockscreen_clock_view_large diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 090b65922d2d..6fb31c0e4191 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -48,19 +48,16 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.core.view.isInvisible -import com.android.internal.policy.SystemBarUtils import com.android.keyguard.ClockEventController -import com.android.keyguard.KeyguardClockSwitch import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.communal.ui.binder.CommunalTutorialIndicatorViewBinder import com.android.systemui.communal.ui.viewmodel.CommunalTutorialIndicatorViewModel -import com.android.systemui.coroutines.newTracingContext +import com.android.systemui.customization.R as customR import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder @@ -80,7 +77,6 @@ import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.clocks.ClockRegistry -import com.android.systemui.shared.clocks.DefaultClockController import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.shared.quickaffordance.shared.model.KeyguardPreviewConstants @@ -91,18 +87,13 @@ import com.android.systemui.util.settings.SecureSettings import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.json.JSONException import org.json.JSONObject -import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.systemui.customization.R as customR /** Renders the preview of the lock screen. */ class KeyguardPreviewRenderer @@ -110,7 +101,6 @@ class KeyguardPreviewRenderer @AssistedInject constructor( @Application private val context: Context, - @Application applicationScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, @Main private val mainHandler: Handler, @Background private val backgroundDispatcher: CoroutineDispatcher, @@ -157,8 +147,6 @@ constructor( val surfacePackage: SurfaceControlViewHost.SurfacePackage get() = checkNotNull(host.surfacePackage) - private lateinit var largeClockHostView: FrameLayout - private lateinit var smallClockHostView: FrameLayout private var smartSpaceView: View? = null private val disposables = DisposableHandles() @@ -166,29 +154,18 @@ constructor( private val shortcutsBindings = mutableSetOf<KeyguardQuickAffordanceViewBinder.Binding>() - private val coroutineScope: CoroutineScope - @Style.Type private var themeStyle: Int? = null init { - coroutineScope = - CoroutineScope( - applicationScope.coroutineContext + - Job() + - newTracingContext("KeyguardPreviewRenderer") - ) - disposables += DisposableHandle { coroutineScope.cancel() } clockController.setFallbackWeatherData(WeatherData.getPlaceholderWeatherData()) - quickAffordancesCombinedViewModel.enablePreviewMode( initiallySelectedSlotId = - bundle.getString(KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID) - ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + bundle.getString(KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID) + ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance, ) - if (MigrateClocksToBlueprint.isEnabled) { - clockViewModel.shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance - } + + clockViewModel.shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance runBlocking(mainDispatcher) { host = SurfaceControlViewHost( @@ -348,6 +325,7 @@ constructor( smartSpaceView?.alpha = if (shouldHighlightSelectedAffordance) DIM_ALPHA else 1.0f } + @OptIn(ExperimentalCoroutinesApi::class) private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) { val keyguardRootView = KeyguardRootView(previewContext, null) rootView.addView( @@ -358,34 +336,23 @@ constructor( ), ) - setUpUdfps( - previewContext, - if (MigrateClocksToBlueprint.isEnabled) keyguardRootView else rootView, - ) + setUpUdfps(previewContext, keyguardRootView) setupShortcuts(keyguardRootView) if (!shouldHideClock) { setUpClock(previewContext, rootView) - if (MigrateClocksToBlueprint.isEnabled) { - KeyguardPreviewClockViewBinder.bind( - keyguardRootView, - clockViewModel, - clockRegistry, - ::updateClockAppearance, - ClockPreviewConfig( - previewContext, - getPreviewShadeLayoutWide(display!!), - SceneContainerFlag.isEnabled, - ), - ) - } else { - KeyguardPreviewClockViewBinder.bind( - largeClockHostView, - smallClockHostView, - clockViewModel, - ) - } + KeyguardPreviewClockViewBinder.bind( + keyguardRootView, + clockViewModel, + clockRegistry, + ::updateClockAppearance, + ClockPreviewConfig( + previewContext, + getPreviewShadeLayoutWide(display!!), + SceneContainerFlag.isEnabled, + ), + ) } setUpSmartspace(previewContext, rootView) @@ -451,82 +418,22 @@ constructor( .inflate(R.layout.udfps_keyguard_preview, parentView, false) as View // Place the UDFPS view in the proper sensor location - if (MigrateClocksToBlueprint.isEnabled) { - val lockId = KeyguardPreviewClockViewBinder.lockId - finger.id = lockId - parentView.addView(finger) - val cs = ConstraintSet() - cs.clone(parentView as ConstraintLayout) - cs.apply { - constrainWidth(lockId, sensorBounds.width()) - constrainHeight(lockId, sensorBounds.height()) - connect(lockId, TOP, PARENT_ID, TOP, sensorBounds.top) - connect(lockId, START, PARENT_ID, START, sensorBounds.left) - } - cs.applyTo(parentView) - } else { - val fingerprintLayoutParams = - FrameLayout.LayoutParams(sensorBounds.width(), sensorBounds.height()) - fingerprintLayoutParams.setMarginsRelative( - sensorBounds.left, - sensorBounds.top, - sensorBounds.right, - sensorBounds.bottom, - ) - parentView.addView(finger, fingerprintLayoutParams) + val lockId = KeyguardPreviewClockViewBinder.lockId + finger.id = lockId + parentView.addView(finger) + val cs = ConstraintSet() + cs.clone(parentView as ConstraintLayout) + cs.apply { + constrainWidth(lockId, sensorBounds.width()) + constrainHeight(lockId, sensorBounds.height()) + connect(lockId, TOP, PARENT_ID, TOP, sensorBounds.top) + connect(lockId, START, PARENT_ID, START, sensorBounds.left) } + cs.applyTo(parentView) } private fun setUpClock(previewContext: Context, parentView: ViewGroup) { val resources = parentView.resources - if (!MigrateClocksToBlueprint.isEnabled) { - largeClockHostView = FrameLayout(previewContext) - largeClockHostView.layoutParams = - FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT, - ) - largeClockHostView.isInvisible = true - parentView.addView(largeClockHostView) - - smallClockHostView = FrameLayout(previewContext) - val layoutParams = - FrameLayout.LayoutParams( - FrameLayout.LayoutParams.WRAP_CONTENT, - resources.getDimensionPixelSize(customR.dimen.small_clock_height), - ) - layoutParams.topMargin = - SystemBarUtils.getStatusBarHeight(previewContext) + - resources.getDimensionPixelSize(customR.dimen.small_clock_padding_top) - smallClockHostView.layoutParams = layoutParams - smallClockHostView.setPaddingRelative( - /* start = */ resources.getDimensionPixelSize(customR.dimen.clock_padding_start), - /* top = */ 0, - /* end = */ 0, - /* bottom = */ 0, - ) - smallClockHostView.clipChildren = false - parentView.addView(smallClockHostView) - smallClockHostView.isInvisible = true - } - - // TODO (b/283465254): Move the listeners to KeyguardClockRepository - if (!MigrateClocksToBlueprint.isEnabled) { - val clockChangeListener = - object : ClockRegistry.ClockChangeListener { - override fun onCurrentClockChanged() { - onClockChanged() - } - } - clockRegistry.registerClockChangeListener(clockChangeListener) - disposables += DisposableHandle { - clockRegistry.unregisterClockChangeListener(clockChangeListener) - } - - clockController.registerListeners(parentView) - disposables += DisposableHandle { clockController.unregisterListeners() } - } - val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -544,38 +451,9 @@ constructor( }, ) disposables += DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) } - - if (!MigrateClocksToBlueprint.isEnabled) { - val layoutChangeListener = - View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - if (clockController.clock !is DefaultClockController) { - clockController.clock - ?.largeClock - ?.events - ?.onTargetRegionChanged( - KeyguardClockSwitch.getLargeClockRegion(parentView) - ) - clockController.clock - ?.smallClock - ?.events - ?.onTargetRegionChanged( - KeyguardClockSwitch.getSmallClockRegion(parentView) - ) - } - } - parentView.addOnLayoutChangeListener(layoutChangeListener) - disposables += DisposableHandle { - parentView.removeOnLayoutChangeListener(layoutChangeListener) - } - } - - onClockChanged() } private suspend fun updateClockAppearance(clock: ClockController, resources: Resources) { - if (!MigrateClocksToBlueprint.isEnabled) { - clockController.clock = clock - } val colors = wallpaperColors if (clockRegistry.seedColor == null && colors != null) { // Seed color null means users do not override any color on the clock. The default @@ -601,9 +479,7 @@ constructor( // In clock preview, we should have a seed color for clock // before setting clock to clockEventController to avoid updateColor with seedColor == null // So in update colors, it should already have the correct theme in clockFaceController - if (MigrateClocksToBlueprint.isEnabled) { - clockController.clock = clock - } + clockController.clock = clock // When set clock to clockController,it will reset fontsize based on context.resources // We need to override it with overlaid resources clock.largeClock.events.onFontSettingChanged( @@ -611,19 +487,6 @@ constructor( ) } - private fun onClockChanged() { - if (MigrateClocksToBlueprint.isEnabled) { - return - } - coroutineScope.launch { - val clock = clockRegistry.createCurrentClock() - clockController.clock = clock - updateClockAppearance(clock, context.resources) - updateLargeClock(clock) - updateSmallClock(clock) - } - } - private fun setupCommunalTutorialIndicator(keyguardRootView: ConstraintLayout) { keyguardRootView.findViewById<TextView>(R.id.communal_tutorial_indicator)?.let { indicatorView -> @@ -657,34 +520,6 @@ constructor( } } - private fun updateLargeClock(clock: ClockController) { - if (MigrateClocksToBlueprint.isEnabled) { - return - } - clock.largeClock.events.onTargetRegionChanged( - KeyguardClockSwitch.getLargeClockRegion(largeClockHostView) - ) - if (shouldHighlightSelectedAffordance) { - clock.largeClock.view.alpha = DIM_ALPHA - } - largeClockHostView.removeAllViews() - largeClockHostView.addView(clock.largeClock.view) - } - - private fun updateSmallClock(clock: ClockController) { - if (MigrateClocksToBlueprint.isEnabled) { - return - } - clock.smallClock.events.onTargetRegionChanged( - KeyguardClockSwitch.getSmallClockRegion(smallClockHostView) - ) - if (shouldHighlightSelectedAffordance) { - clock.smallClock.view.alpha = DIM_ALPHA - } - smallClockHostView.removeAllViews() - smallClockHostView.addView(clock.smallClock.view) - } - private fun getPreviewShadeLayoutWide(display: Display): Boolean { return if (display.displayId == 0) { shadeInteractor.isShadeLayoutWide.value diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt index 729759a9ad00..5d463f72d8b2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt @@ -23,7 +23,6 @@ import androidx.constraintlayout.widget.ConstraintSet.END import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.res.R import com.android.systemui.shade.NotificationPanelView import com.android.systemui.shade.ShadeDisplayAware @@ -50,16 +49,13 @@ constructor( sharedNotificationContainerBinder, ) { override fun applyConstraints(constraintSet: ConstraintSet) { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } constraintSet.apply { connect( R.id.nssl_placeholder, TOP, PARENT_ID, TOP, - context.resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin) + context.resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin), ) connect(R.id.nssl_placeholder, START, PARENT_ID, START) connect(R.id.nssl_placeholder, END, PARENT_ID, END) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt index 1c897237fe89..fb311a533aa2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt @@ -24,7 +24,6 @@ import com.android.app.animation.Interpolators import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -194,12 +193,7 @@ constructor( (!useAltAod) && keyguardClockViewModel.clockSize.value == ClockSize.LARGE val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolated).toInt() - val translationY = - if (MigrateClocksToBlueprint.isEnabled) { - max(params.topInset - params.minViewY, burnInY) - } else { - max(params.topInset, params.minViewY + burnInY) - params.minViewY - } + val translationY = max(params.topInset - params.minViewY, burnInY) BurnInModel( translationX = MathUtils.lerp(0, burnIn.translationX, interpolated).toInt(), translationY = translationY, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceHapticViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceHapticViewModel.kt new file mode 100644 index 000000000000..890628c31c55 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceHapticViewModel.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +class KeyguardQuickAffordanceHapticViewModel +@AssistedInject +constructor( + @Assisted quickAffordanceViewModel: Flow<KeyguardQuickAffordanceViewModel>, + private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor, +) { + + private val activatedHistory = MutableStateFlow(ActivatedHistory(false)) + + private val launchingHapticState: Flow<HapticState> = + combine( + quickAffordanceViewModel.map { it.configKey }, + quickAffordanceInteractor.launchingFromTriggeredResult, + ) { key, launchingResult -> + val validKey = key != null && key == launchingResult?.configKey + if (validKey && launchingResult?.launched == true) { + HapticState.LAUNCH + } else { + HapticState.NO_HAPTICS + } + } + .distinctUntilChanged() + + private val toggleHapticState: Flow<HapticState> = + activatedHistory + .map { history -> + when { + history.previousValue == false && history.currentValue -> HapticState.TOGGLE_ON + history.previousValue == true && !history.currentValue -> HapticState.TOGGLE_OFF + else -> HapticState.NO_HAPTICS + } + } + .distinctUntilChanged() + + val quickAffordanceHapticState = + merge(launchingHapticState, toggleHapticState).distinctUntilChanged() + + fun resetLaunchingFromTriggeredResult() = + quickAffordanceInteractor.setLaunchingFromTriggeredResult(null) + + fun updateActivatedHistory(isActivated: Boolean) { + activatedHistory.value = + ActivatedHistory( + currentValue = isActivated, + previousValue = activatedHistory.value.currentValue, + ) + } + + enum class HapticState { + TOGGLE_ON, + TOGGLE_OFF, + LAUNCH, + NO_HAPTICS, + } + + private data class ActivatedHistory( + val currentValue: Boolean, + val previousValue: Boolean? = null, + ) + + @AssistedFactory + interface Factory { + fun create( + quickAffordanceViewModel: Flow<KeyguardQuickAffordanceViewModel> + ): KeyguardQuickAffordanceHapticViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 9066d466ceca..eaba5d5a149c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -29,7 +29,6 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.PulseExpansionInteractor import com.android.systemui.keyguard.shared.model.Edge -import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.KeyguardState.GONE @@ -130,7 +129,6 @@ constructor( PrimaryBouncerToLockscreenTransitionViewModel, private val screenOffAnimationController: ScreenOffAnimationController, private val aodBurnInViewModel: AodBurnInViewModel, - private val aodAlphaViewModel: AodAlphaViewModel, private val shadeInteractor: ShadeInteractor, ) { val burnInLayerVisibility: Flow<Int> = @@ -284,15 +282,6 @@ constructor( .distinctUntilChanged() } - /** Specific alpha value for elements visible during [KeyguardState.LOCKSCREEN] */ - @Deprecated("only used for legacy status view") - fun lockscreenStateAlpha(viewState: ViewStateAccessor): Flow<Float> { - return aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState) - } - - /** For elements that appear and move during the animation -> AOD */ - val burnInLayerAlpha: Flow<Float> = aodAlphaViewModel.alpha - val translationY: Flow<Float> = aodBurnInViewModel.movement.map { it.translationY.toFloat() } val translationX: Flow<StateToValue> = diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt index c32bd403d2e8..b4dabbe036e9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt @@ -34,13 +34,14 @@ import android.view.ViewGroup import android.view.ViewGroupOverlay import androidx.annotation.VisibleForTesting import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.app.tracing.traceSection import com.android.keyguard.KeyguardViewController import com.android.systemui.Flags.mediaControlsLockscreenShadeBugFix import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -68,7 +69,6 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.mapLatest -import com.android.app.tracing.coroutines.launchTraced as launch private val TAG: String = MediaHierarchyManager::class.java.simpleName @@ -115,7 +115,7 @@ constructor( wakefulnessLifecycle: WakefulnessLifecycle, shadeInteractor: ShadeInteractor, private val secureSettings: SecureSettings, - @Main private val handler: Handler, + @Background private val handler: Handler, @Application private val coroutineScope: CoroutineScope, private val splitShadeStateController: SplitShadeStateController, private val logger: MediaViewLogger, @@ -631,7 +631,7 @@ constructor( } } } - secureSettings.registerContentObserverForUserSync( + secureSettings.registerContentObserverForUserAsync( Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, settingsObserver, UserHandle.USER_ALL, diff --git a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt index 311cbfb7e632..b2696aeaabfc 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt @@ -132,7 +132,7 @@ constructor( val isDefaultNotesAppSet = noteTaskInfoResolver.resolveInfo( QUICK_AFFORDANCE, - user = controller.getUserForHandlingNotesTaking(QUICK_AFFORDANCE) + user = controller.getUserForHandlingNotesTaking(QUICK_AFFORDANCE), ) != null return when { isEnabled && isDefaultNotesAppSet -> PickerScreenState.Default() @@ -158,7 +158,7 @@ constructor( override fun onTriggered(expandable: Expandable?): OnTriggeredResult { controller.showNoteTask(entryPoint = QUICK_AFFORDANCE) - return OnTriggeredResult.Handled + return OnTriggeredResult.Handled(true) } } @@ -194,7 +194,7 @@ private fun RoleManager.createNotesRoleFlow( fun isDefaultNotesAppSetForUser() = noteTaskInfoResolver.resolveInfo( QUICK_AFFORDANCE, - user = noteTaskController.getUserForHandlingNotesTaking(QUICK_AFFORDANCE) + user = noteTaskController.getUserForHandlingNotesTaking(QUICK_AFFORDANCE), ) != null trySendBlocking(isDefaultNotesAppSetForUser()) diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt b/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt index ef7e7eb59898..84b995e1cd28 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt @@ -22,16 +22,21 @@ import com.android.systemui.qs.tileimpl.QSTileImpl /** * Creates a [QSTile.Icon] from an [Icon]. - * * [Icon.Loaded] -> [QSTileImpl.DrawableIcon] + * * [Icon.Loaded] with null [res] -> [QSTileImpl.DrawableIcon] + * * [Icon.Loaded] & with non null [res] -> [QSTileImpl.DrawableIconWithRes] * * [Icon.Resource] -> [QSTileImpl.ResourceIcon] */ fun Icon.asQSTileIcon(): QSTile.Icon { return when (this) { is Icon.Loaded -> { - QSTileImpl.DrawableIcon(this.drawable) + if (res == null) { + QSTileImpl.DrawableIcon(drawable) + } else { + QSTileImpl.DrawableIconWithRes(drawable, res) + } } is Icon.Resource -> { - QSTileImpl.ResourceIcon.get(this.res) + QSTileImpl.ResourceIcon.get(res) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NotesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/NotesTile.kt index 989fc0fd6f44..5ba1527dbf69 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/NotesTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NotesTile.kt @@ -22,6 +22,7 @@ import android.os.Looper import android.service.quicksettings.Tile import com.android.internal.logging.MetricsLogger import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter @@ -92,7 +93,8 @@ constructor( state?.apply { this.state = tileState.activationState.legacyState - icon = maybeLoadResourceIcon(tileState.iconRes ?: R.drawable.ic_qs_notes) + icon = + maybeLoadResourceIcon((tileState.icon as Icon.Loaded).res ?: R.drawable.ic_qs_notes) label = tileState.label contentDescription = tileState.contentDescription expandedAccessibilityClassName = tileState.expandedAccessibilityClassName diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt index 34c2ec90f1e8..80d429ce2716 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt @@ -35,13 +35,13 @@ constructor(@ShadeDisplayAware private val resources: Resources, val theme: Them override fun map(config: QSTileConfig, data: AirplaneModeTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - iconRes = + val iconRes = if (data.isEnabled) { R.drawable.qs_airplane_icon_on } else { R.drawable.qs_airplane_icon_off } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) if (data.isEnabled) { activationState = QSTileState.ActivationState.ACTIVE secondaryLabel = resources.getStringArray(R.array.tile_states_airplane)[2] diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt index a72992db4496..d56d9944dbb8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt @@ -84,8 +84,8 @@ constructor( secondaryLabel = resources.getString(R.string.qs_alarm_tile_no_alarm) } } - iconRes = R.drawable.ic_alarm - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + val iconRes = R.drawable.ic_alarm + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) sideViewIcon = QSTileState.SideViewIcon.Chevron contentDescription = label supportedActions = setOf(QSTileState.UserAction.CLICK) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt index e116d8cef2ee..72759c5bb066 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt @@ -38,10 +38,10 @@ constructor( QSTileState.build(resources, theme, config.uiConfig) { label = resources.getString(R.string.battery_detail_switch_title) contentDescription = label - iconRes = + val iconRes = if (data.isPowerSaving) R.drawable.qs_battery_saver_icon_on else R.drawable.qs_battery_saver_icon_off - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) sideViewIcon = QSTileState.SideViewIcon.None if (data.isPluggedIn) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapper.kt index 21b9f659dde4..e5a0fe8ed048 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapper.kt @@ -37,8 +37,8 @@ constructor( override fun map(config: QSTileConfig, data: ColorCorrectionTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { val subtitleArray = resources.getStringArray(R.array.tile_states_color_correction) - iconRes = R.drawable.ic_qs_color_correction - icon = Icon.Loaded(resources.getDrawable(R.drawable.ic_qs_color_correction)!!, null) + val iconRes = R.drawable.ic_qs_color_correction + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) if (data.isEnabled) { activationState = QSTileState.ActivationState.ACTIVE secondaryLabel = subtitleArray[2] diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt index 2dfb1fc4fe98..32ccba6f1fa5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt @@ -35,14 +35,14 @@ constructor(@ShadeDisplayAware private val resources: Resources, private val the override fun map(config: QSTileConfig, data: FlashlightTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - iconRes = + val iconRes = if (data is FlashlightTileModel.FlashlightAvailable && data.isEnabled) { R.drawable.qs_flashlight_icon_on } else { R.drawable.qs_flashlight_icon_off } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) contentDescription = label diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt index 7f41cbd322dd..c571b136e18b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt @@ -36,8 +36,8 @@ constructor( override fun map(config: QSTileConfig, data: FontScalingTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - iconRes = R.drawable.ic_qs_font_scaling - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + val iconRes = R.drawable.ic_qs_font_scaling + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) contentDescription = label activationState = QSTileState.ActivationState.ACTIVE sideViewIcon = QSTileState.SideViewIcon.Chevron diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapper.kt index 4c302b363c3b..12f71491c7b4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapper.kt @@ -37,8 +37,8 @@ constructor( override fun map(config: QSTileConfig, data: HearingDevicesTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { label = resources.getString(R.string.quick_settings_hearing_devices_label) - iconRes = R.drawable.qs_hearing_devices_icon - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + val iconRes = R.drawable.qs_hearing_devices_icon + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) sideViewIcon = QSTileState.SideViewIcon.Chevron contentDescription = label if (data.isAnyActiveHearingDevice) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt index 1a6876d0b765..7ad01e463399 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt @@ -61,11 +61,11 @@ constructor( when (val dataIcon = data.icon) { is InternetTileIconModel.ResourceId -> { - iconRes = dataIcon.resId icon = Icon.Loaded( resources.getDrawable(dataIcon.resId, theme), contentDescription = null, + dataIcon.resId, ) } @@ -76,11 +76,11 @@ constructor( } is InternetTileIconModel.Satellite -> { - iconRes = dataIcon.resourceIcon.res // level is inferred from res icon = Icon.Loaded( resources.getDrawable(dataIcon.resourceIcon.res, theme), contentDescription = null, + dataIcon.resourceIcon.res, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt index 8d35b2413bad..05590e803ffa 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt @@ -35,7 +35,7 @@ constructor(@ShadeDisplayAware private val resources: Resources, private val the override fun map(config: QSTileConfig, data: ColorInversionTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { val subtitleArray = resources.getStringArray(R.array.tile_states_inversion) - + val iconRes: Int if (data.isEnabled) { activationState = QSTileState.ActivationState.ACTIVE secondaryLabel = subtitleArray[2] @@ -45,7 +45,7 @@ constructor(@ShadeDisplayAware private val resources: Resources, private val the secondaryLabel = subtitleArray[1] iconRes = R.drawable.qs_invert_colors_icon_off } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) contentDescription = label supportedActions = setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt index 3557c1a4ac9d..afb137e1e92f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt @@ -39,6 +39,7 @@ constructor(@ShadeDisplayAware private val resources: Resources, private val the Icon.Loaded( resources.getDrawable(R.drawable.qs_record_issue_icon_on, theme), null, + R.drawable.qs_record_issue_icon_on, ) } else { activationState = QSTileState.ActivationState.INACTIVE @@ -46,6 +47,7 @@ constructor(@ShadeDisplayAware private val resources: Resources, private val the Icon.Loaded( resources.getDrawable(R.drawable.qs_record_issue_icon_off, theme), null, + R.drawable.qs_record_issue_icon_off, ) } supportedActions = setOf(QSTileState.UserAction.CLICK) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapper.kt index dfc24a10c491..ced5a4f099a2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapper.kt @@ -35,13 +35,13 @@ constructor(@ShadeDisplayAware private val resources: Resources, private val the override fun map(config: QSTileConfig, data: LocationTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - iconRes = + val iconRes = if (data.isEnabled) { R.drawable.qs_location_icon_on } else { R.drawable.qs_location_icon_off } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) label = resources.getString(R.string.quick_settings_location_label) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt index 9b2880b6d47f..479f61823912 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt @@ -17,10 +17,10 @@ package com.android.systemui.qs.tiles.impl.modes.domain.interactor import android.content.Context +import android.graphics.drawable.Drawable import android.os.UserHandle import com.android.app.tracing.coroutines.flow.flowName import com.android.systemui.common.shared.model.Icon -import com.android.systemui.common.shared.model.asIcon import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.modes.shared.ModesUi import com.android.systemui.modes.shared.ModesUiIcons @@ -31,7 +31,6 @@ import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes -import com.android.systemui.statusbar.policy.domain.model.ZenModeInfo import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -68,37 +67,29 @@ constructor( suspend fun getCurrentTileModel() = buildTileData(zenModeInteractor.getActiveModes()) private fun buildTileData(activeModes: ActiveZenModes): ModesTileModel { - if (ModesUiIcons.isEnabled) { - val tileIcon = getTileIcon(activeModes.mainMode) - return ModesTileModel( - isActivated = activeModes.isAnyActive(), - icon = tileIcon.icon, - iconResId = tileIcon.resId, - activeModes = activeModes.modeNames, - ) - } else { - return ModesTileModel( - isActivated = activeModes.isAnyActive(), - icon = context.getDrawable(ModesTile.ICON_RES_ID)!!.asIcon(), - iconResId = ModesTile.ICON_RES_ID, - activeModes = activeModes.modeNames, - ) - } - } + val drawable: Drawable + val iconRes: Int? + val activeMode = activeModes.mainMode - private data class TileIcon(val icon: Icon.Loaded, val resId: Int?) - - private fun getTileIcon(activeMode: ZenModeInfo?): TileIcon { - return if (activeMode != null) { + if (ModesUiIcons.isEnabled && activeMode != null) { // ZenIconKey.resPackage is null if its resId is a system icon. - if (activeMode.icon.key.resPackage == null) { - TileIcon(activeMode.icon.drawable.asIcon(), activeMode.icon.key.resId) - } else { - TileIcon(activeMode.icon.drawable.asIcon(), null) - } + iconRes = + if (activeMode.icon.key.resPackage == null) { + activeMode.icon.key.resId + } else { + null + } + drawable = activeMode.icon.drawable } else { - TileIcon(context.getDrawable(ModesTile.ICON_RES_ID)!!.asIcon(), ModesTile.ICON_RES_ID) + iconRes = ModesTile.ICON_RES_ID + drawable = context.getDrawable(iconRes)!! } + + return ModesTileModel( + isActivated = activeModes.isAnyActive(), + icon = Icon.Loaded(drawable, null, iconRes), + activeModes = activeModes.modeNames, + ) } override fun availability(user: UserHandle): Flow<Boolean> = flowOf(ModesUi.isEnabled) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt index db4812342050..d0eacbc9a957 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt @@ -21,12 +21,10 @@ import com.android.systemui.common.shared.model.Icon data class ModesTileModel( val isActivated: Boolean, val activeModes: List<String>, - val icon: Icon.Loaded, - /** - * Resource id corresponding to [icon]. Will only be present if it's know to correspond to a - * resource with a known id in SystemUI (such as resources from `android.R`, - * `com.android.internal.R`, or `com.android.systemui.res` itself). + * icon.res will only be present if it is known to correspond to a resource with a known id in + * SystemUI (such as resources from `android.R`, `com.android.internal.R`, or + * `com.android.systemui.res` itself). */ - val iconResId: Int? = null + val icon: Icon.Loaded, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt index 1507ef4b3b58..99ae3b8db709 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt @@ -34,7 +34,6 @@ constructor(@ShadeDisplayAware private val resources: Resources, val theme: Reso QSTileDataToStateMapper<ModesTileModel> { override fun map(config: QSTileConfig, data: ModesTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - iconRes = data.iconResId icon = data.icon activationState = if (data.isActivated) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt index 3569e4d0b42c..16b36289ad95 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt @@ -49,7 +49,7 @@ constructor( supportedActions = setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) sideViewIcon = QSTileState.SideViewIcon.None - + val iconRes: Int if (data.isActivated) { activationState = QSTileState.ActivationState.ACTIVE iconRes = R.drawable.qs_nightlight_icon_on @@ -58,7 +58,7 @@ constructor( iconRes = R.drawable.qs_nightlight_icon_off } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) secondaryLabel = getSecondaryLabel(data, resources) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapper.kt index a5436192af39..ecdd71170cda 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapper.kt @@ -35,8 +35,8 @@ constructor( ) : QSTileDataToStateMapper<NotesTileModel> { override fun map(config: QSTileConfig, data: NotesTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - iconRes = R.drawable.ic_qs_notes - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null) + val iconRes = R.drawable.ic_qs_notes + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) contentDescription = label activationState = QSTileState.ActivationState.INACTIVE sideViewIcon = QSTileState.SideViewIcon.Chevron diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt index 76f1e8b8760c..5b3ea93ab1ae 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt @@ -38,8 +38,8 @@ constructor( QSTileState.build(resources, theme, config.uiConfig) { val subtitleArray = resources.getStringArray(R.array.tile_states_onehanded) label = resources.getString(R.string.quick_settings_onehanded_label) - iconRes = com.android.internal.R.drawable.ic_qs_one_handed_mode - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + val iconRes = com.android.internal.R.drawable.ic_qs_one_handed_mode + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) if (data.isEnabled) { activationState = QSTileState.ActivationState.ACTIVE secondaryLabel = subtitleArray[2] diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapper.kt index c546250e73d2..21e92d3a1972 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapper.kt @@ -38,8 +38,8 @@ constructor( QSTileState.build(resources, theme, config.uiConfig) { label = resources.getString(R.string.qr_code_scanner_title) contentDescription = label - iconRes = R.drawable.ic_qr_code_scanner - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + val iconRes = R.drawable.ic_qr_code_scanner + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) sideViewIcon = QSTileState.SideViewIcon.Chevron supportedActions = setOf(QSTileState.UserAction.CLICK) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapper.kt index 66d0f96fdcde..66759cdfd1a6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapper.kt @@ -37,6 +37,7 @@ constructor( override fun map(config: QSTileConfig, data: ReduceBrightColorsTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { + val iconRes: Int if (data.isEnabled) { activationState = QSTileState.ActivationState.ACTIVE iconRes = R.drawable.qs_extra_dim_icon_on @@ -50,7 +51,7 @@ constructor( resources .getStringArray(R.array.tile_states_reduce_brightness)[Tile.STATE_INACTIVE] } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) label = resources.getString(com.android.internal.R.string.reduce_bright_colors_feature_name) contentDescription = label diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapper.kt index a0144221577d..000c7025e32b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapper.kt @@ -42,7 +42,7 @@ constructor( QSTileState.build(resources, theme, config.uiConfig) { label = resources.getString(R.string.quick_settings_rotation_unlocked_label) contentDescription = resources.getString(R.string.accessibility_quick_settings_rotation) - + val iconRes: Int if (data.isRotationLocked) { activationState = QSTileState.ActivationState.INACTIVE secondaryLabel = EMPTY_SECONDARY_STRING @@ -57,7 +57,7 @@ constructor( } iconRes = R.drawable.qs_auto_rotate_icon_on } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) if (isDeviceFoldable(resources, deviceStateManager)) { secondaryLabel = getSecondaryLabelWithPosture(activationState) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt index aea4967c546c..1d5cf29f2462 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt @@ -36,6 +36,7 @@ constructor( override fun map(config: QSTileConfig, data: DataSaverTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { with(data) { + val iconRes: Int if (isEnabled) { activationState = QSTileState.ActivationState.ACTIVE iconRes = R.drawable.qs_data_saver_icon_on @@ -45,7 +46,7 @@ constructor( iconRes = R.drawable.qs_data_saver_icon_off secondaryLabel = resources.getStringArray(R.array.tile_states_saver)[1] } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) contentDescription = label supportedActions = setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt index f3136e015acf..0a61e3cbe616 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt @@ -38,7 +38,7 @@ constructor( QSTileState.build(resources, theme, config.uiConfig) { label = resources.getString(R.string.quick_settings_screen_record_label) supportedActions = setOf(QSTileState.UserAction.CLICK) - + val iconRes: Int when (data) { is ScreenRecordModel.Recording -> { activationState = QSTileState.ActivationState.ACTIVE @@ -61,7 +61,7 @@ constructor( resources.getString(R.string.quick_settings_screen_record_start) } } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) contentDescription = if (TextUtils.isEmpty(secondaryLabel)) label diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapper.kt index 73e61b7d178e..f54f46c01dee 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapper.kt @@ -50,8 +50,8 @@ constructor( contentDescription = label supportedActions = setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) - iconRes = sensorPrivacyTileResources.getIconRes(data.isBlocked) - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + val iconRes = sensorPrivacyTileResources.getIconRes(data.isBlocked) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) sideViewIcon = QSTileState.SideViewIcon.None if (data.isBlocked) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt index e9aa46c5f253..5933d65bc61f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt @@ -116,11 +116,11 @@ constructor(@ShadeDisplayAware private val resources: Resources, private val the } } - iconRes = + val iconRes = if (activationState == QSTileState.ActivationState.ACTIVE) R.drawable.qs_light_dark_theme_icon_on else R.drawable.qs_light_dark_theme_icon_off - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) supportedActions = if (activationState == QSTileState.ActivationState.UNAVAILABLE) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt index 6a3195a493c8..5b462ba074ec 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt @@ -41,8 +41,8 @@ constructor( QSTileState.build(resources, theme, config.uiConfig) { label = getTileLabel()!! contentDescription = label - iconRes = com.android.internal.R.drawable.stat_sys_managed_profile_status - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null) + val iconRes = com.android.internal.R.drawable.stat_sys_managed_profile_status + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) when (data) { is WorkModeTileModel.HasActiveProfile -> { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt index 8394be5e0a38..c6af729cd4a7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt @@ -36,7 +36,6 @@ import kotlin.reflect.KClass */ data class QSTileState( val icon: Icon?, - val iconRes: Int?, val label: CharSequence, val activationState: ActivationState, val secondaryLabel: CharSequence?, @@ -58,7 +57,7 @@ data class QSTileState( ): QSTileState { val iconDrawable = resources.getDrawable(config.iconRes, theme) return build( - Icon.Loaded(iconDrawable, null), + Icon.Loaded(iconDrawable, null, config.iconRes), resources.getString(config.labelRes), builder, ) @@ -115,7 +114,6 @@ data class QSTileState( } class Builder(var icon: Icon?, var label: CharSequence) { - var iconRes: Int? = null var activationState: ActivationState = ActivationState.INACTIVE var secondaryLabel: CharSequence? = null var supportedActions: Set<UserAction> = setOf(UserAction.CLICK) @@ -128,7 +126,6 @@ data class QSTileState( fun build(): QSTileState = QSTileState( icon, - iconRes, label, activationState, secondaryLabel, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index 632eeefcb462..c34edc81bfe7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -260,8 +260,8 @@ constructor( icon = when (val stateIcon = viewModelState.icon) { is Icon.Loaded -> - if (viewModelState.iconRes == null) DrawableIcon(stateIcon.drawable) - else DrawableIconWithRes(stateIcon.drawable, viewModelState.iconRes) + if (stateIcon.res == null) DrawableIcon(stateIcon.drawable) + else DrawableIconWithRes(stateIcon.drawable, stateIcon.res) is Icon.Resource -> ResourceIcon.get(stateIcon.res) null -> null } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 66af275bc702..a7dbb47bc609 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel import android.view.View +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor @@ -99,6 +100,17 @@ constructor( ) } + if (Flags.promoteNotificationsAutomatically()) { + // When we're promoting notifications automatically, the `when` time set on the + // notification will likely just be set to the current time, which would cause the chip + // to always show "now". We don't want early testers to get that experience since it's + // not what will happen at launch, so just don't show any time. + // TODO(b/364653005): Only ignore the `when` time if the notification was + // *automatically* promoted (as opposed to being legitimately promoted by the + // criteria). We'll need to track that status somehow. + return OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener) + } + if (this.promotedContent.time == null) { return OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/StatusBarPopupChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/StatusBarPopupChips.kt new file mode 100644 index 000000000000..9f523fc845ab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/StatusBarPopupChips.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips.notification.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the status bar popup chips flag state. */ +@Suppress("NOTHING_TO_INLINE") +object StatusBarPopupChips { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_STATUS_BAR_POPUP_CHIPS + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.statusBarPopupChips() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is not enabled to ensure that the refactor author catches issues in testing. + * Caution!! Using this check incorrectly will cause crashes in nextfood builds! + */ + @JvmStatic + inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt new file mode 100644 index 000000000000..1663aebd7287 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.featurepods.popups.shared.model + +import com.android.systemui.common.shared.model.Icon + +/** + * Ids used to track different types of popup chips. Will be used to ensure only one chip is + * displaying its popup at a time. + */ +sealed class PopupChipId(val value: String) { + data object MediaControls : PopupChipId("MediaControls") +} + +/** Model for individual status bar popup chips. */ +sealed class PopupChipModel { + abstract val logName: String + abstract val chipId: PopupChipId + + data class Hidden(override val chipId: PopupChipId, val shouldAnimate: Boolean = true) : + PopupChipModel() { + override val logName = "Hidden(id=$chipId, anim=$shouldAnimate)" + } + + data class Shown( + override val chipId: PopupChipId, + val icon: Icon, + val chipText: String, + val isToggled: Boolean = false, + val onToggle: () -> Unit, + val onIconPressed: () -> Unit, + ) : PopupChipModel() { + override val logName = "Shown(id=$chipId, toggled=$isToggled)" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipViewModel.kt new file mode 100644 index 000000000000..5712be30ccd6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipViewModel.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.featurepods.popups.ui.viewmodel + +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel +import kotlinx.coroutines.flow.StateFlow + +/** + * Interface for a view model that knows the display requirements for a single type of status bar + * popup chip. + */ +interface StatusBarPopupChipViewModel { + /** A flow modeling the popup chip that should be shown (or not shown). */ + val chip: StateFlow<PopupChipModel> +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt new file mode 100644 index 000000000000..b390f29b166c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.featurepods.popups.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * View model deciding which system process chips to show in the status bar. Emits a list of + * PopupChipModels. + */ +@SysUISingleton +class StatusBarPopupChipsViewModel @Inject constructor(@Background scope: CoroutineScope) { + private data class PopupChipBundle( + val media: PopupChipModel = PopupChipModel.Hidden(chipId = PopupChipId.MediaControls) + ) + + private val incomingPopupChipBundle: Flow<PopupChipBundle?> = + flowOf(null).stateIn(scope, SharingStarted.Lazily, PopupChipBundle()) + + val popupChips: Flow<List<PopupChipModel>> = + incomingPopupChipBundle + .map { _ -> listOf(null).filterIsInstance<PopupChipModel.Shown>() } + .stateIn(scope, SharingStarted.Lazily, emptyList()) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java index 80e8f55b897a..d83acf34ca99 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.collection.inflation; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC; @@ -186,6 +187,9 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC); if (AsyncHybridViewInflation.isEnabled()) { params.markContentViewsFreeable(FLAG_CONTENT_VIEW_SINGLE_LINE); + if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { + params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE); + } } mRowContentBindStage.requestRebind(entry, null); } @@ -256,10 +260,10 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { params.requireContentViews(FLAG_CONTENT_VIEW_EXPANDED); params.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight); params.setUseMinimized(isMinimized); - // TODO b/358403414: use the different types of redaction - boolean needsRedaction = inflaterParams.getRedactionType() != REDACTION_TYPE_NONE; + int redactionType = inflaterParams.getRedactionType(); - if (needsRedaction) { + params.setRedactionType(redactionType); + if (redactionType != REDACTION_TYPE_NONE) { params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC); } else { params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC); @@ -276,8 +280,8 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { } if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { - - if (inflaterParams.isChildInGroup() && needsRedaction) { + if (inflaterParams.isChildInGroup() + && redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE); } else { params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt index 13ad1413e89d..a43f8dbc1b5d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.promoted import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel.ERROR import com.android.systemui.log.core.LogLevel.INFO -import com.android.systemui.log.dagger.NotificationLog import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel @@ -27,7 +26,7 @@ import javax.inject.Inject class PromotedNotificationLogger @Inject -constructor(@NotificationLog private val buffer: LogBuffer) { +constructor(@PromotedNotificationLog private val buffer: LogBuffer) { fun logExtractionSkipped(entry: NotificationEntry, reason: String) { buffer.log( EXTRACTION_TAG, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt new file mode 100644 index 000000000000..0f21514fcc94 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.promoted.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +@SysUISingleton +class AODPromotedNotificationInteractor +@Inject +constructor(activeNotificationsInteractor: ActiveNotificationsInteractor) { + val content: Flow<PromotedNotificationContentModel?> = + activeNotificationsInteractor.topLevelRepresentativeNotifications.map { notifs -> + notifs.firstNotNullOfOrNull { it.promotedContent } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt index fe2dabe1ba8a..74809fd8622f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt @@ -28,7 +28,7 @@ import com.android.systemui.statusbar.notification.promoted.PromotedNotification * like the skeleton view on AOD or the status bar chip. */ data class PromotedNotificationContentModel( - val key: String, + val identity: Identity, // for all styles: val skeletonSmallIcon: Icon?, // TODO(b/377568176): Make into an IconModel. @@ -82,7 +82,7 @@ data class PromotedNotificationContentModel( fun build() = PromotedNotificationContentModel( - key = key, + identity = Identity(key, style), skeletonSmallIcon = skeletonSmallIcon, appName = appName, subText = subText, @@ -103,6 +103,8 @@ data class PromotedNotificationContentModel( ) } + data class Identity(val key: String, val style: Style) + /** The timestamp associated with a notification, along with the mode used to display it. */ data class When(val time: Long, val mode: Mode) { /** The mode used to display a notification's `when` value. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/AODPromotedNotificationViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/AODPromotedNotificationViewModel.kt new file mode 100644 index 000000000000..adfa6a10814d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/AODPromotedNotificationViewModel.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.promoted.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.promoted.domain.interactor.AODPromotedNotificationInteractor +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Identity +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map + +@SysUISingleton +class AODPromotedNotificationViewModel +@Inject +constructor(interactor: AODPromotedNotificationInteractor) { + private val content: Flow<PromotedNotificationContentModel?> = interactor.content + private val identity: Flow<Identity?> = content.mapNonNullsKeepingNulls { it.identity } + + val notification: Flow<PromotedNotificationViewModel?> = + identity.distinctUntilChanged().mapNonNullsKeepingNulls { identity -> + val updates = interactor.content.filterNotNull().filter { it.identity == identity } + PromotedNotificationViewModel(identity, updates) + } +} + +private fun <T, R> Flow<T?>.mapNonNullsKeepingNulls(block: (T) -> R): Flow<R?> = map { + it?.let(block) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt new file mode 100644 index 000000000000..f265e0ff33f8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.promoted.ui.viewmodel + +import android.graphics.drawable.Icon +import com.android.internal.widget.NotificationProgressModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class PromotedNotificationViewModel( + identity: PromotedNotificationContentModel.Identity, + content: Flow<PromotedNotificationContentModel>, +) { + // for all styles: + + val key: String = identity.key + val style: Style = identity.style + + val skeletonSmallIcon: Flow<Icon?> = content.map { it.skeletonSmallIcon } + val appName: Flow<CharSequence?> = content.map { it.appName } + val subText: Flow<CharSequence?> = content.map { it.subText } + + private val time: Flow<When?> = content.map { it.time } + val whenTime: Flow<Long?> = time.map { it?.time } + val whenMode: Flow<When.Mode?> = time.map { it?.mode } + + val lastAudiblyAlertedMs: Flow<Long> = content.map { it.lastAudiblyAlertedMs } + val profileBadgeResId: Flow<Int?> = content.map { it.profileBadgeResId } + val title: Flow<CharSequence?> = content.map { it.title } + val text: Flow<CharSequence?> = content.map { it.text } + val skeletonLargeIcon: Flow<Icon?> = content.map { it.skeletonLargeIcon } + + // for CallStyle: + val personIcon: Flow<Icon?> = content.map { it.personIcon } + val personName: Flow<CharSequence?> = content.map { it.personName } + val verificationIcon: Flow<Icon?> = content.map { it.verificationIcon } + val verificationText: Flow<CharSequence?> = content.map { it.verificationText } + + // for ProgressStyle: + val progress: Flow<NotificationProgressModel?> = content.map { it.progress } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 6eeb80d45211..70e27a981b49 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.row; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; @@ -25,6 +26,7 @@ import static com.android.systemui.statusbar.notification.row.NotificationConten import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Notification; +import android.app.Notification.MessagingStyle; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; @@ -161,9 +163,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder entry, mConversationProcessor, row, - bindParams.isMinimized, - bindParams.usesIncreasedHeight, - bindParams.usesIncreasedHeadsUpHeight, + bindParams, callback, mRemoteInputManager.getRemoteViewsOnClickHandler(), /* isMediaFlagEnabled = */ mIsMediaInQS, @@ -187,13 +187,13 @@ public class NotificationContentInflater implements NotificationRowContentBinder boolean inflateSynchronously, @InflationFlag int reInflateFlags, Notification.Builder builder, + Context systemUiContext, Context packageContext, SmartReplyStateInflater smartRepliesInflater) { InflationProgress result = createRemoteViews(reInflateFlags, builder, - bindParams.isMinimized, - bindParams.usesIncreasedHeight, - bindParams.usesIncreasedHeadsUpHeight, + bindParams, + systemUiContext, packageContext, row, mNotifLayoutInflaterFactoryProvider, @@ -203,18 +203,20 @@ public class NotificationContentInflater implements NotificationRowContentBinder result = inflateSmartReplyViews(result, reInflateFlags, entry, row.getContext(), packageContext, row.getExistingSmartReplyState(), smartRepliesInflater, mLogger); boolean isConversation = entry.getRanking().isConversation(); + Notification.MessagingStyle messagingStyle = null; + if (isConversation && (AsyncHybridViewInflation.isEnabled() + || LockscreenOtpRedaction.isSingleLineViewEnabled())) { + messagingStyle = mConversationProcessor + .processNotification(entry, builder, mLogger); + } if (AsyncHybridViewInflation.isEnabled()) { - Notification.MessagingStyle messagingStyle = null; - if (isConversation) { - messagingStyle = mConversationProcessor - .processNotification(entry, builder, mLogger); - } SingleLineViewModel viewModel = SingleLineViewInflater .inflateSingleLineViewModel( entry.getSbn().getNotification(), messagingStyle, builder, - row.getContext() + row.getContext(), + false ); // If the messagingStyle is null, we want to inflate the normal view isConversation = viewModel.isConversation(); @@ -228,11 +230,22 @@ public class NotificationContentInflater implements NotificationRowContentBinder mLogger ); } - if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { - result.mPublicInflatedSingleLineViewModel = - SingleLineViewInflater.inflateRedactedSingleLineViewModel(row.getContext(), - isConversation); + if (bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { + result.mPublicInflatedSingleLineViewModel = + SingleLineViewInflater.inflateSingleLineViewModel( + entry.getSbn().getNotification(), + messagingStyle, + builder, + row.getContext(), + true); + } else { + result.mPublicInflatedSingleLineViewModel = + SingleLineViewInflater.inflateRedactedSingleLineViewModel( + row.getContext(), + isConversation + ); + } result.mPublicInflatedSingleLineView = SingleLineViewInflater.inflatePublicSingleLineView( isConversation, @@ -411,8 +424,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder } private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags, - Notification.Builder builder, boolean isMinimized, boolean usesIncreasedHeight, - boolean usesIncreasedHeadsUpHeight, Context packageContext, + Notification.Builder builder, BindParams bindParams, Context systemUiContext, + Context packageContext, ExpandableNotificationRow row, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, HeadsUpStyleProvider headsUpStyleProvider, @@ -423,13 +436,13 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view"); - result.newContentView = createContentView(builder, isMinimized, - usesIncreasedHeight); + result.newContentView = createContentView(builder, bindParams.isMinimized, + bindParams.usesIncreasedHeight); } if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating expanded remote view"); - result.newExpandedView = createExpandedView(builder, isMinimized); + result.newExpandedView = createExpandedView(builder, bindParams.isMinimized); } if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { @@ -439,13 +452,20 @@ public class NotificationContentInflater implements NotificationRowContentBinder result.newHeadsUpView = builder.createCompactHeadsUpContentView(); } else { result.newHeadsUpView = builder.createHeadsUpContentView( - usesIncreasedHeadsUpHeight); + bindParams.usesIncreasedHeadsUpHeight); } } if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating public remote view"); - result.newPublicView = builder.makePublicContentView(isMinimized); + if (LockscreenOtpRedaction.isEnabled() + && bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { + result.newPublicView = createSensitiveContentMessageNotification( + row.getEntry().getSbn().getNotification(), builder.getStyle(), + systemUiContext, packageContext).createContentView(true); + } else { + result.newPublicView = builder.makePublicContentView(bindParams.isMinimized); + } } if (AsyncGroupHeaderViewInflation.isEnabled()) { @@ -473,6 +493,42 @@ public class NotificationContentInflater implements NotificationRowContentBinder }); } + private static Notification.Builder createSensitiveContentMessageNotification( + Notification original, + Notification.Style originalStyle, + Context systemUiContext, + Context packageContext) { + Notification.Builder redacted = + new Notification.Builder(packageContext, original.getChannelId()); + redacted.setContentTitle(original.extras.getCharSequence(Notification.EXTRA_TITLE)); + CharSequence redactedMessage = systemUiContext.getString( + R.string.redacted_notification_single_line_text + ); + + if (originalStyle instanceof MessagingStyle oldStyle) { + MessagingStyle newStyle = new MessagingStyle(oldStyle.getUser()); + newStyle.setConversationTitle(oldStyle.getConversationTitle()); + newStyle.setGroupConversation(false); + newStyle.setConversationType(oldStyle.getConversationType()); + newStyle.setShortcutIcon(oldStyle.getShortcutIcon()); + newStyle.setBuilder(redacted); + MessagingStyle.Message latestMessage = + MessagingStyle.findLatestIncomingMessage(oldStyle.getMessages()); + if (latestMessage != null) { + MessagingStyle.Message newMessage = new MessagingStyle.Message(redactedMessage, + latestMessage.getTimestamp(), latestMessage.getSenderPerson()); + newStyle.addMessage(newMessage); + } + redacted.setStyle(newStyle); + } else { + redacted.setContentText(redactedMessage); + } + redacted.setLargeIcon(original.getLargeIcon()); + redacted.setSmallIcon(original.getSmallIcon()); + return redacted; + } + + private static void setNotifsViewsInflaterFactory(InflationProgress result, ExpandableNotificationRow row, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider) { @@ -1118,10 +1174,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder private final NotificationEntry mEntry; private final Context mContext; private final boolean mInflateSynchronously; - private final boolean mIsMinimized; - private final boolean mUsesIncreasedHeight; + private final BindParams mBindParams; private final InflationCallback mCallback; - private final boolean mUsesIncreasedHeadsUpHeight; private final @InflationFlag int mReInflateFlags; private final NotifRemoteViewCache mRemoteViewCache; private final Executor mInflationExecutor; @@ -1145,9 +1199,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder NotificationEntry entry, ConversationNotificationProcessor conversationProcessor, ExpandableNotificationRow row, - boolean isMinimized, - boolean usesIncreasedHeight, - boolean usesIncreasedHeadsUpHeight, + BindParams bindParams, InflationCallback callback, RemoteViews.InteractionHandler remoteViewClickHandler, boolean isMediaFlagEnabled, @@ -1164,9 +1216,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mRemoteViewCache = cache; mSmartRepliesInflater = smartRepliesInflater; mContext = mRow.getContext(); - mIsMinimized = isMinimized; - mUsesIncreasedHeight = usesIncreasedHeight; - mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight; + mBindParams = bindParams; mRemoteViewClickHandler = remoteViewClickHandler; mCallback = callback; mConversationProcessor = conversationProcessor; @@ -1236,8 +1286,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mEntry, recoveredBuilder, mLogger); } InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, - recoveredBuilder, mIsMinimized, mUsesIncreasedHeight, - mUsesIncreasedHeadsUpHeight, packageContext, mRow, + recoveredBuilder, mBindParams, mContext, packageContext, mRow, mNotifLayoutInflaterFactoryProvider, mHeadsUpStyleProvider, mLogger); mLogger.logAsyncTaskProgress(mEntry, @@ -1264,7 +1313,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder mEntry.getSbn().getNotification(), messagingStyle, recoveredBuilder, - mContext + mContext, + false ); result.mInflatedSingleLineView = SingleLineViewInflater.inflatePrivateSingleLineView( @@ -1277,9 +1327,22 @@ public class NotificationContentInflater implements NotificationRowContentBinder } if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { - result.mPublicInflatedSingleLineViewModel = - SingleLineViewInflater.inflateRedactedSingleLineViewModel(mContext, - isConversation); + if (mBindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { + result.mPublicInflatedSingleLineViewModel = + SingleLineViewInflater.inflateSingleLineViewModel( + mEntry.getSbn().getNotification(), + messagingStyle, + recoveredBuilder, + mContext, + true + ); + } else { + result.mPublicInflatedSingleLineViewModel = + SingleLineViewInflater.inflateRedactedSingleLineViewModel( + mContext, + isConversation + ); + } result.mPublicInflatedSingleLineView = SingleLineViewInflater.inflatePublicSingleLineView( isConversation, @@ -1320,7 +1383,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mCancellationSignal = apply( mInflationExecutor, mInflateSynchronously, - mIsMinimized, + mBindParams.isMinimized, result, mReInflateFlags, mRemoteViewCache, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java index 07384afe2d2e..1cef8791e0ea 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.row; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType; + import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -141,20 +143,33 @@ public interface NotificationRowContentBinder { */ class BindParams { + public BindParams(boolean minimized, boolean increasedHeight, + boolean increasedHeadsUpHeight, int redaction) { + isMinimized = minimized; + usesIncreasedHeight = increasedHeight; + usesIncreasedHeadsUpHeight = increasedHeadsUpHeight; + redactionType = redaction; + } + /** * Bind a minimized version of the content views. */ - public boolean isMinimized; + public final boolean isMinimized; /** * Use increased height when binding contracted view. */ - public boolean usesIncreasedHeight; + public final boolean usesIncreasedHeight; /** * Use increased height when binding heads up views. */ - public boolean usesIncreasedHeadsUpHeight; + public final boolean usesIncreasedHeadsUpHeight; + + /** + * Controls the type of public view to show, if a public view is requested + */ + public final @RedactionType int redactionType; } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index 7dcb2de57e56..c619b17f1ad8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.row import android.annotation.SuppressLint import android.app.Notification +import android.app.Notification.MessagingStyle import android.content.Context import android.content.ContextWrapper import android.content.pm.ApplicationInfo @@ -42,6 +43,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.NotifInflation import com.android.systemui.res.R import com.android.systemui.statusbar.InflationTask +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.notification.ConversationNotificationProcessor import com.android.systemui.statusbar.notification.InflationException @@ -142,9 +144,7 @@ constructor( entry, conversationProcessor, row, - bindParams.isMinimized, - bindParams.usesIncreasedHeight, - bindParams.usesIncreasedHeadsUpHeight, + bindParams, callback, remoteInputManager.remoteViewsOnClickHandler, /* isMediaFlagEnabled = */ smartReplyStateInflater, @@ -178,10 +178,8 @@ constructor( reInflateFlags = reInflateFlags, entry = entry, builder = builder, - isMinimized = bindParams.isMinimized, - usesIncreasedHeight = bindParams.usesIncreasedHeight, - usesIncreasedHeadsUpHeight = bindParams.usesIncreasedHeadsUpHeight, - systemUIContext = systemUIContext, + bindParams, + systemUiContext = systemUIContext, packageContext = packageContext, row = row, notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider, @@ -370,9 +368,7 @@ constructor( private val entry: NotificationEntry, private val conversationProcessor: ConversationNotificationProcessor, private val row: ExpandableNotificationRow, - private val isMinimized: Boolean, - private val usesIncreasedHeight: Boolean, - private val usesIncreasedHeadsUpHeight: Boolean, + private val bindParams: BindParams, private val callback: InflationCallback?, private val remoteViewClickHandler: InteractionHandler?, private val smartRepliesInflater: SmartReplyStateInflater, @@ -440,10 +436,8 @@ constructor( reInflateFlags = reInflateFlags, entry = entry, builder = recoveredBuilder, - isMinimized = isMinimized, - usesIncreasedHeight = usesIncreasedHeight, - usesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight, - systemUIContext = context, + bindParams = bindParams, + systemUiContext = context, packageContext = packageContext, row = row, notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider, @@ -513,7 +507,7 @@ constructor( apply( inflationExecutor, inflateSynchronously, - isMinimized, + bindParams.isMinimized, progress, reInflateFlags, remoteViewCache, @@ -670,10 +664,8 @@ constructor( @InflationFlag reInflateFlags: Int, entry: NotificationEntry, builder: Notification.Builder, - isMinimized: Boolean, - usesIncreasedHeight: Boolean, - usesIncreasedHeadsUpHeight: Boolean, - systemUIContext: Context, + bindParams: BindParams, + systemUiContext: Context, packageContext: Context, row: ExpandableNotificationRow, notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider, @@ -705,9 +697,10 @@ constructor( createRemoteViews( reInflateFlags = reInflateFlags, builder = builder, - isMinimized = isMinimized, - usesIncreasedHeight = usesIncreasedHeight, - usesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight, + bindParams = bindParams, + entry = entry, + systemUiContext = systemUiContext, + packageContext = packageContext, row = row, notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider, headsUpStyleProvider = headsUpStyleProvider, @@ -724,7 +717,8 @@ constructor( notification = entry.sbn.notification, messagingStyle = messagingStyle, builder = builder, - systemUiContext = systemUIContext, + systemUiContext = systemUiContext, + redactText = false, ) } else null @@ -734,10 +728,20 @@ constructor( reInflateFlags and FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE != 0 ) { logger.logAsyncTaskProgress(entry, "inflating public single line view model") - SingleLineViewInflater.inflateRedactedSingleLineViewModel( - systemUIContext, - entry.ranking.isConversation, - ) + if (bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { + SingleLineViewInflater.inflateSingleLineViewModel( + notification = entry.sbn.notification, + messagingStyle = messagingStyle, + builder = builder, + systemUiContext = systemUiContext, + redactText = true, + ) + } else { + SingleLineViewInflater.inflateRedactedSingleLineViewModel( + systemUiContext, + entry.ranking.isConversation, + ) + } } else null val headsUpStatusBarModel = @@ -761,12 +765,50 @@ constructor( ) } + private fun createSensitiveContentMessageNotification( + original: Notification, + originalStyle: Notification.Style?, + sysUiContext: Context, + packageContext: Context, + ): Notification.Builder { + val redacted = Notification.Builder(packageContext, original.channelId) + redacted.setContentTitle(original.extras.getCharSequence(Notification.EXTRA_TITLE)) + val redactedMessage = + sysUiContext.getString(R.string.redacted_notification_single_line_text) + + if (originalStyle is MessagingStyle) { + val newStyle = MessagingStyle(originalStyle.user) + newStyle.conversationTitle = originalStyle.conversationTitle + newStyle.isGroupConversation = false + newStyle.conversationType = originalStyle.conversationType + newStyle.shortcutIcon = originalStyle.shortcutIcon + newStyle.setBuilder(redacted) + val latestMessage = MessagingStyle.findLatestIncomingMessage(originalStyle.messages) + if (latestMessage != null) { + val newMessage = + MessagingStyle.Message( + redactedMessage, + latestMessage.timestamp, + latestMessage.senderPerson, + ) + newStyle.addMessage(newMessage) + } + redacted.style = newStyle + } else { + redacted.setContentText(redactedMessage) + } + redacted.setLargeIcon(original.getLargeIcon()) + redacted.setSmallIcon(original.smallIcon) + return redacted + } + private fun createRemoteViews( @InflationFlag reInflateFlags: Int, builder: Notification.Builder, - isMinimized: Boolean, - usesIncreasedHeight: Boolean, - usesIncreasedHeadsUpHeight: Boolean, + bindParams: BindParams, + entry: NotificationEntry, + systemUiContext: Context, + packageContext: Context, row: ExpandableNotificationRow, notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider, headsUpStyleProvider: HeadsUpStyleProvider, @@ -780,7 +822,11 @@ constructor( entryForLogging, "creating contracted remote view", ) - createContentView(builder, isMinimized, usesIncreasedHeight) + createContentView( + builder, + bindParams.isMinimized, + bindParams.usesIncreasedHeight, + ) } else null val expanded = if (reInflateFlags and FLAG_CONTENT_VIEW_EXPANDED != 0) { @@ -788,7 +834,7 @@ constructor( entryForLogging, "creating expanded remote view", ) - createExpandedView(builder, isMinimized) + createExpandedView(builder, bindParams.isMinimized) } else null val headsUp = if (reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0) { @@ -800,13 +846,26 @@ constructor( if (isHeadsUpCompact) { builder.createCompactHeadsUpContentView() } else { - builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight) + builder.createHeadsUpContentView(bindParams.usesIncreasedHeadsUpHeight) } } else null val public = if (reInflateFlags and FLAG_CONTENT_VIEW_PUBLIC != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating public remote view") - builder.makePublicContentView(isMinimized) + if ( + LockscreenOtpRedaction.isEnabled && + bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT + ) { + createSensitiveContentMessageNotification( + entry.sbn.notification, + builder.style, + systemUiContext, + packageContext, + ) + .createContentView(bindParams.usesIncreasedHeight) + } else { + builder.makePublicContentView(bindParams.isMinimized) + } } else null val normalGroupHeader = if ( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java index 427fb66ca2d0..bc44cb0e1074 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.row; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP; @@ -31,6 +33,7 @@ public final class RowContentBindParams { private boolean mUseIncreasedHeadsUpHeight; private boolean mViewsNeedReinflation; private @InflationFlag int mContentViews = DEFAULT_INFLATION_FLAGS; + private @RedactionType int mRedactionType = REDACTION_TYPE_NONE; /** * Content views that are out of date and need to be rebound. @@ -58,6 +61,20 @@ public final class RowContentBindParams { } /** + * @return What type of redaction should be used by the public view (if requested) + */ + public @RedactionType int getRedactionType() { + return mRedactionType; + } + + /** + * Set the redaction type, which controls what sort of public view is shown. + */ + public void setRedactionType(@RedactionType int redactionType) { + mRedactionType = redactionType; + } + + /** * Set whether content should use an increased height version of its contracted view. */ public void setUseIncreasedCollapsedHeight(boolean useIncreasedHeight) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java index 89fcda949b5b..53f74161e7fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java @@ -72,10 +72,8 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { // Bind/unbind with parameters mBinder.unbindContent(entry, row, contentToUnbind); - BindParams bindParams = new BindParams(); - bindParams.isMinimized = params.useMinimized(); - bindParams.usesIncreasedHeight = params.useIncreasedHeight(); - bindParams.usesIncreasedHeadsUpHeight = params.useIncreasedHeadsUpHeight(); + BindParams bindParams = new BindParams(params.useMinimized(), params.useIncreasedHeight(), + params.useIncreasedHeadsUpHeight(), params.getRedactionType()); boolean forceInflate = params.needsReinflation(); InflationCallback inflationCallback = new InflationCallback() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt index e702f10d7f50..fe2803bfc5d6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt @@ -51,6 +51,7 @@ internal object SingleLineViewInflater { * notification, not for legacy messaging notifications * @param builder the recovered Notification Builder * @param systemUiContext the context of Android System UI + * @param redactText indicates if the text needs to be redacted * @return the inflated SingleLineViewModel */ @JvmStatic @@ -59,13 +60,21 @@ internal object SingleLineViewInflater { messagingStyle: MessagingStyle?, builder: Notification.Builder, systemUiContext: Context, + redactText: Boolean, ): SingleLineViewModel { if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { return SingleLineViewModel(null, null, null) } peopleHelper.init(systemUiContext) var titleText = HybridGroupManager.resolveTitle(notification) - var contentText = HybridGroupManager.resolveText(notification) + var contentText = + if (redactText) { + systemUiContext.getString( + com.android.systemui.res.R.string.redacted_notification_single_line_text + ) + } else { + HybridGroupManager.resolveText(notification) + } if (messagingStyle == null) { return SingleLineViewModel( @@ -81,7 +90,7 @@ internal object SingleLineViewInflater { if (conversationTextData?.conversationTitle?.isNotEmpty() == true) { titleText = conversationTextData.conversationTitle } - if (conversationTextData?.conversationText?.isNotEmpty() == true) { + if (!redactText && conversationTextData?.conversationText?.isNotEmpty() == true) { contentText = conversationTextData.conversationText } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt index 42acd7bcdc8a..705845ff984c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt @@ -75,7 +75,7 @@ class SharedNotificationContainer(context: Context, attrs: AttributeSet?) : constraintSet.apply { if (SceneContainerFlag.isEnabled) { when (horizontalPosition) { - is HorizontalPosition.FloatAtEnd -> + is HorizontalPosition.FloatAtStart -> constrainWidth(nsslId, horizontalPosition.width) is HorizontalPosition.MiddleToEdge -> setGuidelinePercent(R.id.nssl_guideline, horizontalPosition.ratio) @@ -83,13 +83,13 @@ class SharedNotificationContainer(context: Context, attrs: AttributeSet?) : } } + connect(nsslId, START, startConstraintId, START, marginStart) if ( !SceneContainerFlag.isEnabled || - horizontalPosition !is HorizontalPosition.FloatAtEnd + horizontalPosition !is HorizontalPosition.FloatAtStart ) { - connect(nsslId, START, startConstraintId, START, marginStart) + connect(nsslId, END, PARENT_ID, END, marginEnd) } - connect(nsslId, END, PARENT_ID, END, marginEnd) connect(nsslId, BOTTOM, PARENT_ID, BOTTOM, marginBottom) connect(nsslId, TOP, PARENT_ID, TOP, marginTop) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index b81c71ebe19b..fc8c70fb8e9a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -247,7 +247,7 @@ constructor( Split -> HorizontalPosition.MiddleToEdge(ratio = 0.5f) Dual -> if (isShadeLayoutWide) { - HorizontalPosition.FloatAtEnd( + HorizontalPosition.FloatAtStart( width = getDimensionPixelSize(R.dimen.shade_panel_width) ) } else { @@ -830,10 +830,10 @@ constructor( data class MiddleToEdge(val ratio: Float = 0.5f) : HorizontalPosition /** - * The container has a fixed [width] and is aligned to the end of the screen. In this - * layout, the start edge of the container is floating, i.e. unconstrained. + * The container has a fixed [width] and is aligned to the start of the screen. In this + * layout, the end edge of the container is floating, i.e. unconstrained. */ - data class FloatAtEnd(val width: Int) : HorizontalPosition + data class FloatAtStart(val width: Int) : HorizontalPosition } /** diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt index a382cf921152..e08114f6c3cd 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt @@ -21,10 +21,8 @@ import android.content.Context import android.hardware.devicestate.DeviceStateManager import android.os.PowerManager import android.provider.Settings -import androidx.core.view.OneShotPreDrawListener import com.android.internal.util.LatencyTracker import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.ToAodFoldTransitionInteractor @@ -125,11 +123,7 @@ constructor( private val shadeFoldAnimator: ShadeFoldAnimator get() { - return if (MigrateClocksToBlueprint.isEnabled) { - foldTransitionInteractor.get().foldAnimator - } else { - shadeViewController.shadeFoldAnimator - } + return foldTransitionInteractor.get().foldAnimator } private fun setAnimationState(playing: Boolean) { @@ -164,15 +158,7 @@ constructor( setAnimationState(playing = true) shadeFoldAnimator.prepareFoldToAodAnimation() - // We don't need to wait for the scrim as it is already displayed - // but we should wait for the initial animation preparations to be drawn - // (setting initial alpha/translation) - // TODO(b/254878364): remove this call to NPVC.getView() - if (!MigrateClocksToBlueprint.isEnabled) { - shadeFoldAnimator.view?.let { OneShotPreDrawListener.add(it, onReady) } - } else { - onReady.run() - } + onReady.run() } else { // No animation, call ready callback immediately onReady.run() @@ -252,7 +238,7 @@ constructor( if (isFolded) { foldToAodLatencyTracker.onFolded() } - } + }, ) /** @@ -272,6 +258,7 @@ constructor( latencyTracker.onActionStart(LatencyTracker.ACTION_FOLD_TO_AOD) } } + /** * Called once the Fold -> AOD animation is started. * diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index 32fa160fc29f..21dd5bc068f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -344,7 +344,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { canShowWhileLocked = canShowWhileLocked, ) } else { - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } underTest.onQuickAffordanceTriggered( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt index eb19a9c2528d..1ce128c2403a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt @@ -347,7 +347,7 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() { canShowWhileLocked = canShowWhileLocked, ) } else { - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } underTest.onQuickAffordanceTriggered( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt index 503fa789cb80..1eb88c5a5616 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt @@ -89,6 +89,7 @@ class SingleLineViewBinderTest : SysuiTestCase() { messagingStyle = null, builder = notificationBuilder, systemUiContext = context, + redactText = false, ) // WHEN: binds the viewHolder @@ -149,6 +150,7 @@ class SingleLineViewBinderTest : SysuiTestCase() { messagingStyle = style, builder = notificationBuilder, systemUiContext = context, + redactText = false, ) // WHEN: binds the view SingleLineViewBinder.bind(viewModel, view) @@ -197,6 +199,7 @@ class SingleLineViewBinderTest : SysuiTestCase() { messagingStyle = null, builder = notificationBuilder, systemUiContext = context, + redactText = false, ) // WHEN: binds the view with the view model SingleLineViewBinder.bind(viewModel, view) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt index d3666321c8e4..ef70e277832e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt @@ -379,7 +379,8 @@ class SingleLineViewInflaterTest : SysuiTestCase() { this, if (isConversation) messagingStyle else null, builder, - context + context, + false ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorKosmos.kt new file mode 100644 index 000000000000..57c8fd066ea8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.scene.domain.interactor.sceneInteractor + +val Kosmos.communalBackActionInteractor by + Kosmos.Fixture { + CommunalBackActionInteractor( + communalInteractor = communalInteractor, + communalSceneInteractor = communalSceneInteractor, + sceneInteractor = sceneInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt index 548b5646f5d4..5d206691b520 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt @@ -30,7 +30,7 @@ class FakeKeyguardQuickAffordanceConfig( override val pickerIconResourceId: Int = 0, ) : KeyguardQuickAffordanceConfig { - var onTriggeredResult: OnTriggeredResult = OnTriggeredResult.Handled + var onTriggeredResult: OnTriggeredResult = OnTriggeredResult.Handled(false) private val _lockScreenState = MutableStateFlow<KeyguardQuickAffordanceConfig.LockScreenState>( @@ -41,9 +41,7 @@ class FakeKeyguardQuickAffordanceConfig( override fun pickerName(): String = pickerName - override fun onTriggered( - expandable: Expandable?, - ): OnTriggeredResult { + override fun onTriggered(expandable: Expandable?): OnTriggeredResult { return onTriggeredResult } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceHapticViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceHapticViewModelKosmos.kt new file mode 100644 index 000000000000..d857157137b6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceHapticViewModelKosmos.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceHapticViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel +import com.android.systemui.kosmos.Kosmos +import kotlinx.coroutines.flow.Flow + +val Kosmos.keyguardQuickAffordanceHapticViewModelFactory by + Kosmos.Fixture { + object : KeyguardQuickAffordanceHapticViewModel.Factory { + override fun create( + quickAffordanceViewModel: Flow<KeyguardQuickAffordanceViewModel> + ): KeyguardQuickAffordanceHapticViewModel = + KeyguardQuickAffordanceHapticViewModel( + quickAffordanceViewModel, + keyguardQuickAffordanceInteractor, + ) + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index e47310727905..abbfa93edd17 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -87,7 +87,6 @@ val Kosmos.keyguardRootViewModel by Fixture { primaryBouncerToLockscreenTransitionViewModel, screenOffAnimationController = screenOffAnimationController, aodBurnInViewModel = aodBurnInViewModel, - aodAlphaViewModel = aodAlphaViewModel, shadeInteractor = shadeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt index ab1c1818bf80..aa29808bd9ee 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt @@ -45,7 +45,6 @@ private constructor(failureMetadata: FailureMetadata, subject: QSTileState?) : other ?: return } check("icon").that(actual.icon).isEqualTo(other.icon) - check("iconRes").that(actual.iconRes).isEqualTo(other.iconRes) check("label").that(actual.label).isEqualTo(other.label) check("activationState").that(actual.activationState).isEqualTo(other.activationState) check("secondaryLabel").that(actual.secondaryLabel).isEqualTo(other.secondaryLabel) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt new file mode 100644 index 000000000000..62cdc87f980f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.featurepods.popups.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope + +val Kosmos.statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel by + Kosmos.Fixture { StatusBarPopupChipsViewModel(testScope.backgroundScope) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt index a3572754ab19..f31697e82a45 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt @@ -70,6 +70,14 @@ class FakeSettings : SecureSettings, SystemSettings, UserSettingsProxy { } } + fun getContentObservers(uri: Uri, userHandle: Int): List<ContentObserver> { + if (userHandle == UserHandle.USER_ALL) { + return contentObserversAllUsers[uri.toString()] ?: listOf() + } else { + return contentObservers[SettingsKey(userHandle, uri.toString())] ?: listOf() + } + } + override fun getContentResolver(): ContentResolver { throw UnsupportedOperationException("FakeSettings.getContentResolver is not implemented") } diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index 3441d94facda..9ceca5d1dbfe 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -1589,7 +1589,13 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ * lock because this calls out to WindowManagerService. */ void addWindowTokensForAllDisplays() { - final Display[] displays = mDisplayManager.getDisplays(); + Display[] displays = {}; + final long identity = Binder.clearCallingIdentity(); + try { + displays = mDisplayManager.getDisplays(); + } finally { + Binder.restoreCallingIdentity(identity); + } for (int i = 0; i < displays.length; i++) { final int displayId = displays[i].getDisplayId(); addWindowTokenForDisplay(displayId); @@ -1625,7 +1631,13 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } public void onRemoved() { - final Display[] displays = mDisplayManager.getDisplays(); + Display[] displays = {}; + final long identity = Binder.clearCallingIdentity(); + try { + displays = mDisplayManager.getDisplays(); + } finally { + Binder.restoreCallingIdentity(identity); + } for (int i = 0; i < displays.length; i++) { final int displayId = displays[i].getDisplayId(); onDisplayRemoved(displayId); diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index 4944cafeb83d..4b8770b3cd35 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -16,6 +16,7 @@ package com.android.server.am; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_RECONFIGURATION; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION; @@ -31,6 +32,7 @@ import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROA import static com.android.server.am.BroadcastConstants.getDeviceConfigBoolean; import android.annotation.NonNull; +import android.app.ActivityManagerInternal; import android.app.ActivityThread; import android.app.ForegroundServiceTypePolicy; import android.content.ComponentName; @@ -55,6 +57,7 @@ import android.util.SparseBooleanArray; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; +import com.android.server.LocalServices; import dalvik.annotation.optimization.NeverCompile; @@ -181,6 +184,12 @@ final class ActivityManagerConstants extends ContentObserver { static final String KEY_FOLLOW_UP_OOMADJ_UPDATE_WAIT_DURATION = "follow_up_oomadj_update_wait_duration"; + /* + * Oom score cutoff beyond which any process that does not have the CPU_TIME capability will be + * frozen. + */ + static final String KEY_FREEZER_CUTOFF_ADJ = "freezer_cutoff_adj"; + private static final int DEFAULT_MAX_CACHED_PROCESSES = 1024; private static final boolean DEFAULT_PRIORITIZE_ALARM_BROADCASTS = true; private static final long DEFAULT_FGSERVICE_MIN_SHOWN_TIME = 2*1000; @@ -267,6 +276,9 @@ final class ActivityManagerConstants extends ContentObserver { */ private static final long DEFAULT_FOLLOW_UP_OOMADJ_UPDATE_WAIT_DURATION = 1000L; + /** The default value to {@link #KEY_FREEZER_CUTOFF_ADJ} */ + private static final int DEFAULT_FREEZER_CUTOFF_ADJ = ProcessList.CACHED_APP_MIN_ADJ; + /** * Same as {@link TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED} */ @@ -1171,6 +1183,14 @@ final class ActivityManagerConstants extends ContentObserver { DEFAULT_FOLLOW_UP_OOMADJ_UPDATE_WAIT_DURATION; /** + * The cutoff adj for the freezer, app processes with adj greater than this value will be + * eligible for the freezer. + * + * @see #KEY_FREEZER_CUTOFF_ADJ + */ + public int FREEZER_CUTOFF_ADJ = DEFAULT_FREEZER_CUTOFF_ADJ; + + /** * Indicates whether PSS profiling in AppProfiler is disabled or not. */ static final String KEY_DISABLE_APP_PROFILER_PSS_PROFILING = @@ -1194,6 +1214,7 @@ final class ActivityManagerConstants extends ContentObserver { new OnPropertiesChangedListener() { @Override public void onPropertiesChanged(Properties properties) { + boolean oomAdjusterConfigUpdated = false; for (String name : properties.getKeyset()) { if (name == null) { return; @@ -1372,6 +1393,11 @@ final class ActivityManagerConstants extends ContentObserver { case KEY_TIERED_CACHED_ADJ_UI_TIER_SIZE: updateUseTieredCachedAdj(); break; + case KEY_FREEZER_CUTOFF_ADJ: + FREEZER_CUTOFF_ADJ = properties.getInt(KEY_FREEZER_CUTOFF_ADJ, + DEFAULT_FREEZER_CUTOFF_ADJ); + oomAdjusterConfigUpdated = true; + break; case KEY_DISABLE_APP_PROFILER_PSS_PROFILING: updateDisableAppProfilerPssProfiling(); break; @@ -1389,6 +1415,13 @@ final class ActivityManagerConstants extends ContentObserver { break; } } + if (oomAdjusterConfigUpdated) { + final ActivityManagerInternal ami = LocalServices.getService( + ActivityManagerInternal.class); + if (ami != null) { + ami.updateOomAdj(OOM_ADJ_REASON_RECONFIGURATION); + } + } } }; @@ -2534,6 +2567,9 @@ final class ActivityManagerConstants extends ContentObserver { pw.print(" "); pw.print(KEY_ENABLE_NEW_OOMADJ); pw.print("="); pw.println(ENABLE_NEW_OOMADJ); + pw.print(" "); pw.print(KEY_FREEZER_CUTOFF_ADJ); + pw.print("="); pw.println(FREEZER_CUTOFF_ADJ); + pw.print(" "); pw.print(KEY_DISABLE_APP_PROFILER_PSS_PROFILING); pw.print("="); pw.println(APP_PROFILER_PSS_PROFILING_DISABLED); diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 2f5362f53361..d335529a006a 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -25,9 +25,11 @@ import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_BIND_SERVICE; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_COMPONENT_DISABLED; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_EXECUTING_SERVICE; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_FINISH_RECEIVER; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_FOLLOW_UP; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_GET_PROVIDER; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_BEGIN; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_END; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_RECONFIGURATION; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_REMOVE_PROVIDER; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_REMOVE_TASK; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_RESTRICTION_CHANGE; @@ -203,6 +205,10 @@ public class CachedAppOptimizer { FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_RESTRICTION_CHANGE; static final int UNFREEZE_REASON_COMPONENT_DISABLED = FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_COMPONENT_DISABLED; + static final int UNFREEZE_REASON_OOM_ADJ_FOLLOW_UP = + FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_OOM_ADJ_FOLLOW_UP; + static final int UNFREEZE_REASON_OOM_ADJ_RECONFIGURATION = + FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_OOM_ADJ_RECONFIGURATION; @IntDef(prefix = {"UNFREEZE_REASON_"}, value = { UNFREEZE_REASON_NONE, @@ -234,6 +240,8 @@ public class CachedAppOptimizer { UNFREEZE_REASON_EXECUTING_SERVICE, UNFREEZE_REASON_RESTRICTION_CHANGE, UNFREEZE_REASON_COMPONENT_DISABLED, + UNFREEZE_REASON_OOM_ADJ_FOLLOW_UP, + UNFREEZE_REASON_OOM_ADJ_RECONFIGURATION, }) @Retention(RetentionPolicy.SOURCE) public @interface UnfreezeReason {} @@ -2451,8 +2459,8 @@ public class CachedAppOptimizer { synchronized (mAm.mPidsSelfLocked) { pr = mAm.mPidsSelfLocked.get(blocked); } - if (pr != null - && pr.mState.getCurAdj() < ProcessList.FREEZER_CUTOFF_ADJ) { + if (pr != null && pr.mState.getCurAdj() + < mAm.mConstants.FREEZER_CUTOFF_ADJ) { Slog.d(TAG_AM, app.processName + " (" + pid + ") blocks " + pr.processName + " (" + blocked + ")"); // Found at least one blocked non-cached process @@ -2539,6 +2547,10 @@ public class CachedAppOptimizer { return UNFREEZE_REASON_RESTRICTION_CHANGE; case OOM_ADJ_REASON_COMPONENT_DISABLED: return UNFREEZE_REASON_COMPONENT_DISABLED; + case OOM_ADJ_REASON_FOLLOW_UP: + return UNFREEZE_REASON_OOM_ADJ_FOLLOW_UP; + case OOM_ADJ_REASON_RECONFIGURATION: + return UNFREEZE_REASON_OOM_ADJ_RECONFIGURATION; default: return UNFREEZE_REASON_NONE; } diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index aadf6f61956c..9c569db99797 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -55,6 +55,7 @@ import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_GET_PROVIDER; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_NONE; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_BEGIN; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_END; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_RECONFIGURATION; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_REMOVE_PROVIDER; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_REMOVE_TASK; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_RESTRICTION_CHANGE; @@ -105,7 +106,6 @@ import static com.android.server.am.ProcessList.CACHED_APP_IMPORTANCE_LEVELS; import static com.android.server.am.ProcessList.CACHED_APP_MAX_ADJ; import static com.android.server.am.ProcessList.CACHED_APP_MIN_ADJ; import static com.android.server.am.ProcessList.FOREGROUND_APP_ADJ; -import static com.android.server.am.ProcessList.FREEZER_CUTOFF_ADJ; import static com.android.server.am.ProcessList.HEAVY_WEIGHT_APP_ADJ; import static com.android.server.am.ProcessList.HOME_APP_ADJ; import static com.android.server.am.ProcessList.INVALID_ADJ; @@ -232,6 +232,8 @@ public class OomAdjuster { return AppProtoEnums.OOM_ADJ_REASON_COMPONENT_DISABLED; case OOM_ADJ_REASON_FOLLOW_UP: return AppProtoEnums.OOM_ADJ_REASON_FOLLOW_UP; + case OOM_ADJ_REASON_RECONFIGURATION: + return AppProtoEnums.OOM_ADJ_REASON_RECONFIGURATION; default: return AppProtoEnums.OOM_ADJ_REASON_UNKNOWN_TO_PROTO; } @@ -288,6 +290,8 @@ public class OomAdjuster { return OOM_ADJ_REASON_METHOD + "_componentDisabled"; case OOM_ADJ_REASON_FOLLOW_UP: return OOM_ADJ_REASON_METHOD + "_followUp"; + case OOM_ADJ_REASON_RECONFIGURATION: + return OOM_ADJ_REASON_METHOD + "_reconfiguration"; default: return "_unknown"; } @@ -4079,7 +4083,7 @@ public class OomAdjuster { } // Reasons to freeze: - if (proc.mState.getCurAdj() >= FREEZER_CUTOFF_ADJ) { + if (proc.mState.getCurAdj() >= mConstants.FREEZER_CUTOFF_ADJ) { // Oomscore is in a high enough state, it is safe to freeze. return true; } @@ -4098,9 +4102,8 @@ public class OomAdjuster { final ProcessCachedOptimizerRecord opt = app.mOptRecord; final ProcessStateRecord state = app.mState; if (Flags.traceUpdateAppFreezeStateLsp()) { - final boolean oomAdjChanged = - (state.getCurAdj() >= FREEZER_CUTOFF_ADJ ^ oldOomAdj >= FREEZER_CUTOFF_ADJ) - || oldOomAdj == UNKNOWN_ADJ; + final boolean oomAdjChanged = (state.getCurAdj() >= mConstants.FREEZER_CUTOFF_ADJ + ^ oldOomAdj >= mConstants.FREEZER_CUTOFF_ADJ) || oldOomAdj == UNKNOWN_ADJ; final boolean shouldNotFreezeChanged = opt.shouldNotFreezeAdjSeq() == mAdjSeq; final boolean hasCpuCapability = (PROCESS_CAPABILITY_CPU_TIME & app.mState.getCurCapability()) diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 70febcd63455..bddde9d589f3 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -383,12 +383,6 @@ public final class ProcessList { private static final long LMKD_RECONNECT_DELAY_MS = 1000; /** - * The cuttoff adj for the freezer, app processes with adj greater than this value will be - * eligible for the freezer. - */ - static final int FREEZER_CUTOFF_ADJ = CACHED_APP_MIN_ADJ; - - /** * Apps have no access to the private data directories of any other app, even if the other * app has made them world-readable. */ diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index 98f738c38d63..49149e1fa415 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -1699,7 +1699,7 @@ class ProcessRecord implements WindowProcessListener { return mService.mOomAdjuster.mCachedAppOptimizer.useFreezer() && !mOptRecord.isFreezeExempt() && !mOptRecord.shouldNotFreeze() - && mState.getCurAdj() >= ProcessList.FREEZER_CUTOFF_ADJ; + && mState.getCurAdj() >= mService.mConstants.FREEZER_CUTOFF_ADJ; } public void forEachConnectionHost(Consumer<ProcessRecord> consumer) { diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 5740e16dc886..737820b4a788 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -119,6 +119,7 @@ import android.os.Handler; import android.os.HandlerExecutor; import android.os.IBinder; import android.os.IBinder.DeathRecipient; +import android.os.IBinder.FrozenStateChangeCallback; import android.os.IThermalService; import android.os.Looper; import android.os.Message; @@ -272,6 +273,7 @@ public final class DisplayManagerService extends SystemService { private static final int MSG_DELIVER_DISPLAY_EVENT_FRAME_RATE_OVERRIDE = 7; private static final int MSG_DELIVER_DISPLAY_GROUP_EVENT = 8; private static final int MSG_RECEIVED_DEVICE_STATE = 9; + private static final int MSG_DISPATCH_PENDING_PROCESS_EVENTS = 10; private static final int[] EMPTY_ARRAY = new int[0]; private static final HdrConversionMode HDR_CONVERSION_MODE_UNSUPPORTED = new HdrConversionMode( HDR_CONVERSION_UNSUPPORTED); @@ -286,7 +288,6 @@ public final class DisplayManagerService extends SystemService { private InputManagerInternal mInputManagerInternal; private ActivityManagerInternal mActivityManagerInternal; private final UidImportanceListener mUidImportanceListener = new UidImportanceListener(); - private final DisplayFrozenProcessListener mDisplayFrozenProcessListener; @Nullable private IMediaProjectionManager mProjectionService; @@ -630,7 +631,6 @@ public final class DisplayManagerService extends SystemService { mFlags = injector.getFlags(); mHandler = new DisplayManagerHandler(displayThreadLooper); mHandlerExecutor = new HandlerExecutor(mHandler); - mDisplayFrozenProcessListener = new DisplayFrozenProcessListener(); mUiHandler = UiThread.getHandler(); mDisplayDeviceRepo = new DisplayDeviceRepository(mSyncRoot, mPersistentDataStore); mLogicalDisplayMapper = new LogicalDisplayMapper(mContext, @@ -1165,31 +1165,11 @@ public final class DisplayManagerService extends SystemService { } } - private class DisplayFrozenProcessListener - implements ActivityManagerInternal.FrozenProcessListener { - public void onProcessFrozen(int pid) { - synchronized (mSyncRoot) { - CallbackRecord callback = mCallbacks.get(pid); - if (callback == null) { - return; - } - callback.setFrozen(true); - } - } - - public void onProcessUnfrozen(int pid) { - // First, see if there is a callback associated with this pid. If there's no - // callback, then there is nothing to do. - CallbackRecord callback; - synchronized (mSyncRoot) { - callback = mCallbacks.get(pid); - if (callback == null) { - return; - } - callback.setFrozen(false); - } - // Attempt to dispatch pending events if the process is coming out of frozen. + private void dispatchPendingProcessEvents(@NonNull Object cb) { + if (cb instanceof CallbackRecord callback) { callback.dispatchPending(); + } else { + Slog.wtf(TAG, "not a callback: " + cb); } } @@ -4052,6 +4032,9 @@ public final class DisplayManagerService extends SystemService { deliverDisplayGroupEvent(msg.arg1, msg.arg2); break; + case MSG_DISPATCH_PENDING_PROCESS_EVENTS: + dispatchPendingProcessEvents(msg.obj); + break; } } } @@ -4119,7 +4102,7 @@ public final class DisplayManagerService extends SystemService { } } - private final class CallbackRecord implements DeathRecipient { + private final class CallbackRecord implements DeathRecipient, FrozenStateChangeCallback { public final int mPid; public final int mUid; private final IDisplayManagerCallback mCallback; @@ -4142,6 +4125,8 @@ public final class DisplayManagerService extends SystemService { private boolean mCached; @GuardedBy("mCallback") private boolean mFrozen; + @GuardedBy("mCallback") + private boolean mAlive; CallbackRecord(int pid, int uid, @NonNull IDisplayManagerCallback callback, @InternalEventFlag long internalEventFlagsMask) { @@ -4151,18 +4136,20 @@ public final class DisplayManagerService extends SystemService { mInternalEventFlagsMask = new AtomicLong(internalEventFlagsMask); mCached = false; mFrozen = false; + mAlive = true; if (deferDisplayEventsWhenFrozen()) { - // Some CallbackRecords are registered very early in system boot, before - // mActivityManagerInternal is initialized. If mActivityManagerInternal is null, - // do not register the frozen process listener. However, do verify that all such - // registrations are for the self pid (which can never be frozen, so the frozen - // process listener does not matter). - if (mActivityManagerInternal != null) { - mActivityManagerInternal.addFrozenProcessListener(pid, mHandlerExecutor, - mDisplayFrozenProcessListener); - } else if (Process.myPid() != pid) { - Slog.e(TAG, "DisplayListener registered too early"); + try { + callback.asBinder().addFrozenStateChangeCallback(this); + } catch (UnsupportedOperationException e) { + // Ignore the exception. The callback is not supported on this platform or on + // this binder. The callback is never supported for local binders. There is + // no error: the UID importance listener will still operate. A log message is + // provided for debug. + Slog.v(TAG, "FrozenStateChangeCallback not supported for pid " + mPid); + } catch (RemoteException e) { + // This is unexpected. Just give up. + throw new RuntimeException(e); } } @@ -4187,7 +4174,7 @@ public final class DisplayManagerService extends SystemService { */ @GuardedBy("mCallback") private boolean hasPendingAndIsReadyLocked() { - return isReadyLocked() && mPendingEvents != null && !mPendingEvents.isEmpty(); + return isReadyLocked() && mPendingEvents != null && !mPendingEvents.isEmpty() && mAlive; } /** @@ -4195,7 +4182,7 @@ public final class DisplayManagerService extends SystemService { * receive events and there are pending events to be delivered. * This is only used if {@link deferDisplayEventsWhenFrozen()} is true. */ - public boolean setFrozen(boolean frozen) { + private boolean setFrozen(boolean frozen) { synchronized (mCallback) { mFrozen = frozen; return hasPendingAndIsReadyLocked(); @@ -4216,6 +4203,9 @@ public final class DisplayManagerService extends SystemService { @Override public void binderDied() { + synchronized (mCallback) { + mAlive = false; + } if (DEBUG || extraLogging(mPackageName)) { Slog.d(TAG, "Display listener for pid " + mPid + " died."); } @@ -4226,6 +4216,14 @@ public final class DisplayManagerService extends SystemService { onCallbackDied(this); } + @Override + public void onFrozenStateChanged(@NonNull IBinder who, int state) { + if (setFrozen(state == FrozenStateChangeCallback.STATE_FROZEN)) { + Message msg = mHandler.obtainMessage(MSG_DISPATCH_PENDING_PROCESS_EVENTS, this); + mHandler.sendMessage(msg); + } + } + /** * @return {@code false} if RemoteException happens; otherwise {@code true} for * success. This returns true even if the event was deferred because the remote client is @@ -4392,7 +4390,7 @@ public final class DisplayManagerService extends SystemService { // This is only used if {@link deferDisplayEventsWhenFrozen()} is true. public boolean dispatchPending() { synchronized (mCallback) { - if (mPendingEvents == null || mPendingEvents.isEmpty()) { + if (mPendingEvents == null || mPendingEvents.isEmpty() || !mAlive) { return true; } if (!isReadyLocked()) { @@ -6064,6 +6062,7 @@ public final class DisplayManagerService extends SystemService { * Return the value of the pause */ private static boolean deferDisplayEventsWhenFrozen() { - return com.android.server.am.Flags.deferDisplayEventsWhenFrozen(); + return android.os.Flags.binderFrozenStateChangeCallback() + && com.android.server.am.Flags.deferDisplayEventsWhenFrozen(); } } diff --git a/services/core/java/com/android/server/location/provider/proxy/ProxyGnssAssistanceProvider.java b/services/core/java/com/android/server/location/provider/proxy/ProxyGnssAssistanceProvider.java new file mode 100644 index 000000000000..6cab60c05b8e --- /dev/null +++ b/services/core/java/com/android/server/location/provider/proxy/ProxyGnssAssistanceProvider.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.location.provider.proxy; + +import android.annotation.Nullable; +import android.content.Context; +import android.location.provider.GnssAssistanceProviderBase; +import android.location.provider.IGnssAssistanceCallback; +import android.location.provider.IGnssAssistanceProvider; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.android.server.servicewatcher.CurrentUserServiceSupplier; +import com.android.server.servicewatcher.ServiceWatcher; + +/** + * Proxy for IGnssAssitanceProvider implementations. + */ +public class ProxyGnssAssistanceProvider { + + private static final String TAG = "GnssAssistanceProxy"; + /** + * Creates and registers this proxy. If no suitable service is available for the proxy, returns + * null. + */ + @Nullable + public static ProxyGnssAssistanceProvider createAndRegister(Context context) { + ProxyGnssAssistanceProvider proxy = new ProxyGnssAssistanceProvider(context); + if (proxy.register()) { + return proxy; + } else { + return null; + } + } + + private final ServiceWatcher mServiceWatcher; + + private ProxyGnssAssistanceProvider(Context context) { + mServiceWatcher = + ServiceWatcher.create( + context, + TAG, + CurrentUserServiceSupplier.createFromConfig( + context, + GnssAssistanceProviderBase.ACTION_GNSS_ASSISTANCE_PROVIDER, + com.android.internal.R.bool.config_enableGnssAssistanceOverlay, + com.android.internal.R.string + .config_gnssAssistanceProviderPackageName), + /* serviceListener= */ null); + } + + private boolean register() { + boolean resolves = mServiceWatcher.checkServiceResolves(); + if (resolves) { + mServiceWatcher.register(); + } + return resolves; + } + + /** + * Request GNSS assistance. + */ + public void request(IGnssAssistanceCallback callback) { + mServiceWatcher.runOnBinder( + new ServiceWatcher.BinderOperation() { + @Override + public void run(IBinder binder) throws RemoteException { + IGnssAssistanceProvider.Stub.asInterface(binder).request(callback); + } + + @Override + public void onError(Throwable t) { + try { + Log.w(TAG, "Error on requesting GnssAssistance: " + t); + callback.onError(); + } catch (RemoteException e) { + // ignore + } + } + }); + } +} diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java index 271c818f7daa..7f88e7463208 100644 --- a/services/core/java/com/android/server/power/Notifier.java +++ b/services/core/java/com/android/server/power/Notifier.java @@ -479,6 +479,7 @@ public class Notifier { case PowerManager.PARTIAL_WAKE_LOCK: return BatteryStats.WAKE_TYPE_PARTIAL; + case PowerManager.FULL_WAKE_LOCK: case PowerManager.SCREEN_DIM_WAKE_LOCK: case PowerManager.SCREEN_BRIGHT_WAKE_LOCK: return BatteryStats.WAKE_TYPE_FULL; diff --git a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java index 19eba5fe5755..90c2216f6b22 100644 --- a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java +++ b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java @@ -51,8 +51,11 @@ import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; import com.android.server.wm.utils.InsetUtils; +import com.android.window.flags.Flags; import java.io.PrintWriter; +import java.util.function.Consumer; +import java.util.function.Supplier; /** * Base class for a Snapshot controller @@ -148,43 +151,60 @@ abstract class AbsAppSnapshotController<TYPE extends WindowContainer, protected abstract Rect getLetterboxInsets(ActivityRecord topActivity); /** - * This is different than {@link #recordSnapshotInner(TYPE)} because it doesn't store - * the snapshot to the cache and returns the TaskSnapshot immediately. - * - * This is only used for testing so the snapshot content can be verified. + * This is different than {@link #recordSnapshotInner(TYPE, boolean, Consumer)} because it + * doesn't store the snapshot to the cache and returns the TaskSnapshot immediately. */ @VisibleForTesting - TaskSnapshot captureSnapshot(TYPE source) { - final TaskSnapshot snapshot; + SnapshotSupplier captureSnapshot(TYPE source, boolean allowAppTheme) { + final SnapshotSupplier supplier = new SnapshotSupplier(); switch (getSnapshotMode(source)) { - case SNAPSHOT_MODE_NONE: - return null; case SNAPSHOT_MODE_APP_THEME: - snapshot = drawAppThemeSnapshot(source); + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "drawAppThemeSnapshot"); + if (Flags.excludeDrawingAppThemeSnapshotFromLock()) { + if (allowAppTheme) { + supplier.setSupplier(drawAppThemeSnapshot(source)); + } + } else { + final Supplier<TaskSnapshot> original = drawAppThemeSnapshot(source); + final TaskSnapshot snapshot = original != null ? original.get() : null; + supplier.setSnapshot(snapshot); + } + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); break; case SNAPSHOT_MODE_REAL: - snapshot = snapshot(source); + supplier.setSnapshot(snapshot(source)); break; default: - snapshot = null; break; } - return snapshot; + return supplier; } - final TaskSnapshot recordSnapshotInner(TYPE source) { + /** + * @param allowAppTheme If true, allows to draw app theme snapshot when it's not allowed to take + * a real screenshot, but create a fake representation of the app. + * @param inLockConsumer Extra task to do in WM lock when first get the snapshot object. + */ + final SnapshotSupplier recordSnapshotInner(TYPE source, boolean allowAppTheme, + @Nullable Consumer<TaskSnapshot> inLockConsumer) { if (shouldDisableSnapshots()) { return null; } - final TaskSnapshot snapshot = captureSnapshot(source); - if (snapshot == null) { - return null; - } - mCache.putSnapshot(source, snapshot); - return snapshot; + final SnapshotSupplier supplier = captureSnapshot(source, allowAppTheme); + supplier.setConsumer(t -> { + synchronized (mService.mGlobalLock) { + if (!source.isAttached()) { + return; + } + mCache.putSnapshot(source, t); + if (inLockConsumer != null) { + inLockConsumer.accept(t); + } + } + }); + return supplier; } - @VisibleForTesting int getSnapshotMode(TYPE source) { final int type = source.getActivityType(); if (type == ACTIVITY_TYPE_RECENTS || type == ACTIVITY_TYPE_DREAM) { @@ -400,7 +420,7 @@ abstract class AbsAppSnapshotController<TYPE extends WindowContainer, * If we are not allowed to take a real screenshot, this attempts to represent the app as best * as possible by using the theme's window background. */ - private TaskSnapshot drawAppThemeSnapshot(TYPE source) { + private Supplier<TaskSnapshot> drawAppThemeSnapshot(TYPE source) { final ActivityRecord topActivity = getTopActivity(source); if (topActivity == null) { return null; @@ -432,26 +452,46 @@ abstract class AbsAppSnapshotController<TYPE extends WindowContainer, decorPainter.setInsets(systemBarInsets); decorPainter.drawDecors(c /* statusBarExcludeFrame */, null /* alreadyDrawFrame */); node.end(c); - final Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, width, height); - if (hwBitmap == null) { - return null; - } + final Rect contentInsets = new Rect(systemBarInsets); final Rect letterboxInsets = getLetterboxInsets(topActivity); InsetUtils.addInsets(contentInsets, letterboxInsets); - // Note, the app theme snapshot is never translucent because we enforce a non-translucent - // color above - final TaskSnapshot taskSnapshot = new TaskSnapshot( - System.currentTimeMillis() /* id */, - SystemClock.elapsedRealtimeNanos() /* captureTime */, - topActivity.mActivityComponent, hwBitmap.getHardwareBuffer(), - hwBitmap.getColorSpace(), mainWindow.getConfiguration().orientation, - mainWindow.getWindowConfiguration().getRotation(), new Point(taskWidth, taskHeight), - contentInsets, letterboxInsets, false /* isLowResolution */, - false /* isRealSnapshot */, source.getWindowingMode(), - attrs.insetsFlags.appearance, false /* isTranslucent */, false /* hasImeSurface */, - topActivity.getConfiguration().uiMode /* uiMode */); - return validateSnapshot(taskSnapshot); + + final TaskSnapshot.Builder builder = new TaskSnapshot.Builder(); + builder.setIsRealSnapshot(false); + builder.setId(System.currentTimeMillis()); + builder.setContentInsets(contentInsets); + builder.setLetterboxInsets(letterboxInsets); + + builder.setTopActivityComponent(topActivity.mActivityComponent); + // Note, the app theme snapshot is never translucent because we enforce a + // non-translucent color above. + builder.setIsTranslucent(false); + builder.setWindowingMode(source.getWindowingMode()); + builder.setAppearance(attrs.insetsFlags.appearance); + builder.setUiMode(topActivity.getConfiguration().uiMode); + + builder.setRotation(mainWindow.getWindowConfiguration().getRotation()); + builder.setOrientation(mainWindow.getConfiguration().orientation); + builder.setTaskSize(new Point(taskWidth, taskHeight)); + builder.setCaptureTime(SystemClock.elapsedRealtimeNanos()); + + return () -> { + try { + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "drawAppThemeSnapshot_acquire"); + // Do not hold WM lock when calling to render thread. + final Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, width, + height); + if (hwBitmap == null) { + return null; + } + builder.setSnapshot(hwBitmap.getHardwareBuffer()); + builder.setColorSpace(hwBitmap.getColorSpace()); + return validateSnapshot(builder.build()); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + }; } static Rect getSystemBarInsets(Rect frame, InsetsState state) { @@ -482,4 +522,45 @@ abstract class AbsAppSnapshotController<TYPE extends WindowContainer, pw.println(prefix + "mSnapshotEnabled=" + mSnapshotEnabled); mCache.dump(pw, prefix); } + + static class SnapshotSupplier implements Supplier<TaskSnapshot> { + + private TaskSnapshot mSnapshot; + private boolean mHasSet; + private Consumer<TaskSnapshot> mConsumer; + private Supplier<TaskSnapshot> mSupplier; + + /** Callback when the snapshot is get for the first time. */ + void setConsumer(@NonNull Consumer<TaskSnapshot> consumer) { + mConsumer = consumer; + } + + void setSupplier(@NonNull Supplier<TaskSnapshot> createSupplier) { + mSupplier = createSupplier; + } + + void setSnapshot(TaskSnapshot snapshot) { + mSnapshot = snapshot; + } + + void handleSnapshot() { + if (mHasSet) { + return; + } + mHasSet = true; + if (mSnapshot == null) { + mSnapshot = mSupplier != null ? mSupplier.get() : null; + } + if (mConsumer != null && mSnapshot != null) { + mConsumer.accept(mSnapshot); + } + } + + @Override + @Nullable + public TaskSnapshot get() { + handleSnapshot(); + return mSnapshot; + } + } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 83b273c04648..a01fa48f2af2 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -10355,7 +10355,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } if (!isVisibleRequested()) { // TODO(b/294925498): Remove this finishing check once we have accurate ready tracking. - if (task != null && task.getPausingActivity() == this) { + if (task != null && task.getPausingActivity() == this + // Display is asleep, so nothing will be visible anyways. + && !mDisplayContent.isSleeping()) { // Visibility of starting activities isn't calculated until pause-complete, so if // this is not paused yet, don't consider it ready. return false; diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java index 9aaa0e1cfd6b..cfd324830db5 100644 --- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java +++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java @@ -38,6 +38,7 @@ import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider; import java.io.File; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.function.Supplier; /** * When an app token becomes invisible, we take a snapshot (bitmap) and put it into our cache. @@ -355,7 +356,9 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord final int[] mixedCode = new int[size]; if (size == 1) { final ActivityRecord singleActivity = activity.get(0); - final TaskSnapshot snapshot = recordSnapshotInner(singleActivity); + final Supplier<TaskSnapshot> supplier = recordSnapshotInner(singleActivity, + false /* allowAppTheme */, null /* inLockConsumer */); + final TaskSnapshot snapshot = supplier != null ? supplier.get() : null; if (snapshot != null) { mixedCode[0] = getSystemHashCode(singleActivity); addUserSavedFile(singleActivity.mUserId, snapshot, mixedCode); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 5eee8ece6a67..290f155bb4cd 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -314,6 +314,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Supplier; /** * System service for managing activities and their containers (task, displays,... ). @@ -4038,6 +4039,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mAmInternal.enforceCallingPermission(READ_FRAME_BUFFER, "takeTaskSnapshot()"); final long ident = Binder.clearCallingIdentity(); try { + final Supplier<TaskSnapshot> supplier; synchronized (mGlobalLock) { final Task task = mRootWindowContainer.anyTaskForId(taskId, MATCH_ATTACHED_TASK_OR_RECENT_TASKS); @@ -4050,11 +4052,13 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { // be retrieved by recents. While if updateCache is false, the real snapshot will // always be taken and the snapshot won't be put into SnapshotPersister. if (updateCache) { - return mWindowManager.mTaskSnapshotController.recordSnapshot(task); + supplier = mWindowManager.mTaskSnapshotController + .getRecordSnapshotSupplier(task); } else { return mWindowManager.mTaskSnapshotController.snapshot(task); } } + return supplier != null ? supplier.get() : null; } finally { Binder.restoreCallingIdentity(ident); } @@ -6403,6 +6407,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { @Override public boolean shuttingDown(boolean booted, int timeout) { mShuttingDown = true; + mWindowManager.mSnapshotController.mTaskSnapshotController.prepareShutdown(); synchronized (mGlobalLock) { mRootWindowContainer.prepareForShutdown(); updateEventDispatchingLocked(booted); diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java b/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java index caff96ba4a9f..4fac81b06680 100644 --- a/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java @@ -35,8 +35,6 @@ import android.annotation.NonNull; import android.content.res.Configuration; import android.graphics.Rect; -import com.android.window.flags.Flags; - /** * Encapsulate overrides and configurations about app compat reachability. */ @@ -157,33 +155,27 @@ class AppCompatReachabilityOverrides { } /** - * @return {@value true} if the vertical reachability should be allowed in case of + * @return {@code true} if the vertical reachability should be allowed in case of * thin letterboxing. */ boolean allowVerticalReachabilityForThinLetterbox() { - if (!Flags.disableThinLetterboxingPolicy()) { - return true; - } // When the flag is enabled we allow vertical reachability only if the // app is not thin letterboxed vertically. return !isVerticalThinLetterboxed(); } /** - * @return {@value true} if the horizontal reachability should be enabled in case of + * @return {@code true} if the horizontal reachability should be enabled in case of * thin letterboxing. */ boolean allowHorizontalReachabilityForThinLetterbox() { - if (!Flags.disableThinLetterboxingPolicy()) { - return true; - } // When the flag is enabled we allow horizontal reachability only if the // app is not thin pillarboxed. return !isHorizontalThinLetterboxed(); } /** - * @return {@value true} if the resulting app is letterboxed in a way defined as thin. + * @return {@code true} if the resulting app is letterboxed in a way defined as thin. */ boolean isVerticalThinLetterboxed() { final int thinHeight = mAppCompatConfiguration.getThinLetterboxHeightPx(); @@ -200,7 +192,7 @@ class AppCompatReachabilityOverrides { } /** - * @return {@value true} if the resulting app is pillarboxed in a way defined as thin. + * @return {@code true} if the resulting app is pillarboxed in a way defined as thin. */ boolean isHorizontalThinLetterboxed() { final int thinWidth = mAppCompatConfiguration.getThinLetterboxWidthPx(); diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 1a7c6b70f007..e2499bc0f3ee 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -997,11 +997,9 @@ class BackNavigationController { /** * Handle the pending animation when the running transition finished, all the visibility change * has applied so ready to start pending predictive back animation. - * @param targets The final animation targets derived in transition. * @param finishedTransition The finished transition target. */ - void onTransitionFinish(ArrayList<Transition.ChangeInfo> targets, - @NonNull Transition finishedTransition) { + void onTransitionFinish(@NonNull Transition finishedTransition) { if (isMonitoringPrepareTransition(finishedTransition)) { if (mAnimationHandler.mPrepareCloseTransition == null) { clearBackAnimations(true /* cancel */); @@ -1049,14 +1047,6 @@ class BackNavigationController { return; } - // Ensure the final animation targets which hidden by transition could be visible. - for (int i = 0; i < targets.size(); i++) { - final WindowContainer wc = targets.get(i).mContainer; - if (wc.mSurfaceControl != null) { - wc.prepareSurfaces(); - } - } - // The pending builder could be cleared due to prepareSurfaces // => updateNonSystemOverlayWindowsVisibilityIfNeeded // => setForceHideNonSystemOverlayWindowIfNeeded diff --git a/services/core/java/com/android/server/wm/CameraStateMonitor.java b/services/core/java/com/android/server/wm/CameraStateMonitor.java index 3aa355869d85..00279921953d 100644 --- a/services/core/java/com/android/server/wm/CameraStateMonitor.java +++ b/services/core/java/com/android/server/wm/CameraStateMonitor.java @@ -110,8 +110,10 @@ class CameraStateMonitor { } void startListeningToCameraState() { - mCameraManager.registerAvailabilityCallback( - mWmService.mContext.getMainExecutor(), mAvailabilityCallback); + if (mCameraManager != null) { + mCameraManager.registerAvailabilityCallback( + mWmService.mContext.getMainExecutor(), mAvailabilityCallback); + } mIsRunning = true; } diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java index 258a87eae196..3c60d8296577 100644 --- a/services/core/java/com/android/server/wm/DragDropController.java +++ b/services/core/java/com/android/server/wm/DragDropController.java @@ -289,7 +289,8 @@ class DragDropController { transaction.setAlpha(surfaceControl, mDragState.mOriginalAlpha); transaction.show(surfaceControl); displayContent.reparentToOverlay(transaction, surfaceControl); - mDragState.updateDragSurfaceLocked(true, touchX, touchY); + mDragState.updateDragSurfaceLocked(true /* keepHandling */, + displayContent.getDisplayId(), touchX, touchY); if (SHOW_LIGHT_TRANSACTIONS) { Slog.i(TAG_WM, "<<< CLOSE TRANSACTION performDrag"); } @@ -483,10 +484,11 @@ class DragDropController { * Handles motion events. * @param keepHandling Whether if the drag operation is continuing or this is the last motion * event. + * @param displayId id of the display the X,Y coordinate is n. * @param newX X coordinate value in dp in the screen coordinate * @param newY Y coordinate value in dp in the screen coordinate */ - void handleMotionEvent(boolean keepHandling, float newX, float newY) { + void handleMotionEvent(boolean keepHandling, int displayId, float newX, float newY) { synchronized (mService.mGlobalLock) { if (!dragDropActiveLocked()) { // The drag has ended but the clean-up message has not been processed by @@ -495,7 +497,7 @@ class DragDropController { return; } - mDragState.updateDragSurfaceLocked(keepHandling, newX, newY); + mDragState.updateDragSurfaceLocked(keepHandling, displayId, newX, newY); } } diff --git a/services/core/java/com/android/server/wm/DragInputEventReceiver.java b/services/core/java/com/android/server/wm/DragInputEventReceiver.java index 5372d8b6e796..8f4548fa4fcb 100644 --- a/services/core/java/com/android/server/wm/DragInputEventReceiver.java +++ b/services/core/java/com/android/server/wm/DragInputEventReceiver.java @@ -22,13 +22,13 @@ import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_UP; import static android.view.MotionEvent.BUTTON_STYLUS_PRIMARY; + import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DRAG; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.os.Looper; import android.util.Slog; import android.view.InputChannel; -import android.view.InputDevice; import android.view.InputEvent; import android.view.InputEventReceiver; import android.view.MotionEvent; @@ -63,6 +63,7 @@ class DragInputEventReceiver extends InputEventReceiver { return; } final MotionEvent motionEvent = (MotionEvent) event; + final int displayId = motionEvent.getDisplayId(); final float newX = motionEvent.getRawX(); final float newY = motionEvent.getRawY(); final boolean isStylusButtonDown = @@ -102,7 +103,8 @@ class DragInputEventReceiver extends InputEventReceiver { return; } - mDragDropController.handleMotionEvent(!mMuteInput /* keepHandling */, newX, newY); + mDragDropController.handleMotionEvent(!mMuteInput /* keepHandling */, displayId, newX, + newY); handled = true; } catch (Exception e) { Slog.e(TAG_WM, "Exception caught by drag handleMotion", e); diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java index 1c4e487d2e7e..3a0e41a5f9f8 100644 --- a/services/core/java/com/android/server/wm/DragState.java +++ b/services/core/java/com/android/server/wm/DragState.java @@ -113,8 +113,8 @@ class DragState { boolean mRelinquishDragSurfaceToDropTarget; float mAnimatedScale = 1.0f; float mOriginalAlpha; - float mOriginalX, mOriginalY; - float mCurrentX, mCurrentY; + float mOriginalDisplayX, mOriginalDisplayY; + float mCurrentDisplayX, mCurrentDisplayY; float mThumbOffsetX, mThumbOffsetY; InputInterceptor mInputInterceptor; ArrayList<WindowState> mNotifiedWindows; @@ -230,22 +230,22 @@ class DragState { if (mDragInProgress) { if (DEBUG_DRAG) Slog.d(TAG_WM, "Broadcasting DRAG_ENDED"); for (WindowState ws : mNotifiedWindows) { - float x = 0; - float y = 0; + float inWindowX = 0; + float inWindowY = 0; SurfaceControl dragSurface = null; if (!mDragResult && (ws.mSession.mPid == mPid)) { // Report unconsumed drop location back to the app that started the drag. - x = ws.translateToWindowX(mCurrentX); - y = ws.translateToWindowY(mCurrentY); + inWindowX = ws.translateToWindowX(mCurrentDisplayX); + inWindowY = ws.translateToWindowY(mCurrentDisplayY); if (relinquishDragSurfaceToDragSource()) { // If requested (and allowed), report the drag surface back to the app // starting the drag to handle the return animation dragSurface = mSurfaceControl; } } - DragEvent event = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED, x, y, - mThumbOffsetX, mThumbOffsetY, mFlags, null, null, null, dragSurface, null, - mDragResult); + DragEvent event = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED, inWindowX, + inWindowY, mThumbOffsetX, mThumbOffsetY, mFlags, null, null, null, + dragSurface, null, mDragResult); try { if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DRAG_ENDED to " + ws); ws.mClient.dispatchDragEvent(event); @@ -297,70 +297,71 @@ class DragState { } /** - * Creates the drop event for this drag gesture. If `touchedWin` is null, then the drop event - * will be created for dispatching to the unhandled drag and the drag surface will be provided - * as a part of the dispatched event. + * Creates the drop event for dispatching to the unhandled drag. + * TODO(b/384841906): Update `inWindowX` and `inWindowY` to be display-coordinate. */ - private DragEvent createDropEvent(float x, float y, @Nullable WindowState touchedWin, - boolean includePrivateInfo) { - if (touchedWin != null) { - final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid()); - final DragAndDropPermissionsHandler dragAndDropPermissions; - if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0 - && mData != null) { - dragAndDropPermissions = new DragAndDropPermissionsHandler(mService.mGlobalLock, - mData, - mUid, - touchedWin.getOwningPackage(), - mFlags & DRAG_FLAGS_URI_PERMISSIONS, - mSourceUserId, - targetUserId); - } else { - dragAndDropPermissions = null; - } - if (mSourceUserId != targetUserId) { - if (mData != null) { - mData.fixUris(mSourceUserId); - } - } - final boolean targetInterceptsGlobalDrag = targetInterceptsGlobalDrag(touchedWin); - return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mDataDescription, mData, - /* includeDragSurface= */ targetInterceptsGlobalDrag, - /* includeDragFlags= */ targetInterceptsGlobalDrag, - dragAndDropPermissions); + private DragEvent createUnhandledDropEvent(float inWindowX, float inWindowY) { + return obtainDragEvent(DragEvent.ACTION_DROP, inWindowX, inWindowY, mDataDescription, mData, + /* includeDragSurface= */ true, + /* includeDragFlags= */ true, null /* dragAndDropPermissions */); + } + + /** + * Creates the drop event for this drag gesture. + */ + private DragEvent createDropEvent(float inWindowX, float inWindowY, WindowState touchedWin) { + final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid()); + final DragAndDropPermissionsHandler dragAndDropPermissions; + if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0 + && mData != null) { + dragAndDropPermissions = new DragAndDropPermissionsHandler(mService.mGlobalLock, mData, + mUid, touchedWin.getOwningPackage(), mFlags & DRAG_FLAGS_URI_PERMISSIONS, + mSourceUserId, targetUserId); } else { - return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mDataDescription, mData, - /* includeDragSurface= */ includePrivateInfo, - /* includeDragFlags= */ includePrivateInfo, - null /* dragAndDropPermissions */); + dragAndDropPermissions = null; } + if (mSourceUserId != targetUserId) { + if (mData != null) { + mData.fixUris(mSourceUserId); + } + } + final boolean targetInterceptsGlobalDrag = targetInterceptsGlobalDrag(touchedWin); + return obtainDragEvent(DragEvent.ACTION_DROP, inWindowX, inWindowY, mDataDescription, mData, + /* includeDragSurface= */ targetInterceptsGlobalDrag, + /* includeDragFlags= */ targetInterceptsGlobalDrag, dragAndDropPermissions); } /** * Notify the drop target and tells it about the data. If the drop event is not sent to the * target, invokes {@code endDragLocked} after the unhandled drag listener gets a chance to * handle the drop. + * @param inWindowX if `token` refers to a dragEvent-accepting window, `inWindowX` will be + * inside the window, else values might be invalid (0, 0). + * @param inWindowY if `token` refers to a dragEvent-accepting window, `inWindowY` will be + * inside the window, else values might be invalid (0, 0). */ - boolean reportDropWindowLock(IBinder token, float x, float y) { + boolean reportDropWindowLock(IBinder token, float inWindowX, float inWindowY) { if (mAnimator != null) { return false; } try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "DragDropController#DROP"); - return reportDropWindowLockInner(token, x, y); + return reportDropWindowLockInner(token, inWindowX, inWindowY); } finally { Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } } - private boolean reportDropWindowLockInner(IBinder token, float x, float y) { + private boolean reportDropWindowLockInner(IBinder token, float inWindowX, float inWindowY) { if (mAnimator != null) { return false; } final WindowState touchedWin = mService.mInputToWindowMap.get(token); - final DragEvent unhandledDropEvent = createDropEvent(x, y, null /* touchedWin */, - true /* includePrivateInfo */); + // TODO(b/384841906): The x, y here when sent to a window and unhandled, will still be + // relative to the window it was originally sent to. Need to update this to actually be + // display-coordinate. + final DragEvent unhandledDropEvent = createUnhandledDropEvent(inWindowX, inWindowY); if (!isWindowNotified(touchedWin)) { // Delegate to the unhandled drag listener as a first pass if (mDragDropController.notifyUnhandledDrop(unhandledDropEvent, "unhandled-drop")) { @@ -381,7 +382,7 @@ class DragState { if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DROP to " + touchedWin); final IBinder clientToken = touchedWin.mClient.asBinder(); - final DragEvent event = createDropEvent(x, y, touchedWin, false /* includePrivateInfo */); + final DragEvent event = createDropEvent(inWindowX, inWindowY, touchedWin); try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "DragDropController#dispatchDrop"); touchedWin.mClient.dispatchDragEvent(event); @@ -486,8 +487,8 @@ class DragState { */ void broadcastDragStartedLocked(final float touchX, final float touchY) { Trace.instant(TRACE_TAG_WINDOW_MANAGER, "DragDropController#DRAG_STARTED"); - mOriginalX = mCurrentX = touchX; - mOriginalY = mCurrentY = touchY; + mOriginalDisplayX = mCurrentDisplayX = touchX; + mOriginalDisplayY = mCurrentDisplayY = touchY; // Cache a base-class instance of the clip metadata so that parceling // works correctly in calling out to the apps. @@ -636,7 +637,7 @@ class DragState { if (isWindowNotified(newWin)) { return; } - sendDragStartedLocked(newWin, mCurrentX, mCurrentY, + sendDragStartedLocked(newWin, mCurrentDisplayX, mCurrentDisplayY, containsApplicationExtras(mDataDescription)); } } @@ -685,12 +686,21 @@ class DragState { mAnimator = createCancelAnimationLocked(); } - void updateDragSurfaceLocked(boolean keepHandling, float x, float y) { + /** + * Updates the position of the drag surface. + * + * @param keepHandling whether to keep handling the drag. + * @param displayId the display ID of the drag surface. + * @param displayX the x-coordinate of the drag surface in the display's coordinate frame. + * @param displayY the y-coordinate of the drag surface in the display's coordinate frame. + */ + void updateDragSurfaceLocked(boolean keepHandling, int displayId, float displayX, + float displayY) { if (mAnimator != null) { return; } - mCurrentX = x; - mCurrentY = y; + mCurrentDisplayX = displayX; + mCurrentDisplayY = displayY; if (!keepHandling) { return; @@ -700,9 +710,10 @@ class DragState { if (SHOW_LIGHT_TRANSACTIONS) { Slog.i(TAG_WM, ">>> OPEN TRANSACTION notifyMoveLocked"); } - mTransaction.setPosition(mSurfaceControl, x - mThumbOffsetX, y - mThumbOffsetY).apply(); - ProtoLog.i(WM_SHOW_TRANSACTIONS, "DRAG %s: pos=(%d,%d)", mSurfaceControl, - (int) (x - mThumbOffsetX), (int) (y - mThumbOffsetY)); + mTransaction.setPosition(mSurfaceControl, displayX - mThumbOffsetX, + displayY - mThumbOffsetY).apply(); + ProtoLog.i(WM_SHOW_TRANSACTIONS, "DRAG %s: displayId=%d, pos=(%d,%d)", mSurfaceControl, + displayId, (int) (displayX - mThumbOffsetX), (int) (displayY - mThumbOffsetY)); } /** @@ -713,6 +724,12 @@ class DragState { return mDragInProgress; } + /** + * `x` and `y` here varies between local window coordinate, relative coordinate to another + * window and local display coordinate, all depending on the `action`. Please take a look + * at the callers to determine the type. + * TODO(b/384845022): Properly document the events sent based on the event type. + */ private DragEvent obtainDragEvent(int action, float x, float y, ClipDescription description, ClipData data, boolean includeDragSurface, boolean includeDragFlags, IDragAndDropPermissions dragAndDropPermissions) { @@ -728,34 +745,34 @@ class DragState { final long duration; if (mCallingTaskIdToHide != -1) { animator = ValueAnimator.ofPropertyValuesHolder( - PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_X, mCurrentX, mCurrentX), - PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_Y, mCurrentY, mCurrentY), + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_X, mCurrentDisplayX, + mCurrentDisplayX), + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_Y, mCurrentDisplayY, + mCurrentDisplayY), PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_SCALE, mAnimatedScale, mAnimatedScale), PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, 0f)); duration = MIN_ANIMATION_DURATION_MS; } else { animator = ValueAnimator.ofPropertyValuesHolder( - PropertyValuesHolder.ofFloat( - ANIMATED_PROPERTY_X, mCurrentX - mThumbOffsetX, - mOriginalX - mThumbOffsetX), - PropertyValuesHolder.ofFloat( - ANIMATED_PROPERTY_Y, mCurrentY - mThumbOffsetY, - mOriginalY - mThumbOffsetY), + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_X, + mCurrentDisplayX - mThumbOffsetX, mOriginalDisplayX - mThumbOffsetX), + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_Y, + mCurrentDisplayY - mThumbOffsetY, mOriginalDisplayY - mThumbOffsetY), PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_SCALE, mAnimatedScale, mAnimatedScale), - PropertyValuesHolder.ofFloat( - ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, mOriginalAlpha / 2)); + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, + mOriginalAlpha / 2)); - final float translateX = mOriginalX - mCurrentX; - final float translateY = mOriginalY - mCurrentY; + final float translateX = mOriginalDisplayX - mCurrentDisplayX; + final float translateY = mOriginalDisplayY - mCurrentDisplayY; // Adjust the duration to the travel distance. final double travelDistance = Math.sqrt( translateX * translateX + translateY * translateY); - final double displayDiagonal = - Math.sqrt(mDisplaySize.x * mDisplaySize.x + mDisplaySize.y * mDisplaySize.y); - duration = MIN_ANIMATION_DURATION_MS + (long) (travelDistance / displayDiagonal - * (MAX_ANIMATION_DURATION_MS - MIN_ANIMATION_DURATION_MS)); + final double displayDiagonal = Math.sqrt( + mDisplaySize.x * mDisplaySize.x + mDisplaySize.y * mDisplaySize.y); + duration = MIN_ANIMATION_DURATION_MS + (long) (travelDistance / displayDiagonal * ( + MAX_ANIMATION_DURATION_MS - MIN_ANIMATION_DURATION_MS)); } final AnimationListener listener = new AnimationListener(); @@ -771,18 +788,20 @@ class DragState { private ValueAnimator createCancelAnimationLocked() { final ValueAnimator animator; if (mCallingTaskIdToHide != -1) { - animator = ValueAnimator.ofPropertyValuesHolder( - PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_X, mCurrentX, mCurrentX), - PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_Y, mCurrentY, mCurrentY), + animator = ValueAnimator.ofPropertyValuesHolder( + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_X, mCurrentDisplayX, + mCurrentDisplayX), + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_Y, mCurrentDisplayY, + mCurrentDisplayY), PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_SCALE, mAnimatedScale, mAnimatedScale), PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, 0f)); } else { animator = ValueAnimator.ofPropertyValuesHolder( - PropertyValuesHolder.ofFloat( - ANIMATED_PROPERTY_X, mCurrentX - mThumbOffsetX, mCurrentX), - PropertyValuesHolder.ofFloat( - ANIMATED_PROPERTY_Y, mCurrentY - mThumbOffsetY, mCurrentY), + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_X, + mCurrentDisplayX - mThumbOffsetX, mCurrentDisplayX), + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_Y, + mCurrentDisplayY - mThumbOffsetY, mCurrentDisplayY), PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_SCALE, mAnimatedScale, 0), PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, 0)); } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 3d2868540334..865d5facc4a4 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2926,7 +2926,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } void prepareForShutdown() { - mWindowManager.mSnapshotController.mTaskSnapshotController.prepareShutdown(); for (int i = 0; i < getChildCount(); i++) { createSleepToken("shutdown", getChildAt(i).mDisplayId); } diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java index 38e011509885..efc68aac0323 100644 --- a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java +++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java @@ -95,8 +95,9 @@ public class ScreenRecordingCallbackController { if (mediaProjectionInfo.getLaunchCookie() == null) { mRecordedWC = (WindowContainer) mWms.mRoot.getDefaultDisplay(); } else { - mRecordedWC = mWms.mRoot.getActivity(activity -> activity.mLaunchCookie - == mediaProjectionInfo.getLaunchCookie().binder).getTask(); + final ActivityRecord matchingActivity = mWms.mRoot.getActivity(activity -> + activity.mLaunchCookie == mediaProjectionInfo.getLaunchCookie().binder); + mRecordedWC = matchingActivity != null ? matchingActivity.getTask() : null; } } diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java index 38a2ebeba332..7d300e98f44b 100644 --- a/services/core/java/com/android/server/wm/TaskSnapshotController.java +++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java @@ -36,7 +36,9 @@ import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; import com.android.server.policy.WindowManagerPolicy.ScreenOffListener; import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider; +import com.android.window.flags.Flags; +import java.util.ArrayList; import java.util.Set; /** @@ -154,6 +156,8 @@ class TaskSnapshotController extends AbsAppSnapshotController<Task, TaskSnapshot * The attributes of task snapshot are based on task configuration. But sometimes the * configuration may have been changed during a transition, so supply the ChangeInfo that * stored the previous appearance of the closing task. + * + * The snapshot won't be created immediately if it should be captured as fake snapshot. */ void recordSnapshot(Task task, Transition.ChangeInfo changeInfo) { mCurrentChangeInfo = changeInfo; @@ -164,13 +168,35 @@ class TaskSnapshotController extends AbsAppSnapshotController<Task, TaskSnapshot } } - TaskSnapshot recordSnapshot(Task task) { - final TaskSnapshot snapshot = recordSnapshotInner(task); - if (snapshot != null && !task.isActivityTypeHome()) { - mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot); - task.onSnapshotChanged(snapshot); + void recordSnapshot(Task task) { + if (shouldDisableSnapshots()) { + return; + } + final SnapshotSupplier supplier = getRecordSnapshotSupplier(task); + if (supplier == null) { + return; } - return snapshot; + final int mode = getSnapshotMode(task); + if (Flags.excludeDrawingAppThemeSnapshotFromLock() && mode == SNAPSHOT_MODE_APP_THEME) { + mService.mH.post(supplier::handleSnapshot); + } else { + supplier.handleSnapshot(); + } + } + + /** + * Note that the snapshot is not created immediately, if the returned supplier is non-null, the + * caller must call {@link AbsAppSnapshotController.SnapshotSupplier#get} or + * {@link AbsAppSnapshotController.SnapshotSupplier#handleSnapshot} to complete the entire + * record request. + */ + SnapshotSupplier getRecordSnapshotSupplier(Task task) { + return recordSnapshotInner(task, true /* allowAppTheme */, snapshot -> { + if (!task.isActivityTypeHome()) { + mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot); + task.onSnapshotChanged(snapshot); + } + }); } /** @@ -328,27 +354,38 @@ class TaskSnapshotController extends AbsAppSnapshotController<Task, TaskSnapshot * Record task snapshots before shutdown. */ void prepareShutdown() { - if (!com.android.window.flags.Flags.recordTaskSnapshotsBeforeShutdown()) { + if (!Flags.recordTaskSnapshotsBeforeShutdown()) { return; } - // Make write items run in a batch. - mPersister.mSnapshotPersistQueue.setPaused(true); - mPersister.mSnapshotPersistQueue.prepareShutdown(); - for (int i = 0; i < mService.mRoot.getChildCount(); i++) { - mService.mRoot.getChildAt(i).forAllLeafTasks(task -> { - if (task.isVisible() && !task.isActivityTypeHome()) { - final TaskSnapshot snapshot = captureSnapshot(task); - if (snapshot != null) { - mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot); + final ArrayList<SnapshotSupplier> supplierArrayList = new ArrayList<>(); + synchronized (mService.mGlobalLock) { + // Make write items run in a batch. + mPersister.mSnapshotPersistQueue.setPaused(true); + mPersister.mSnapshotPersistQueue.prepareShutdown(); + for (int i = 0; i < mService.mRoot.getChildCount(); i++) { + mService.mRoot.getChildAt(i).forAllLeafTasks(task -> { + if (task.isVisible() && !task.isActivityTypeHome()) { + final SnapshotSupplier supplier = captureSnapshot(task, + true /* allowAppTheme */); + if (supplier != null) { + supplier.setConsumer(t -> + mPersister.persistSnapshot(task.mTaskId, task.mUserId, t)); + supplierArrayList.add(supplier); + } } - } - }, true /* traverseTopToBottom */); + }, true /* traverseTopToBottom */); + } + } + for (int i = supplierArrayList.size() - 1; i >= 0; --i) { + supplierArrayList.get(i).handleSnapshot(); + } + synchronized (mService.mGlobalLock) { + mPersister.mSnapshotPersistQueue.setPaused(false); } - mPersister.mSnapshotPersistQueue.setPaused(false); } void waitFlush(long timeout) { - if (!com.android.window.flags.Flags.recordTaskSnapshotsBeforeShutdown()) { + if (!Flags.recordTaskSnapshotsBeforeShutdown()) { return; } mPersister.mSnapshotPersistQueue.waitFlush(timeout); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 1f539a129e7d..d08d6f22feef 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1589,7 +1589,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { cleanUpInternal(); // Handle back animation if it's already started. - mController.mAtm.mBackNavigationController.onTransitionFinish(mTargets, this); + mController.mAtm.mBackNavigationController.onTransitionFinish(this); mController.mFinishingTransition = null; mController.mSnapshotController.onTransitionFinish(mType, mTargets); // Resume snapshot persist thread after snapshot controller analysis this transition. diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index db62cebf7603..04650b9e0150 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -10084,14 +10084,16 @@ public class WindowManagerService extends IWindowManager.Stub TaskSnapshot taskSnapshot; final long token = Binder.clearCallingIdentity(); try { + final Supplier<TaskSnapshot> supplier; synchronized (mGlobalLock) { Task task = mRoot.anyTaskForId(taskId, MATCH_ATTACHED_TASK_OR_RECENT_TASKS); if (task == null) { throw new IllegalArgumentException( "Failed to find matching task for taskId=" + taskId); } - taskSnapshot = mTaskSnapshotController.captureSnapshot(task); + supplier = mTaskSnapshotController.captureSnapshot(task, true /* allowAppTheme */); } + taskSnapshot = supplier != null ? supplier.get() : null; } finally { Binder.restoreCallingIdentity(token); } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 60130d1f97be..f10b7b9a95a4 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -91,6 +91,7 @@ import android.server.ServerProtoEnums; import android.system.ErrnoException; import android.system.Os; import android.text.TextUtils; +import android.tracing.perfetto.InitArguments; import android.util.ArrayMap; import android.util.DisplayMetrics; import android.util.Dumpable; @@ -792,6 +793,12 @@ public final class SystemServer implements Dumpable { private void run() { TimingsTraceAndSlog t = new TimingsTraceAndSlog(); try { + if (android.tracing.Flags.systemServerLargePerfettoShmemBuffer()) { + // Explicitly initialize a 4 MB shmem buffer for Perfetto producers (b/382369925) + android.tracing.perfetto.Producer.init(new InitArguments( + InitArguments.PERFETTO_BACKEND_SYSTEM, 4 * 1024)); + } + t.traceBegin("InitBeforeStartServices"); // Record the process start information in sys props. @@ -3114,10 +3121,10 @@ public final class SystemServer implements Dumpable { if (com.android.ranging.flags.Flags.rangingStackEnabled()) { if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_UWB) || context.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_WIFI_RTT) + PackageManager.FEATURE_WIFI_AWARE) || (com.android.ranging.flags.Flags.rangingCsEnabled() && context.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_BLUETOOTH_LE_CHANNEL_SOUNDING))) { + PackageManager.FEATURE_BLUETOOTH_LE))) { t.traceBegin("RangingService"); // TODO: b/375264320 - Remove after RELEASE_RANGING_STACK is ramped to next. try { diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java index d00e2c677930..1f45792e5097 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java @@ -33,6 +33,7 @@ import android.content.Context; import android.content.Intent; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; +import android.os.BinderProxy; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -290,11 +291,15 @@ public class DisplayEventDeliveryTest { } /** - * Return true if the freezer is enabled on this platform. + * Return true if the freezer is enabled on this platform and if freezer notifications are + * supported. It is not enough to test that the freezer notification feature is enabled + * because some devices do not have the necessary kernel support. */ private boolean isAppFreezerEnabled() { try { - return mActivityManager.getService().isAppFreezerEnabled(); + return mActivityManager.getService().isAppFreezerEnabled() + && android.os.Flags.binderFrozenStateChangeCallback() + && BinderProxy.isFrozenStateChangeCallbackSupported(); } catch (Exception e) { Log.e(TAG, "isAppFreezerEnabled() failed: " + e); return false; diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java index a7fc10f2fcc5..948371f74a9c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java @@ -29,6 +29,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.never; @@ -253,7 +254,11 @@ public class ActivitySnapshotControllerTests extends TaskSnapshotPersisterTestBa */ @Test public void testSkipRecordActivity() { - doReturn(createSnapshot()).when(mActivitySnapshotController).recordSnapshotInner(any()); + final AbsAppSnapshotController.SnapshotSupplier supplier = + new AbsAppSnapshotController.SnapshotSupplier(); + supplier.setSupplier(this::createSnapshot); + doReturn(supplier).when(mActivitySnapshotController).recordSnapshotInner( + any(), anyBoolean(), any()); final Task task = createTask(mDisplayContent); mSnapshotPersistQueue.setPaused(true); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java index 1edbcd527bf4..463254caa845 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java @@ -23,14 +23,10 @@ import static org.mockito.Mockito.spy; import android.compat.testing.PlatformCompatChangeRule; import android.graphics.Rect; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import androidx.annotation.NonNull; -import com.android.window.flags.Flags; - import junit.framework.Assert; import org.junit.Rule; @@ -125,8 +121,7 @@ public class AppCompatReachabilityOverridesTest extends WindowTestsBase { } @Test - @EnableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY) - public void testAllowReachabilityForThinLetterboxWithFlagEnabled() { + public void testAllowReachabilityForThinLetterbox_disableForThinLetterboxing() { runTestScenario((robot) -> { robot.activity().createActivityWithComponent(); @@ -142,24 +137,6 @@ public class AppCompatReachabilityOverridesTest extends WindowTestsBase { }); } - @Test - @DisableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY) - public void testAllowReachabilityForThinLetterboxWithFlagDisabled() { - runTestScenario((robot) -> { - robot.activity().createActivityWithComponent(); - - robot.configureIsVerticalThinLetterboxed(/* isThin */ true); - robot.checkAllowVerticalReachabilityForThinLetterbox(/* expected */ true); - robot.configureIsHorizontalThinLetterboxed(/* isThin */ true); - robot.checkAllowHorizontalReachabilityForThinLetterbox(/* expected */ true); - - robot.configureIsVerticalThinLetterboxed(/* isThin */ false); - robot.checkAllowVerticalReachabilityForThinLetterbox(/* expected */ true); - robot.configureIsHorizontalThinLetterboxed(/* isThin */ false); - robot.checkAllowHorizontalReachabilityForThinLetterbox(/* expected */ true); - }); - } - /** * Runs a test scenario providing a Robot. */ diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java index 429a396ad997..de4b6fac7abf 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java @@ -150,8 +150,8 @@ public class DragDropControllerTests extends WindowTestsBase { mProcess).build(); // Use a new TestIWindow so we don't collect events for other windows - final WindowState window = createWindow(null, TYPE_BASE_APPLICATION, activity, name, - ownerId, false, new TestIWindow()); + final WindowState window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken( + activity).setOwnerId(ownerId).setClientWindow(new TestIWindow()).build(); InputChannel channel = new InputChannel(); window.openInputChannel(channel); window.mHasSurface = true; @@ -249,7 +249,7 @@ public class DragDropControllerTests extends WindowTestsBase { mTarget.mDeferDragStateClosed = true; mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0); // Verify the drop event includes the drag surface - mTarget.handleMotionEvent(false, 0, 0); + mTarget.handleMotionEvent(false, mWindow.getDisplayId(), 0, 0); final DragEvent dropEvent = dragEvents.get(dragEvents.size() - 1); assertTrue(dropEvent.getDragSurface() != null); @@ -296,7 +296,7 @@ public class DragDropControllerTests extends WindowTestsBase { 0).getClipData().willParcelWithActivityInfo()); mTarget.reportDropWindow(globalInterceptWindow.mInputChannelToken, 0, 0); - mTarget.handleMotionEvent(false, 0, 0); + mTarget.handleMotionEvent(false, globalInterceptWindow.getDisplayId(), 0, 0); mToken = globalInterceptWindow.mClient.asBinder(); // Verify the drop event is only sent for the global intercept window @@ -334,8 +334,8 @@ public class DragDropControllerTests extends WindowTestsBase { try { mTarget.mDeferDragStateClosed = true; mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0); - // // Verify the drop event does not have the drag flags - mTarget.handleMotionEvent(false, 0, 0); + // Verify the drop event does not have the drag flags + mTarget.handleMotionEvent(false, mWindow.getDisplayId(), 0, 0); final DragEvent dropEvent = dragEvents.get(dragEvents.size() - 1); assertTrue(dropEvent.getDragFlags() == (View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG)); @@ -520,7 +520,7 @@ public class DragDropControllerTests extends WindowTestsBase { // Verify after consuming that the drag surface is relinquished mTarget.reportDropWindow(otherWindow.mInputChannelToken, 0, 0); - mTarget.handleMotionEvent(false, 0, 0); + mTarget.handleMotionEvent(false, otherWindow.getDisplayId(), 0, 0); mToken = otherWindow.mClient.asBinder(); mTarget.reportDropResult(otherIWindow, true); @@ -551,7 +551,7 @@ public class DragDropControllerTests extends WindowTestsBase { // Verify after consuming that the drag surface is relinquished mTarget.reportDropWindow(otherWindow.mInputChannelToken, 0, 0); - mTarget.handleMotionEvent(false, 0, 0); + mTarget.handleMotionEvent(false, otherWindow.getDisplayId(), 0, 0); mToken = otherWindow.mClient.asBinder(); mTarget.reportDropResult(otherIWindow, false); @@ -586,7 +586,8 @@ public class DragDropControllerTests extends WindowTestsBase { ClipData.newPlainText("label", "Test"), () -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY); - mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), + invalidXY, invalidXY); mTarget.reportDropResult(mWindow.mClient, false); mTarget.onUnhandledDropCallback(true); mToken = null; @@ -610,7 +611,8 @@ public class DragDropControllerTests extends WindowTestsBase { ClipData.newPlainText("label", "Test"), () -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); - mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), + invalidXY, invalidXY); mTarget.onUnhandledDropCallback(true); mToken = null; try { @@ -632,7 +634,8 @@ public class DragDropControllerTests extends WindowTestsBase { startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> { // Trigger an unhandled drop and verify the global drag listener was not called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); - mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, mDisplayContent.getDisplayId(), + invalidXY, invalidXY); mToken = null; try { verify(listener, never()).onUnhandledDrop(any(), any()); @@ -654,7 +657,8 @@ public class DragDropControllerTests extends WindowTestsBase { ClipData.newPlainText("label", "Test"), () -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); - mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, + mDisplayContent.getDisplayId(), invalidXY, invalidXY); // Verify that the unhandled drop listener callback timeout has been scheduled final Handler handler = mTarget.getHandler(); @@ -673,7 +677,8 @@ public class DragDropControllerTests extends WindowTestsBase { private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) { startDrag(flags, data, () -> { mTarget.reportDropWindow(mWindow.mInputChannelToken, dropX, dropY); - mTarget.handleMotionEvent(false /* keepHandling */, dropX, dropY); + mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), dropX, + dropY); mToken = mWindow.mClient.asBinder(); }); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java index 6655932b060b..c6b2a6b8d42f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java @@ -33,6 +33,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -45,12 +46,15 @@ import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; import android.hardware.HardwareBuffer; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.util.ArraySet; import android.window.TaskSnapshot; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; + import com.google.android.collect.Sets; import org.junit.Test; @@ -285,4 +289,27 @@ public class TaskSnapshotControllerTest extends WindowTestsBase { assertFalse(success); } + + @Test + @EnableFlags(Flags.FLAG_EXCLUDE_DRAWING_APP_THEME_SNAPSHOT_FROM_LOCK) + public void testRecordTaskSnapshot() { + spyOn(mWm.mTaskSnapshotController.mCache); + spyOn(mWm.mTaskSnapshotController); + doReturn(false).when(mWm.mTaskSnapshotController).shouldDisableSnapshots(); + + final WindowState normalWindow = createWindow(null, + FIRST_APPLICATION_WINDOW, mDisplayContent, "normalWindow"); + final TaskSnapshot snapshot = new TaskSnapshotPersisterTestBase.TaskSnapshotBuilder() + .setTopActivityComponent(normalWindow.mActivityRecord.mActivityComponent).build(); + doReturn(snapshot).when(mWm.mTaskSnapshotController).snapshot(any()); + final Task task = normalWindow.mActivityRecord.getTask(); + mWm.mTaskSnapshotController.recordSnapshot(task); + verify(mWm.mTaskSnapshotController.mCache).putSnapshot(eq(task), any()); + clearInvocations(mWm.mTaskSnapshotController.mCache); + + normalWindow.mAttrs.flags |= FLAG_SECURE; + mWm.mTaskSnapshotController.recordSnapshot(task); + waitHandlerIdle(mWm.mH); + verify(mWm.mTaskSnapshotController.mCache).putSnapshot(eq(task), any()); + } } diff --git a/tests/Input/src/com/android/test/input/KeyCharacterMapTest.kt b/tests/Input/src/com/android/test/input/KeyCharacterMapTest.kt index 281837920548..860d9f680c4c 100644 --- a/tests/Input/src/com/android/test/input/KeyCharacterMapTest.kt +++ b/tests/Input/src/com/android/test/input/KeyCharacterMapTest.kt @@ -16,10 +16,17 @@ package com.android.test.input +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule + import android.view.KeyCharacterMap import android.view.KeyEvent +import com.android.hardware.input.Flags + import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Rule import org.junit.Test /** @@ -30,26 +37,38 @@ import org.junit.Test * */ class KeyCharacterMapTest { + @get:Rule + val setFlagsRule = SetFlagsRule() + @Test + @EnableFlags(Flags.FLAG_REMOVE_FALLBACK_MODIFIERS) fun testGetFallback() { // Based off of VIRTUAL kcm fallbacks. val keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD) // One modifier fallback. - assertEquals( - keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_SPACE, - KeyEvent.META_CTRL_ON).keyCode, - KeyEvent.KEYCODE_LANGUAGE_SWITCH) + val oneModifierFallback = keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_SPACE, + KeyEvent.META_CTRL_ON) + assertEquals(KeyEvent.KEYCODE_LANGUAGE_SWITCH, oneModifierFallback.keyCode) + assertEquals(0, oneModifierFallback.metaState) // Multiple modifier fallback. - assertEquals( - keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_DEL, - KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON).keyCode, - KeyEvent.KEYCODE_BACK) + val twoModifierFallback = keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_DEL, + KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON) + assertEquals(KeyEvent.KEYCODE_BACK, twoModifierFallback.keyCode) + assertEquals(0, twoModifierFallback.metaState) // No default button, fallback only. - assertEquals( - keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_BUTTON_A, 0).keyCode, - KeyEvent.KEYCODE_DPAD_CENTER) + val keyOnlyFallback = + keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_BUTTON_A, 0) + assertEquals(KeyEvent.KEYCODE_DPAD_CENTER, keyOnlyFallback.keyCode) + assertEquals(0, keyOnlyFallback.metaState) + + // A key event that is not an exact match for a fallback. Expect a null return. + // E.g. Ctrl + Space -> LanguageSwitch + // Ctrl + Alt + Space -> Ctrl + Alt + Space (No fallback). + val noMatchFallback = keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_SPACE, + KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON) + assertNull(noMatchFallback) } } diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp index 661df4d0fe33..e24fe07f959b 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -683,8 +683,6 @@ class ChunkPrinter { item->PrettyPrint(printer_); printer_->Print(")"); } - - printer_->Print("\n"); } void PrintQualifiers(uint32_t qualifiers) const { @@ -763,11 +761,13 @@ class ChunkPrinter { bool PrintTableType(const ResTable_type* chunk) { printer_->Print(StringPrintf(" id: 0x%02x", android::util::DeviceToHost32(chunk->id))); - printer_->Print(StringPrintf( - " name: %s", - android::util::GetString(type_pool_, android::util::DeviceToHost32(chunk->id) - 1) - .c_str())); + const auto name = + android::util::GetString(type_pool_, android::util::DeviceToHost32(chunk->id) - 1); + printer_->Print(StringPrintf(" name: %s", name.c_str())); printer_->Print(StringPrintf(" flags: 0x%02x", android::util::DeviceToHost32(chunk->flags))); + printer_->Print(android::util::DeviceToHost32(chunk->flags) & ResTable_type::FLAG_SPARSE + ? " (SPARSE)" + : " (DENSE)"); printer_->Print( StringPrintf(" entryCount: %u", android::util::DeviceToHost32(chunk->entryCount))); printer_->Print( @@ -777,8 +777,7 @@ class ChunkPrinter { config.copyFromDtoH(chunk->config); printer_->Print(StringPrintf(" config: %s\n", config.to_string().c_str())); - const ResourceType* type = ParseResourceType( - android::util::GetString(type_pool_, android::util::DeviceToHost32(chunk->id) - 1)); + const ResourceType* type = ParseResourceType(name); printer_->Indent(); @@ -817,11 +816,8 @@ class ChunkPrinter { for (size_t i = 0; i < map_entry_count; i++) { PrintResValue(&(maps[i].value), config, type); - printer_->Print(StringPrintf( - " name: %s name-id:%d\n", - android::util::GetString(key_pool_, android::util::DeviceToHost32(maps[i].name.ident)) - .c_str(), - android::util::DeviceToHost32(maps[i].name.ident))); + printer_->Print(StringPrintf(" name-id: 0x%08x\n", + android::util::DeviceToHost32(maps[i].name.ident))); } } else { printer_->Print("\n"); @@ -829,6 +825,8 @@ class ChunkPrinter { // Print the value of the entry Res_value value = entry->value(); PrintResValue(&value, config, type); + + printer_->Print("\n"); } printer_->Undent(); |