diff options
129 files changed, 3587 insertions, 1341 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 9ba84c9d7944..a3d8978caa21 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -15651,6 +15651,7 @@ package android.graphics { public final class Gainmap implements android.os.Parcelable { ctor public Gainmap(@NonNull android.graphics.Bitmap); + ctor public Gainmap(@NonNull android.graphics.Gainmap, @NonNull android.graphics.Bitmap); method public int describeContents(); method @NonNull public float getDisplayRatioForFullHdr(); method @NonNull public float[] getEpsilonHdr(); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 7d2ef4d9b943..6cad578b6d7e 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1628,6 +1628,14 @@ public class Notification implements Parcelable */ public static final int GROUP_ALERT_CHILDREN = 2; + /** + * Constant for the {@link Builder#setGroup(String) group key} that is added to notifications + * that are not already grouped when {@link Builder#setSilent()} is used. + * + * @hide + */ + public static final String GROUP_KEY_SILENT = "silent"; + private int mGroupAlertBehavior = GROUP_ALERT_ALL; /** @@ -4290,6 +4298,35 @@ public class Notification implements Parcelable } /** + * If {@code true}, silences this instance of the notification, regardless of the sounds or + * vibrations set on the notification or notification channel. If {@code false}, then the + * normal sound and vibration logic applies. + * + * @hide + */ + public @NonNull Builder setSilent(boolean silent) { + if (!silent) { + return this; + } + if (mN.isGroupSummary()) { + setGroupAlertBehavior(GROUP_ALERT_CHILDREN); + } else { + setGroupAlertBehavior(GROUP_ALERT_SUMMARY); + } + + setVibrate(null); + setSound(null); + mN.defaults &= ~DEFAULT_SOUND; + mN.defaults &= ~DEFAULT_VIBRATE; + setDefaults(mN.defaults); + + if (TextUtils.isEmpty(mN.mGroupKey)) { + setGroup(GROUP_KEY_SILENT); + } + return this; + } + + /** * Set the first line of text in the platform notification template. */ @NonNull diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl index ee7836f0f7f0..ed8484fe7266 100644 --- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl +++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl @@ -21,6 +21,7 @@ import android.companion.virtual.IVirtualDeviceActivityListener; import android.companion.virtual.IVirtualDeviceSoundEffectListener; import android.companion.virtual.VirtualDevice; import android.companion.virtual.VirtualDeviceParams; +import android.content.AttributionSource; import android.hardware.display.IVirtualDisplayCallback; import android.hardware.display.VirtualDisplayConfig; @@ -46,7 +47,7 @@ interface IVirtualDeviceManager { */ @EnforcePermission("CREATE_VIRTUAL_DEVICE") IVirtualDevice createVirtualDevice( - in IBinder token, String packageName, int associationId, + in IBinder token, in AttributionSource attributionSource, int associationId, in VirtualDeviceParams params, in IVirtualDeviceActivityListener activityListener, in IVirtualDeviceSoundEffectListener soundEffectListener); diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java index f68cfff1c053..d13bfd4f6229 100644 --- a/core/java/android/companion/virtual/VirtualDeviceInternal.java +++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java @@ -145,7 +145,7 @@ public class VirtualDeviceInternal { mContext = context.getApplicationContext(); mVirtualDevice = service.createVirtualDevice( new Binder(), - mContext.getPackageName(), + mContext.getAttributionSource(), associationId, params, mActivityListenerBinder, diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java index 45d6dc62bfe8..b6d837589284 100644 --- a/core/java/android/companion/virtual/VirtualDeviceParams.java +++ b/core/java/android/companion/virtual/VirtualDeviceParams.java @@ -43,6 +43,7 @@ import android.util.ArraySet; import android.util.SparseArray; import android.util.SparseIntArray; +import java.io.PrintWriter; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -501,6 +502,26 @@ public final class VirtualDeviceParams implements Parcelable { + ")"; } + /** + * Dumps debugging information about the VirtualDeviceParams + * @hide + */ + public void dump(PrintWriter pw, String prefix) { + pw.println(prefix + "mName=" + mName); + pw.println(prefix + "mLockState=" + mLockState); + pw.println(prefix + "mUsersWithMatchingAccounts=" + mUsersWithMatchingAccounts); + pw.println(prefix + "mAllowedCrossTaskNavigations=" + mAllowedCrossTaskNavigations); + pw.println(prefix + "mBlockedCrossTaskNavigations=" + mBlockedCrossTaskNavigations); + pw.println(prefix + "mAllowedActivities=" + mAllowedActivities); + pw.println(prefix + "mBlockedActivities=" + mBlockedActivities); + pw.println(prefix + "mDevicePolicies=" + mDevicePolicies); + pw.println(prefix + "mDefaultNavigationPolicy=" + mDefaultNavigationPolicy); + pw.println(prefix + "mDefaultActivityPolicy=" + mDefaultActivityPolicy); + pw.println(prefix + "mVirtualSensorConfigs=" + mVirtualSensorConfigs); + pw.println(prefix + "mAudioPlaybackSessionId=" + mAudioPlaybackSessionId); + pw.println(prefix + "mAudioRecordingSessionId=" + mAudioRecordingSessionId); + } + @NonNull public static final Parcelable.Creator<VirtualDeviceParams> CREATOR = new Parcelable.Creator<VirtualDeviceParams>() { diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java index 3bdf9aa8015b..0dbe411a400e 100644 --- a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java +++ b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java @@ -104,6 +104,11 @@ public final class VirtualSensorConfig implements Parcelable { parcel.writeInt(mFlags); } + @Override + public String toString() { + return "VirtualSensorConfig{" + "mType=" + mType + ", mName='" + mName + '\'' + '}'; + } + /** * Returns the type of the sensor. * diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index 912e8df6bdc7..af448f0c4917 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -466,6 +466,19 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan // LINT.ThenChange(frameworks/base/core/java/android/hardware/biometrics/PromptInfo.java) /** + * Set if emergency call button should show, for example if biometrics are + * required to access the dialer app + * @param showEmergencyCallButton if true, shows emergency call button + * @return This builder. + * @hide + */ + @NonNull + public Builder setShowEmergencyCallButton(boolean showEmergencyCallButton) { + mPromptInfo.setShowEmergencyCallButton(showEmergencyCallButton); + return this; + } + + /** * Creates a {@link BiometricPrompt}. * * @return An instance of {@link BiometricPrompt}. diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java index e27507874167..24cfd1641410 100644 --- a/core/java/android/hardware/biometrics/PromptInfo.java +++ b/core/java/android/hardware/biometrics/PromptInfo.java @@ -48,6 +48,7 @@ public class PromptInfo implements Parcelable { private boolean mAllowBackgroundAuthentication; private boolean mIgnoreEnrollmentState; private boolean mIsForLegacyFingerprintManager = false; + private boolean mShowEmergencyCallButton = false; public PromptInfo() { @@ -72,6 +73,7 @@ public class PromptInfo implements Parcelable { mAllowBackgroundAuthentication = in.readBoolean(); mIgnoreEnrollmentState = in.readBoolean(); mIsForLegacyFingerprintManager = in.readBoolean(); + mShowEmergencyCallButton = in.readBoolean(); } public static final Creator<PromptInfo> CREATOR = new Creator<PromptInfo>() { @@ -111,6 +113,7 @@ public class PromptInfo implements Parcelable { dest.writeBoolean(mAllowBackgroundAuthentication); dest.writeBoolean(mIgnoreEnrollmentState); dest.writeBoolean(mIsForLegacyFingerprintManager); + dest.writeBoolean(mShowEmergencyCallButton); } // LINT.IfChange @@ -228,6 +231,10 @@ public class PromptInfo implements Parcelable { mAllowedSensorIds.add(sensorId); } + public void setShowEmergencyCallButton(boolean showEmergencyCallButton) { + mShowEmergencyCallButton = showEmergencyCallButton; + } + // Getters public CharSequence getTitle() { @@ -309,4 +316,8 @@ public class PromptInfo implements Parcelable { public boolean isForLegacyFingerprintManager() { return mIsForLegacyFingerprintManager; } + + public boolean isShowEmergencyCallButton() { + return mShowEmergencyCallButton; + } } diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java index 6baf91d720c3..ea951a55bfca 100644 --- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java @@ -236,9 +236,10 @@ public final class CameraExtensionCharacteristics { private static final CameraExtensionManagerGlobal GLOBAL_CAMERA_MANAGER = new CameraExtensionManagerGlobal(); private final Object mLock = new Object(); - private final int PROXY_SERVICE_DELAY_MS = 1000; + private final int PROXY_SERVICE_DELAY_MS = 2000; private InitializerFuture mInitFuture = null; private ServiceConnection mConnection = null; + private int mConnectionCount = 0; private ICameraExtensionsProxyService mProxy = null; private boolean mSupportsAdvancedExtensions = false; @@ -249,6 +250,15 @@ public final class CameraExtensionCharacteristics { return GLOBAL_CAMERA_MANAGER; } + private void releaseProxyConnectionLocked(Context ctx) { + if (mConnection != null ) { + ctx.unbindService(mConnection); + mConnection = null; + mProxy = null; + mConnectionCount = 0; + } + } + private void connectToProxyLocked(Context ctx) { if (mConnection == null) { Intent intent = new Intent(); @@ -270,7 +280,6 @@ public final class CameraExtensionCharacteristics { mConnection = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName component) { - mInitFuture.setStatus(false); mConnection = null; mProxy = null; } @@ -348,23 +357,32 @@ public final class CameraExtensionCharacteristics { public boolean registerClient(Context ctx, IBinder token) { synchronized (mLock) { + boolean ret = false; connectToProxyLocked(ctx); if (mProxy == null) { return false; } + mConnectionCount++; try { - return mProxy.registerClient(token); + ret = mProxy.registerClient(token); } catch (RemoteException e) { Log.e(TAG, "Failed to initialize extension! Extension service does " + " not respond!"); } + if (!ret) { + mConnectionCount--; + } - return false; + if (mConnectionCount <= 0) { + releaseProxyConnectionLocked(ctx); + } + + return ret; } } - public void unregisterClient(IBinder token) { + public void unregisterClient(Context ctx, IBinder token) { synchronized (mLock) { if (mProxy != null) { try { @@ -372,6 +390,11 @@ public final class CameraExtensionCharacteristics { } catch (RemoteException e) { Log.e(TAG, "Failed to de-initialize extension! Extension service does" + " not respond!"); + } finally { + mConnectionCount--; + if (mConnectionCount <= 0) { + releaseProxyConnectionLocked(ctx); + } } } } @@ -446,8 +469,8 @@ public final class CameraExtensionCharacteristics { /** * @hide */ - public static void unregisterClient(IBinder token) { - CameraExtensionManagerGlobal.get().unregisterClient(token); + public static void unregisterClient(Context ctx, IBinder token) { + CameraExtensionManagerGlobal.get().unregisterClient(ctx, token); } /** @@ -578,7 +601,7 @@ public final class CameraExtensionCharacteristics { } } } finally { - unregisterClient(token); + unregisterClient(mContext, token); } return Collections.unmodifiableList(ret); @@ -626,7 +649,7 @@ public final class CameraExtensionCharacteristics { Log.e(TAG, "Failed to query the extension for postview availability! Extension " + "service does not respond!"); } finally { - unregisterClient(token); + unregisterClient(mContext, token); } return false; @@ -722,7 +745,7 @@ public final class CameraExtensionCharacteristics { + "service does not respond!"); return Collections.emptyList(); } finally { - unregisterClient(token); + unregisterClient(mContext, token); } } @@ -791,7 +814,7 @@ public final class CameraExtensionCharacteristics { + " not respond!"); return new ArrayList<>(); } finally { - unregisterClient(token); + unregisterClient(mContext, token); } } @@ -872,7 +895,7 @@ public final class CameraExtensionCharacteristics { } } } finally { - unregisterClient(token); + unregisterClient(mContext, token); } } catch (RemoteException e) { Log.e(TAG, "Failed to query the extension supported sizes! Extension service does" @@ -957,7 +980,7 @@ public final class CameraExtensionCharacteristics { Log.e(TAG, "Failed to query the extension capture latency! Extension service does" + " not respond!"); } finally { - unregisterClient(token); + unregisterClient(mContext, token); } return null; @@ -998,7 +1021,7 @@ public final class CameraExtensionCharacteristics { Log.e(TAG, "Failed to query the extension progress callbacks! Extension service does" + " not respond!"); } finally { - unregisterClient(token); + unregisterClient(mContext, token); } return false; @@ -1075,7 +1098,7 @@ public final class CameraExtensionCharacteristics { } catch (RemoteException e) { throw new IllegalStateException("Failed to query the available capture request keys!"); } finally { - unregisterClient(token); + unregisterClient(mContext, token); } return Collections.unmodifiableSet(ret); @@ -1155,7 +1178,7 @@ public final class CameraExtensionCharacteristics { } catch (RemoteException e) { throw new IllegalStateException("Failed to query the available capture result keys!"); } finally { - unregisterClient(token); + unregisterClient(mContext, token); } return Collections.unmodifiableSet(ret); diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java index e06699bbbd86..c7e74c077f0d 100644 --- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java @@ -90,7 +90,7 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes private final HashMap<Integer, ImageReader> mReaderMap = new HashMap<>(); private RequestProcessor mRequestProcessor = new RequestProcessor(); private final int mSessionId; - private final IBinder mToken; + private IBinder mToken = null; private Surface mClientRepeatingRequestSurface; private Surface mClientCaptureSurface; @@ -103,6 +103,8 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes private boolean mInitialized; private boolean mSessionClosed; + private final Context mContext; + // Lock to synchronize cross-thread access to device public interface final Object mInterfaceLock; @@ -113,14 +115,9 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes public static CameraAdvancedExtensionSessionImpl createCameraAdvancedExtensionSession( @NonNull android.hardware.camera2.impl.CameraDeviceImpl cameraDevice, @NonNull Map<String, CameraCharacteristics> characteristicsMap, - @NonNull Context ctx, @NonNull ExtensionSessionConfiguration config, int sessionId) + @NonNull Context ctx, @NonNull ExtensionSessionConfiguration config, int sessionId, + @NonNull IBinder token) throws CameraAccessException, RemoteException { - final IBinder token = new Binder(TAG + " : " + sessionId); - boolean success = CameraExtensionCharacteristics.registerClient(ctx, token); - if (!success) { - throw new UnsupportedOperationException("Unsupported extension!"); - } - String cameraId = cameraDevice.getId(); CameraExtensionCharacteristics extensionChars = new CameraExtensionCharacteristics(ctx, cameraId, characteristicsMap); @@ -204,8 +201,9 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes IAdvancedExtenderImpl extender = CameraExtensionCharacteristics.initializeAdvancedExtension( config.getExtension()); extender.init(cameraId, characteristicsMapNative); - CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(extender, - cameraDevice, characteristicsMapNative, repeatingRequestSurface, + + CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(ctx, + extender, cameraDevice, characteristicsMapNative, repeatingRequestSurface, burstCaptureSurface, postviewSurface, config.getStateCallback(), config.getExecutor(), sessionId, token); @@ -217,13 +215,16 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes return ret; } - private CameraAdvancedExtensionSessionImpl(@NonNull IAdvancedExtenderImpl extender, + private CameraAdvancedExtensionSessionImpl(Context ctx, + @NonNull IAdvancedExtenderImpl extender, @NonNull CameraDeviceImpl cameraDevice, Map<String, CameraMetadataNative> characteristicsMap, @Nullable Surface repeatingRequestSurface, @Nullable Surface burstCaptureSurface, @Nullable Surface postviewSurface, @NonNull StateCallback callback, @NonNull Executor executor, - int sessionId, @NonNull IBinder token) { + int sessionId, + @NonNull IBinder token) { + mContext = ctx; mAdvancedExtender = extender; mCameraDevice = cameraDevice; mCharacteristicsMap = characteristicsMap; @@ -578,12 +579,16 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes mSessionProcessor = null; } - CameraExtensionCharacteristics.unregisterClient(mToken); - if (mInitialized || (mCaptureSession != null)) { - notifyClose = true; - CameraExtensionCharacteristics.releaseSession(); + + if (mToken != null) { + if (mInitialized || (mCaptureSession != null)) { + notifyClose = true; + CameraExtensionCharacteristics.releaseSession(); + } + CameraExtensionCharacteristics.unregisterClient(mContext, mToken); } mInitialized = false; + mToken = null; for (ImageReader reader : mReaderMap.values()) { reader.close(); diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index d3bde4b4b8a8..181ab2cf3421 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -2550,19 +2550,32 @@ public class CameraDeviceImpl extends CameraDevice HashMap<String, CameraCharacteristics> characteristicsMap = new HashMap<>( mPhysicalIdsToChars); characteristicsMap.put(mCameraId, mCharacteristics); + boolean initializationFailed = true; + IBinder token = new Binder(TAG + " : " + mNextSessionId++); try { + boolean ret = CameraExtensionCharacteristics.registerClient(mContext, token); + if (!ret) { + token = null; + throw new UnsupportedOperationException("Unsupported extension!"); + } + if (CameraExtensionCharacteristics.areAdvancedExtensionsSupported()) { mCurrentAdvancedExtensionSession = CameraAdvancedExtensionSessionImpl.createCameraAdvancedExtensionSession( this, characteristicsMap, mContext, extensionConfiguration, - mNextSessionId++); + mNextSessionId, token); } else { mCurrentExtensionSession = CameraExtensionSessionImpl.createCameraExtensionSession( this, characteristicsMap, mContext, extensionConfiguration, - mNextSessionId++); + mNextSessionId, token); } + initializationFailed = false; } catch (RemoteException e) { throw new CameraAccessException(CameraAccessException.CAMERA_ERROR); + } finally { + if (initializationFailed && (token != null)) { + CameraExtensionCharacteristics.unregisterClient(mContext, token); + } } } } diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java index 5d256813ffb6..bf77681bbbbd 100644 --- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java @@ -91,7 +91,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { private final Set<CaptureRequest.Key> mSupportedRequestKeys; private final Set<CaptureResult.Key> mSupportedResultKeys; private final ExtensionSessionStatsAggregator mStatsAggregator; - private final IBinder mToken; + private IBinder mToken = null; private boolean mCaptureResultsSupported; private CameraCaptureSession mCaptureSession = null; @@ -119,6 +119,8 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { // will do so internally. private boolean mInternalRepeatingRequestEnabled = true; + private final Context mContext; + // Lock to synchronize cross-thread access to device public interface final Object mInterfaceLock; @@ -135,14 +137,9 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { @NonNull Map<String, CameraCharacteristics> characteristicsMap, @NonNull Context ctx, @NonNull ExtensionSessionConfiguration config, - int sessionId) + int sessionId, + @NonNull IBinder token) throws CameraAccessException, RemoteException { - final IBinder token = new Binder(TAG + " : " + sessionId); - boolean success = CameraExtensionCharacteristics.registerClient(ctx, token); - if (!success) { - throw new UnsupportedOperationException("Unsupported extension!"); - } - String cameraId = cameraDevice.getId(); CameraExtensionCharacteristics extensionChars = new CameraExtensionCharacteristics(ctx, cameraId, characteristicsMap); @@ -234,6 +231,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { characteristicsMap.get(cameraId).getNativeMetadata()); CameraExtensionSessionImpl session = new CameraExtensionSessionImpl( + ctx, extenders.second, extenders.first, supportedPreviewSizes, @@ -256,7 +254,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { return session; } - public CameraExtensionSessionImpl(@NonNull IImageCaptureExtenderImpl imageExtender, + public CameraExtensionSessionImpl(Context ctx, @NonNull IImageCaptureExtenderImpl imageExtender, @NonNull IPreviewExtenderImpl previewExtender, @NonNull List<Size> previewSizes, @NonNull android.hardware.camera2.impl.CameraDeviceImpl cameraDevice, @@ -269,6 +267,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { @NonNull IBinder token, @NonNull Set<CaptureRequest.Key> requestKeys, @Nullable Set<CaptureResult.Key> resultKeys) { + mContext = ctx; mImageExtender = imageExtender; mPreviewExtender = previewExtender; mCameraDevice = cameraDevice; @@ -878,12 +877,15 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { + " respond!"); } - CameraExtensionCharacteristics.unregisterClient(mToken); - if (mInitialized || (mCaptureSession != null)) { - notifyClose = true; - CameraExtensionCharacteristics.releaseSession(); + if (mToken != null) { + if (mInitialized || (mCaptureSession != null)) { + notifyClose = true; + CameraExtensionCharacteristics.releaseSession(); + } + CameraExtensionCharacteristics.unregisterClient(mContext, mToken); } mInitialized = false; + mToken = null; if (mRepeatingRequestImageCallback != null) { mRepeatingRequestImageCallback.close(); diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index 94bff893b5a8..4700720736b5 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -370,8 +370,9 @@ public abstract class DisplayManagerInternal { /** * Returns the default size of the surface associated with the display, or null if the surface - * is not provided for layer mirroring by SurfaceFlinger. - * Only used for mirroring started from MediaProjection. + * is not provided for layer mirroring by SurfaceFlinger. Size is rotated to reflect the current + * display device orientation. + * Used for mirroring from MediaProjection, or a physical display based on display flags. */ public abstract Point getDisplaySurfaceDefaultSize(int displayId); diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java index bf72b1d7a035..2cda787082c7 100644 --- a/core/java/android/os/SystemVibrator.java +++ b/core/java/android/os/SystemVibrator.java @@ -18,26 +18,19 @@ package android.os; import android.annotation.CallbackExecutor; import android.annotation.NonNull; -import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; -import android.hardware.vibrator.IVibrator; +import android.os.vibrator.VibratorInfoFactory; import android.util.ArrayMap; import android.util.Log; -import android.util.Range; -import android.util.Slog; import android.util.SparseArray; -import android.util.SparseBooleanArray; -import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; -import java.util.Arrays; import java.util.Objects; import java.util.concurrent.Executor; -import java.util.function.Function; /** * Vibrator implementation that controls the main system vibrator. @@ -82,7 +75,7 @@ public class SystemVibrator extends Vibrator { if (vibratorIds.length == 0) { // It is known that the device has no vibrator, so cache and return info that // reflects the lack of support for effects/primitives. - return mVibratorInfo = new NoVibratorInfo(); + return mVibratorInfo = VibratorInfo.EMPTY_VIBRATOR_INFO; } VibratorInfo[] vibratorInfos = new VibratorInfo[vibratorIds.length]; for (int i = 0; i < vibratorIds.length; i++) { @@ -96,12 +89,7 @@ public class SystemVibrator extends Vibrator { } vibratorInfos[i] = vibrator.getInfo(); } - if (vibratorInfos.length == 1) { - // Device has a single vibrator info, cache and return successfully loaded info. - return mVibratorInfo = new VibratorInfo(/* id= */ -1, vibratorInfos[0]); - } - // Device has multiple vibrators, generate a single info representing all of them. - return mVibratorInfo = new MultiVibratorInfo(vibratorInfos); + return mVibratorInfo = VibratorInfoFactory.create(/* id= */ -1, vibratorInfos); } } @@ -275,296 +263,6 @@ public class SystemVibrator extends Vibrator { } /** - * Represents a device with no vibrator as a single {@link VibratorInfo}. - * - * @hide - */ - @VisibleForTesting - public static class NoVibratorInfo extends VibratorInfo { - public NoVibratorInfo() { - // Use empty arrays to indicate no support, while null would indicate support unknown. - super(/* id= */ -1, - /* capabilities= */ 0, - /* supportedEffects= */ new SparseBooleanArray(), - /* supportedBraking= */ new SparseBooleanArray(), - /* supportedPrimitives= */ new SparseIntArray(), - /* primitiveDelayMax= */ 0, - /* compositionSizeMax= */ 0, - /* pwlePrimitiveDurationMax= */ 0, - /* pwleSizeMax= */ 0, - /* qFactor= */ Float.NaN, - new FrequencyProfile(/* resonantFrequencyHz= */ Float.NaN, - /* minFrequencyHz= */ Float.NaN, - /* frequencyResolutionHz= */ Float.NaN, - /* maxAmplitudes= */ null)); - } - } - - /** - * Represents multiple vibrator information as a single {@link VibratorInfo}. - * - * <p>This uses an intersection of all vibrators to decide the capabilities and effect/primitive - * support. - * - * @hide - */ - @VisibleForTesting - public static class MultiVibratorInfo extends VibratorInfo { - // Epsilon used for float comparison applied in calculations for the merged info. - private static final float EPSILON = 1e-5f; - - public MultiVibratorInfo(VibratorInfo[] vibrators) { - // Need to use an extra constructor to share the computation in super initialization. - this(vibrators, frequencyProfileIntersection(vibrators)); - } - - private MultiVibratorInfo(VibratorInfo[] vibrators, - VibratorInfo.FrequencyProfile mergedProfile) { - super(/* id= */ -1, - capabilitiesIntersection(vibrators, mergedProfile.isEmpty()), - supportedEffectsIntersection(vibrators), - supportedBrakingIntersection(vibrators), - supportedPrimitivesAndDurationsIntersection(vibrators), - integerLimitIntersection(vibrators, VibratorInfo::getPrimitiveDelayMax), - integerLimitIntersection(vibrators, VibratorInfo::getCompositionSizeMax), - integerLimitIntersection(vibrators, VibratorInfo::getPwlePrimitiveDurationMax), - integerLimitIntersection(vibrators, VibratorInfo::getPwleSizeMax), - floatPropertyIntersection(vibrators, VibratorInfo::getQFactor), - mergedProfile); - } - - private static int capabilitiesIntersection(VibratorInfo[] infos, - boolean frequencyProfileIsEmpty) { - int intersection = ~0; - for (VibratorInfo info : infos) { - intersection &= info.getCapabilities(); - } - if (frequencyProfileIsEmpty) { - // Revoke frequency control if the merged frequency profile ended up empty. - intersection &= ~IVibrator.CAP_FREQUENCY_CONTROL; - } - return intersection; - } - - @Nullable - private static SparseBooleanArray supportedBrakingIntersection(VibratorInfo[] infos) { - for (VibratorInfo info : infos) { - if (!info.isBrakingSupportKnown()) { - // If one vibrator support is unknown, then the intersection is also unknown. - return null; - } - } - - SparseBooleanArray intersection = new SparseBooleanArray(); - SparseBooleanArray firstVibratorBraking = infos[0].getSupportedBraking(); - - brakingIdLoop: - for (int i = 0; i < firstVibratorBraking.size(); i++) { - int brakingId = firstVibratorBraking.keyAt(i); - if (!firstVibratorBraking.valueAt(i)) { - // The first vibrator already doesn't support this braking, so skip it. - continue brakingIdLoop; - } - - for (int j = 1; j < infos.length; j++) { - if (!infos[j].hasBrakingSupport(brakingId)) { - // One vibrator doesn't support this braking, so the intersection doesn't. - continue brakingIdLoop; - } - } - - intersection.put(brakingId, true); - } - - return intersection; - } - - @Nullable - private static SparseBooleanArray supportedEffectsIntersection(VibratorInfo[] infos) { - for (VibratorInfo info : infos) { - if (!info.isEffectSupportKnown()) { - // If one vibrator support is unknown, then the intersection is also unknown. - return null; - } - } - - SparseBooleanArray intersection = new SparseBooleanArray(); - SparseBooleanArray firstVibratorEffects = infos[0].getSupportedEffects(); - - effectIdLoop: - for (int i = 0; i < firstVibratorEffects.size(); i++) { - int effectId = firstVibratorEffects.keyAt(i); - if (!firstVibratorEffects.valueAt(i)) { - // The first vibrator already doesn't support this effect, so skip it. - continue effectIdLoop; - } - - for (int j = 1; j < infos.length; j++) { - if (infos[j].isEffectSupported(effectId) != VIBRATION_EFFECT_SUPPORT_YES) { - // One vibrator doesn't support this effect, so the intersection doesn't. - continue effectIdLoop; - } - } - - intersection.put(effectId, true); - } - - return intersection; - } - - @NonNull - private static SparseIntArray supportedPrimitivesAndDurationsIntersection( - VibratorInfo[] infos) { - SparseIntArray intersection = new SparseIntArray(); - SparseIntArray firstVibratorPrimitives = infos[0].getSupportedPrimitives(); - - primitiveIdLoop: - for (int i = 0; i < firstVibratorPrimitives.size(); i++) { - int primitiveId = firstVibratorPrimitives.keyAt(i); - int primitiveDuration = firstVibratorPrimitives.valueAt(i); - if (primitiveDuration == 0) { - // The first vibrator already doesn't support this primitive, so skip it. - continue primitiveIdLoop; - } - - for (int j = 1; j < infos.length; j++) { - int vibratorPrimitiveDuration = infos[j].getPrimitiveDuration(primitiveId); - if (vibratorPrimitiveDuration == 0) { - // One vibrator doesn't support this primitive, so the intersection doesn't. - continue primitiveIdLoop; - } else { - // The primitive vibration duration is the maximum among all vibrators. - primitiveDuration = Math.max(primitiveDuration, vibratorPrimitiveDuration); - } - } - - intersection.put(primitiveId, primitiveDuration); - } - return intersection; - } - - private static int integerLimitIntersection(VibratorInfo[] infos, - Function<VibratorInfo, Integer> propertyGetter) { - int limit = 0; // Limit 0 means unlimited - for (VibratorInfo info : infos) { - int vibratorLimit = propertyGetter.apply(info); - if ((limit == 0) || (vibratorLimit > 0 && vibratorLimit < limit)) { - // This vibrator is limited and intersection is unlimited or has a larger limit: - // use smaller limit here for the intersection. - limit = vibratorLimit; - } - } - return limit; - } - - private static float floatPropertyIntersection(VibratorInfo[] infos, - Function<VibratorInfo, Float> propertyGetter) { - float property = propertyGetter.apply(infos[0]); - if (Float.isNaN(property)) { - // If one vibrator is undefined then the intersection is undefined. - return Float.NaN; - } - for (int i = 1; i < infos.length; i++) { - if (Float.compare(property, propertyGetter.apply(infos[i])) != 0) { - // If one vibrator has a different value then the intersection is undefined. - return Float.NaN; - } - } - return property; - } - - @NonNull - private static FrequencyProfile frequencyProfileIntersection(VibratorInfo[] infos) { - float freqResolution = floatPropertyIntersection(infos, - info -> info.getFrequencyProfile().getFrequencyResolutionHz()); - float resonantFreq = floatPropertyIntersection(infos, - VibratorInfo::getResonantFrequencyHz); - Range<Float> freqRange = frequencyRangeIntersection(infos, freqResolution); - - if ((freqRange == null) || Float.isNaN(freqResolution)) { - return new FrequencyProfile(resonantFreq, Float.NaN, freqResolution, null); - } - - int amplitudeCount = - Math.round(1 + (freqRange.getUpper() - freqRange.getLower()) / freqResolution); - float[] maxAmplitudes = new float[amplitudeCount]; - - // Use MAX_VALUE here to ensure that the FrequencyProfile constructor called with this - // will fail if the loop below is broken and do not replace filled values with actual - // vibrator measurements. - Arrays.fill(maxAmplitudes, Float.MAX_VALUE); - - for (VibratorInfo info : infos) { - Range<Float> vibratorFreqRange = info.getFrequencyProfile().getFrequencyRangeHz(); - float[] vibratorMaxAmplitudes = info.getFrequencyProfile().getMaxAmplitudes(); - int vibratorStartIdx = Math.round( - (freqRange.getLower() - vibratorFreqRange.getLower()) / freqResolution); - int vibratorEndIdx = vibratorStartIdx + maxAmplitudes.length - 1; - - if ((vibratorStartIdx < 0) || (vibratorEndIdx >= vibratorMaxAmplitudes.length)) { - Slog.w(TAG, "Error calculating the intersection of vibrator frequency" - + " profiles: attempted to fetch from vibrator " - + info.getId() + " max amplitude with bad index " + vibratorStartIdx); - return new FrequencyProfile(resonantFreq, Float.NaN, Float.NaN, null); - } - - for (int i = 0; i < maxAmplitudes.length; i++) { - maxAmplitudes[i] = Math.min(maxAmplitudes[i], - vibratorMaxAmplitudes[vibratorStartIdx + i]); - } - } - - return new FrequencyProfile(resonantFreq, freqRange.getLower(), - freqResolution, maxAmplitudes); - } - - @Nullable - private static Range<Float> frequencyRangeIntersection(VibratorInfo[] infos, - float frequencyResolution) { - Range<Float> firstRange = infos[0].getFrequencyProfile().getFrequencyRangeHz(); - if (firstRange == null) { - // If one vibrator is undefined then the intersection is undefined. - return null; - } - float intersectionLower = firstRange.getLower(); - float intersectionUpper = firstRange.getUpper(); - - // Generate the intersection of all vibrator supported ranges, making sure that both - // min supported frequencies are aligned w.r.t. the frequency resolution. - - for (int i = 1; i < infos.length; i++) { - Range<Float> vibratorRange = infos[i].getFrequencyProfile().getFrequencyRangeHz(); - if (vibratorRange == null) { - // If one vibrator is undefined then the intersection is undefined. - return null; - } - - if ((vibratorRange.getLower() >= intersectionUpper) - || (vibratorRange.getUpper() <= intersectionLower)) { - // If the range and intersection are disjoint then the intersection is undefined - return null; - } - - float frequencyDelta = Math.abs(intersectionLower - vibratorRange.getLower()); - if ((frequencyDelta % frequencyResolution) > EPSILON) { - // If the intersection is not aligned with one vibrator then it's undefined - return null; - } - - intersectionLower = Math.max(intersectionLower, vibratorRange.getLower()); - intersectionUpper = Math.min(intersectionUpper, vibratorRange.getUpper()); - } - - if ((intersectionUpper - intersectionLower) < frequencyResolution) { - // If the intersection is empty then it's undefined. - return null; - } - - return Range.create(intersectionLower, intersectionUpper); - } - } - - /** * Listener for all vibrators state change. * * <p>This registers a listener to all vibrators to merge the callbacks into a single state diff --git a/core/java/android/os/VibratorInfo.java b/core/java/android/os/VibratorInfo.java index 0b7d7c3cb877..4f8c24d1f905 100644 --- a/core/java/android/os/VibratorInfo.java +++ b/core/java/android/os/VibratorInfo.java @@ -156,6 +156,16 @@ public class VibratorInfo implements Parcelable { return false; } VibratorInfo that = (VibratorInfo) o; + return mId == that.mId && equalContent(that); + } + + /** + * Returns {@code true} only if the properties and capabilities of the provided info, except for + * the ID, equals to this info. Returns {@code false} otherwise. + * + * @hide + */ + public boolean equalContent(VibratorInfo that) { int supportedPrimitivesCount = mSupportedPrimitives.size(); if (supportedPrimitivesCount != that.mSupportedPrimitives.size()) { return false; @@ -168,7 +178,7 @@ public class VibratorInfo implements Parcelable { return false; } } - return mId == that.mId && mCapabilities == that.mCapabilities + return mCapabilities == that.mCapabilities && mPrimitiveDelayMax == that.mPrimitiveDelayMax && mCompositionSizeMax == that.mCompositionSizeMax && mPwlePrimitiveDurationMax == that.mPwlePrimitiveDurationMax @@ -445,7 +455,8 @@ public class VibratorInfo implements Parcelable { return mFrequencyProfile; } - protected long getCapabilities() { + /** Returns a single int representing all the capabilities of the vibrator. */ + public long getCapabilities() { return mCapabilities; } diff --git a/core/java/android/os/vibrator/MultiVibratorInfo.java b/core/java/android/os/vibrator/MultiVibratorInfo.java new file mode 100644 index 000000000000..5f3273129213 --- /dev/null +++ b/core/java/android/os/vibrator/MultiVibratorInfo.java @@ -0,0 +1,294 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os.vibrator; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.hardware.vibrator.IVibrator; +import android.os.Vibrator; +import android.os.VibratorInfo; +import android.util.Range; +import android.util.Slog; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; + +import java.util.Arrays; +import java.util.function.Function; + +/** + * Represents multiple vibrator information as a single {@link VibratorInfo}. + * + * <p>This uses an intersection of all vibrators to decide the capabilities and effect/primitive + * support. + * + * @hide + */ +public final class MultiVibratorInfo extends VibratorInfo { + private static final String TAG = "MultiVibratorInfo"; + + // Epsilon used for float comparison applied in calculations for the merged info. + private static final float EPSILON = 1e-5f; + + public MultiVibratorInfo(int id, VibratorInfo[] vibrators) { + this(id, vibrators, frequencyProfileIntersection(vibrators)); + } + + private MultiVibratorInfo( + int id, VibratorInfo[] vibrators, VibratorInfo.FrequencyProfile mergedProfile) { + super(id, + capabilitiesIntersection(vibrators, mergedProfile.isEmpty()), + supportedEffectsIntersection(vibrators), + supportedBrakingIntersection(vibrators), + supportedPrimitivesAndDurationsIntersection(vibrators), + integerLimitIntersection(vibrators, VibratorInfo::getPrimitiveDelayMax), + integerLimitIntersection(vibrators, VibratorInfo::getCompositionSizeMax), + integerLimitIntersection(vibrators, VibratorInfo::getPwlePrimitiveDurationMax), + integerLimitIntersection(vibrators, VibratorInfo::getPwleSizeMax), + floatPropertyIntersection(vibrators, VibratorInfo::getQFactor), + mergedProfile); + } + + private static int capabilitiesIntersection(VibratorInfo[] infos, + boolean frequencyProfileIsEmpty) { + int intersection = ~0; + for (VibratorInfo info : infos) { + intersection &= info.getCapabilities(); + } + if (frequencyProfileIsEmpty) { + // Revoke frequency control if the merged frequency profile ended up empty. + intersection &= ~IVibrator.CAP_FREQUENCY_CONTROL; + } + return intersection; + } + + @Nullable + private static SparseBooleanArray supportedBrakingIntersection(VibratorInfo[] infos) { + for (VibratorInfo info : infos) { + if (!info.isBrakingSupportKnown()) { + // If one vibrator support is unknown, then the intersection is also unknown. + return null; + } + } + + SparseBooleanArray intersection = new SparseBooleanArray(); + SparseBooleanArray firstVibratorBraking = infos[0].getSupportedBraking(); + + brakingIdLoop: + for (int i = 0; i < firstVibratorBraking.size(); i++) { + int brakingId = firstVibratorBraking.keyAt(i); + if (!firstVibratorBraking.valueAt(i)) { + // The first vibrator already doesn't support this braking, so skip it. + continue brakingIdLoop; + } + + for (int j = 1; j < infos.length; j++) { + if (!infos[j].hasBrakingSupport(brakingId)) { + // One vibrator doesn't support this braking, so the intersection doesn't. + continue brakingIdLoop; + } + } + + intersection.put(brakingId, true); + } + + return intersection; + } + + @Nullable + private static SparseBooleanArray supportedEffectsIntersection(VibratorInfo[] infos) { + for (VibratorInfo info : infos) { + if (!info.isEffectSupportKnown()) { + // If one vibrator support is unknown, then the intersection is also unknown. + return null; + } + } + + SparseBooleanArray intersection = new SparseBooleanArray(); + SparseBooleanArray firstVibratorEffects = infos[0].getSupportedEffects(); + + effectIdLoop: + for (int i = 0; i < firstVibratorEffects.size(); i++) { + int effectId = firstVibratorEffects.keyAt(i); + if (!firstVibratorEffects.valueAt(i)) { + // The first vibrator already doesn't support this effect, so skip it. + continue effectIdLoop; + } + + for (int j = 1; j < infos.length; j++) { + if (infos[j].isEffectSupported(effectId) != Vibrator.VIBRATION_EFFECT_SUPPORT_YES) { + // One vibrator doesn't support this effect, so the intersection doesn't. + continue effectIdLoop; + } + } + + intersection.put(effectId, true); + } + + return intersection; + } + + @NonNull + private static SparseIntArray supportedPrimitivesAndDurationsIntersection( + VibratorInfo[] infos) { + SparseIntArray intersection = new SparseIntArray(); + SparseIntArray firstVibratorPrimitives = infos[0].getSupportedPrimitives(); + + primitiveIdLoop: + for (int i = 0; i < firstVibratorPrimitives.size(); i++) { + int primitiveId = firstVibratorPrimitives.keyAt(i); + int primitiveDuration = firstVibratorPrimitives.valueAt(i); + if (primitiveDuration == 0) { + // The first vibrator already doesn't support this primitive, so skip it. + continue primitiveIdLoop; + } + + for (int j = 1; j < infos.length; j++) { + int vibratorPrimitiveDuration = infos[j].getPrimitiveDuration(primitiveId); + if (vibratorPrimitiveDuration == 0) { + // One vibrator doesn't support this primitive, so the intersection doesn't. + continue primitiveIdLoop; + } else { + // The primitive vibration duration is the maximum among all vibrators. + primitiveDuration = Math.max(primitiveDuration, vibratorPrimitiveDuration); + } + } + + intersection.put(primitiveId, primitiveDuration); + } + return intersection; + } + + private static int integerLimitIntersection(VibratorInfo[] infos, + Function<VibratorInfo, Integer> propertyGetter) { + int limit = 0; // Limit 0 means unlimited + for (VibratorInfo info : infos) { + int vibratorLimit = propertyGetter.apply(info); + if ((limit == 0) || (vibratorLimit > 0 && vibratorLimit < limit)) { + // This vibrator is limited and intersection is unlimited or has a larger limit: + // use smaller limit here for the intersection. + limit = vibratorLimit; + } + } + return limit; + } + + private static float floatPropertyIntersection(VibratorInfo[] infos, + Function<VibratorInfo, Float> propertyGetter) { + float property = propertyGetter.apply(infos[0]); + if (Float.isNaN(property)) { + // If one vibrator is undefined then the intersection is undefined. + return Float.NaN; + } + for (int i = 1; i < infos.length; i++) { + if (Float.compare(property, propertyGetter.apply(infos[i])) != 0) { + // If one vibrator has a different value then the intersection is undefined. + return Float.NaN; + } + } + return property; + } + + @NonNull + private static FrequencyProfile frequencyProfileIntersection(VibratorInfo[] infos) { + float freqResolution = floatPropertyIntersection(infos, + info -> info.getFrequencyProfile().getFrequencyResolutionHz()); + float resonantFreq = floatPropertyIntersection(infos, + VibratorInfo::getResonantFrequencyHz); + Range<Float> freqRange = frequencyRangeIntersection(infos, freqResolution); + + if ((freqRange == null) || Float.isNaN(freqResolution)) { + return new FrequencyProfile(resonantFreq, Float.NaN, freqResolution, null); + } + + int amplitudeCount = + Math.round(1 + (freqRange.getUpper() - freqRange.getLower()) / freqResolution); + float[] maxAmplitudes = new float[amplitudeCount]; + + // Use MAX_VALUE here to ensure that the FrequencyProfile constructor called with this + // will fail if the loop below is broken and do not replace filled values with actual + // vibrator measurements. + Arrays.fill(maxAmplitudes, Float.MAX_VALUE); + + for (VibratorInfo info : infos) { + Range<Float> vibratorFreqRange = info.getFrequencyProfile().getFrequencyRangeHz(); + float[] vibratorMaxAmplitudes = info.getFrequencyProfile().getMaxAmplitudes(); + int vibratorStartIdx = Math.round( + (freqRange.getLower() - vibratorFreqRange.getLower()) / freqResolution); + int vibratorEndIdx = vibratorStartIdx + maxAmplitudes.length - 1; + + if ((vibratorStartIdx < 0) || (vibratorEndIdx >= vibratorMaxAmplitudes.length)) { + Slog.w(TAG, "Error calculating the intersection of vibrator frequency" + + " profiles: attempted to fetch from vibrator " + + info.getId() + " max amplitude with bad index " + vibratorStartIdx); + return new FrequencyProfile(resonantFreq, Float.NaN, Float.NaN, null); + } + + for (int i = 0; i < maxAmplitudes.length; i++) { + maxAmplitudes[i] = Math.min(maxAmplitudes[i], + vibratorMaxAmplitudes[vibratorStartIdx + i]); + } + } + + return new FrequencyProfile(resonantFreq, freqRange.getLower(), + freqResolution, maxAmplitudes); + } + + @Nullable + private static Range<Float> frequencyRangeIntersection(VibratorInfo[] infos, + float frequencyResolution) { + Range<Float> firstRange = infos[0].getFrequencyProfile().getFrequencyRangeHz(); + if (firstRange == null) { + // If one vibrator is undefined then the intersection is undefined. + return null; + } + float intersectionLower = firstRange.getLower(); + float intersectionUpper = firstRange.getUpper(); + + // Generate the intersection of all vibrator supported ranges, making sure that both + // min supported frequencies are aligned w.r.t. the frequency resolution. + + for (int i = 1; i < infos.length; i++) { + Range<Float> vibratorRange = infos[i].getFrequencyProfile().getFrequencyRangeHz(); + if (vibratorRange == null) { + // If one vibrator is undefined then the intersection is undefined. + return null; + } + + if ((vibratorRange.getLower() >= intersectionUpper) + || (vibratorRange.getUpper() <= intersectionLower)) { + // If the range and intersection are disjoint then the intersection is undefined + return null; + } + + float frequencyDelta = Math.abs(intersectionLower - vibratorRange.getLower()); + if ((frequencyDelta % frequencyResolution) > EPSILON) { + // If the intersection is not aligned with one vibrator then it's undefined + return null; + } + + intersectionLower = Math.max(intersectionLower, vibratorRange.getLower()); + intersectionUpper = Math.min(intersectionUpper, vibratorRange.getUpper()); + } + + if ((intersectionUpper - intersectionLower) < frequencyResolution) { + // If the intersection is empty then it's undefined. + return null; + } + + return Range.create(intersectionLower, intersectionUpper); + } +} diff --git a/core/java/android/os/vibrator/VibratorInfoFactory.java b/core/java/android/os/vibrator/VibratorInfoFactory.java new file mode 100644 index 000000000000..d10d7ec222c3 --- /dev/null +++ b/core/java/android/os/vibrator/VibratorInfoFactory.java @@ -0,0 +1,52 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os.vibrator; + +import android.annotation.NonNull; +import android.os.VibratorInfo; + +/** + * Factory for creating {@link VibratorInfo}s. + * + * @hide + */ +public final class VibratorInfoFactory { + /** + * Creates a single {@link VibratorInfo} that is an intersection of a given collection of + * {@link VibratorInfo}s. That is, the capabilities of the returned info will be an + * intersection of that of the provided infos. + * + * @param id the ID for the new {@link VibratorInfo}. + * @param vibratorInfos the {@link VibratorInfo}s from which to create a single + * {@link VibratorInfo}. + * @return a {@link VibratorInfo} that represents the intersection of {@code vibratorInfos}. + */ + @NonNull + public static VibratorInfo create(int id, @NonNull VibratorInfo[] vibratorInfos) { + if (vibratorInfos.length == 0) { + return new VibratorInfo.Builder(id).build(); + } + if (vibratorInfos.length == 1) { + // Create an equivalent info with the requested ID. + return new VibratorInfo(id, vibratorInfos[0]); + } + // Create a MultiVibratorInfo that intersects all the given infos and has the requested ID. + return new MultiVibratorInfo(id, vibratorInfos); + } + + private VibratorInfoFactory() {} +} diff --git a/core/java/android/service/notification/NotificationRankingUpdate.java b/core/java/android/service/notification/NotificationRankingUpdate.java index 75640bd47ab9..f3b4c6da4a01 100644 --- a/core/java/android/service/notification/NotificationRankingUpdate.java +++ b/core/java/android/service/notification/NotificationRankingUpdate.java @@ -92,6 +92,7 @@ public class NotificationRankingUpdate implements Parcelable { mapParcel.recycle(); if (buffer != null) { mRankingMapFd.unmap(buffer); + mRankingMapFd.close(); } } } else { diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index 751cd210b94c..6e73a3c93fd7 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -19,7 +19,10 @@ package android.view; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.graphics.Matrix; import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; import android.view.inputmethod.InputMethodManager; import android.widget.TextView; @@ -78,11 +81,17 @@ public class HandwritingInitiator { private int mConnectionCount = 0; private final InputMethodManager mImm; + private final RectF mTempRectF = new RectF(); + + private final Region mTempRegion = new Region(); + + private final Matrix mTempMatrix = new Matrix(); + /** * The handwrite-able View that is currently the target of a hovering stylus pointer. This is * used to help determine whether the handwriting PointerIcon should be shown in * {@link #onResolvePointerIcon(Context, MotionEvent)} so that we can reduce the number of calls - * to {@link #findBestCandidateView(float, float)}. + * to {@link #findBestCandidateView(float, float, boolean)}. */ @Nullable private WeakReference<View> mCachedHoverTarget = null; @@ -184,8 +193,8 @@ public class HandwritingInitiator { final float y = motionEvent.getY(pointerIndex); if (largerThanTouchSlop(x, y, mState.mStylusDownX, mState.mStylusDownY)) { mState.mExceedHandwritingSlop = true; - View candidateView = - findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY); + View candidateView = findBestCandidateView(mState.mStylusDownX, + mState.mStylusDownY, /* isHover */ false); if (candidateView != null) { if (candidateView == getConnectedView()) { if (!candidateView.hasFocus()) { @@ -393,13 +402,14 @@ public class HandwritingInitiator { final View cachedHoverTarget = getCachedHoverTarget(); if (cachedHoverTarget != null) { final Rect handwritingArea = getViewHandwritingArea(cachedHoverTarget); - if (isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget) + if (isInHandwritingArea(handwritingArea, hoverX, hoverY, cachedHoverTarget, + /* isHover */ true) && shouldTriggerStylusHandwritingForView(cachedHoverTarget)) { return cachedHoverTarget; } } - final View candidateView = findBestCandidateView(hoverX, hoverY); + final View candidateView = findBestCandidateView(hoverX, hoverY, /* isHover */ true); if (candidateView != null) { mCachedHoverTarget = new WeakReference<>(candidateView); @@ -429,14 +439,14 @@ public class HandwritingInitiator { * @param y the y coordinates of the stylus event, in the coordinates of the window. */ @Nullable - private View findBestCandidateView(float x, float y) { + private View findBestCandidateView(float x, float y, boolean isHover) { // If the connectedView is not null and do not set any handwriting area, it will check // whether the connectedView's boundary contains the initial stylus position. If true, // directly return the connectedView. final View connectedView = getConnectedView(); if (connectedView != null) { Rect handwritingArea = getViewHandwritingArea(connectedView); - if (isInHandwritingArea(handwritingArea, x, y, connectedView) + if (isInHandwritingArea(handwritingArea, x, y, connectedView, isHover) && shouldTriggerStylusHandwritingForView(connectedView)) { return connectedView; } @@ -450,7 +460,7 @@ public class HandwritingInitiator { for (HandwritableViewInfo viewInfo : handwritableViewInfos) { final View view = viewInfo.getView(); final Rect handwritingArea = viewInfo.getHandwritingArea(); - if (!isInHandwritingArea(handwritingArea, x, y, view) + if (!isInHandwritingArea(handwritingArea, x, y, view, isHover) || !shouldTriggerStylusHandwritingForView(view)) { continue; } @@ -546,15 +556,48 @@ public class HandwritingInitiator { * Return true if the (x, y) is inside by the given {@link Rect} with the View's * handwriting bounds with offsets applied. */ - private static boolean isInHandwritingArea(@Nullable Rect handwritingArea, - float x, float y, View view) { + private boolean isInHandwritingArea(@Nullable Rect handwritingArea, + float x, float y, View view, boolean isHover) { if (handwritingArea == null) return false; - return contains(handwritingArea, x, y, + if (!contains(handwritingArea, x, y, view.getHandwritingBoundsOffsetLeft(), view.getHandwritingBoundsOffsetTop(), view.getHandwritingBoundsOffsetRight(), - view.getHandwritingBoundsOffsetBottom()); + view.getHandwritingBoundsOffsetBottom())) { + return false; + } + + // The returned handwritingArea computed by ViewParent#getChildVisibleRect didn't consider + // the case where a view is stacking on top of the editor. (e.g. DrawerLayout, popup) + // We must check the hit region of the editor again, and avoid the case where another + // view on top of the editor is handling MotionEvents. + ViewParent parent = view.getParent(); + if (parent == null) { + return true; + } + + Region region = mTempRegion; + mTempRegion.set(0, 0, view.getWidth(), view.getHeight()); + Matrix matrix = mTempMatrix; + matrix.reset(); + if (!parent.getChildLocalHitRegion(view, region, matrix, isHover)) { + return false; + } + + // It's not easy to extend the region by the given handwritingBoundsOffset. Instead, we + // create a rectangle surrounding the motion event location and check if this rectangle + // overlaps with the hit region of the editor. + float left = x - view.getHandwritingBoundsOffsetRight(); + float top = y - view.getHandwritingBoundsOffsetBottom(); + float right = Math.max(x + view.getHandwritingBoundsOffsetLeft(), left + 1); + float bottom = Math.max(y + view.getHandwritingBoundsOffsetTop(), top + 1); + RectF rectF = mTempRectF; + rectF.set(left, top, right, bottom); + matrix.mapRect(rectF); + + return region.op(Math.round(rectF.left), Math.round(rectF.top), + Math.round(rectF.right), Math.round(rectF.bottom), Region.Op.INTERSECT); } /** diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java index 6c5f195ba2a0..fabfed39a913 100644 --- a/core/java/android/view/InsetsAnimationControlImpl.java +++ b/core/java/android/view/InsetsAnimationControlImpl.java @@ -28,7 +28,6 @@ import static android.view.InsetsAnimationControlImplProto.PENDING_FRACTION; import static android.view.InsetsAnimationControlImplProto.PENDING_INSETS; import static android.view.InsetsAnimationControlImplProto.SHOWN_ON_FINISH; import static android.view.InsetsAnimationControlImplProto.TMP_MATRIX; -import static android.view.InsetsController.ANIMATION_TYPE_SHOW; import static android.view.InsetsController.AnimationType; import static android.view.InsetsController.DEBUG; import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_SHOWN; @@ -285,15 +284,11 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro return false; } final Insets offset = Insets.subtract(mShownInsets, mPendingInsets); - ArrayList<SurfaceParams> params = new ArrayList<>(); - updateLeashesForSide(ISIDE_LEFT, offset.left, mPendingInsets.left, params, outState, - mPendingAlpha); - updateLeashesForSide(ISIDE_TOP, offset.top, mPendingInsets.top, params, outState, - mPendingAlpha); - updateLeashesForSide(ISIDE_RIGHT, offset.right, mPendingInsets.right, params, outState, - mPendingAlpha); - updateLeashesForSide(ISIDE_BOTTOM, offset.bottom, mPendingInsets.bottom, params, outState, - mPendingAlpha); + final ArrayList<SurfaceParams> params = new ArrayList<>(); + updateLeashesForSide(ISIDE_LEFT, offset.left, params, outState, mPendingAlpha); + updateLeashesForSide(ISIDE_TOP, offset.top, params, outState, mPendingAlpha); + updateLeashesForSide(ISIDE_RIGHT, offset.right, params, outState, mPendingAlpha); + updateLeashesForSide(ISIDE_BOTTOM, offset.bottom, params, outState, mPendingAlpha); mController.applySurfaceParams(params.toArray(new SurfaceParams[params.size()])); mCurrentInsets = mPendingInsets; @@ -457,7 +452,7 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro return alpha >= 1 ? 1 : (alpha <= 0 ? 0 : alpha); } - private void updateLeashesForSide(@InternalInsetsSide int side, int offset, int inset, + private void updateLeashesForSide(@InternalInsetsSide int side, int offset, ArrayList<SurfaceParams> surfaceParams, @Nullable InsetsState outState, float alpha) { final ArraySet<InsetsSourceControl> controls = mSideControlsMap.get(side); if (controls == null) { @@ -475,9 +470,9 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro } addTranslationToMatrix(side, offset, mTmpMatrix, mTmpFrame); - final boolean visible = mHasZeroInsetsIme && side == ISIDE_BOTTOM - ? (mAnimationType == ANIMATION_TYPE_SHOW || !mFinished) - : inset != 0; + final boolean visible = mPendingFraction == 0 && source != null + ? source.isVisible() + : !mFinished || mShownOnFinish; if (outState != null && source != null) { outState.addSource(new InsetsSource(source) diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index c6d8bd18bc28..8ec7d6779392 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -666,9 +666,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation /** Set of inset types for which an animation was started since last resetting this field */ private @InsetsType int mLastStartedAnimTypes; - /** Set of inset types which cannot be controlled by the user animation */ - private @InsetsType int mDisabledUserAnimationInsetsTypes; - /** Set of inset types which are existing */ private @InsetsType int mExistingTypes = 0; @@ -887,21 +884,11 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation mState.set(newState, 0 /* types */); @InsetsType int existingTypes = 0; @InsetsType int visibleTypes = 0; - @InsetsType int disabledUserAnimationTypes = 0; @InsetsType int[] cancelledUserAnimationTypes = {0}; for (int i = 0, size = newState.sourceSize(); i < size; i++) { final InsetsSource source = newState.sourceAt(i); @InsetsType int type = source.getType(); @AnimationType int animationType = getAnimationType(type); - if (!source.isUserControllable()) { - // The user animation is not allowed when visible frame is empty. - disabledUserAnimationTypes |= type; - if (animationType == ANIMATION_TYPE_USER) { - // Existing user animation needs to be cancelled. - animationType = ANIMATION_TYPE_NONE; - cancelledUserAnimationTypes[0] |= type; - } - } final InsetsSourceConsumer consumer = mSourceConsumers.get(source.getId()); if (consumer != null) { consumer.updateSource(source, animationType); @@ -931,28 +918,11 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } InsetsState.traverse(mState, newState, mRemoveGoneSources); - updateDisabledUserAnimationTypes(disabledUserAnimationTypes); - if (cancelledUserAnimationTypes[0] != 0) { mHandler.post(() -> show(cancelledUserAnimationTypes[0])); } } - private void updateDisabledUserAnimationTypes(@InsetsType int disabledUserAnimationTypes) { - @InsetsType int diff = mDisabledUserAnimationInsetsTypes ^ disabledUserAnimationTypes; - if (diff != 0) { - for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { - InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); - if (consumer.getControl() != null && (consumer.getType() & diff) != 0) { - mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners); - mHandler.post(mInvokeControllableInsetsChangedListeners); - break; - } - } - mDisabledUserAnimationInsetsTypes = disabledUserAnimationTypes; - } - } - private boolean captionInsetsUnchanged() { if (CAPTION_ON_SHELL) { return false; @@ -1332,26 +1302,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation + " while an existing " + Type.toString(mTypesBeingCancelled) + " is being cancelled."); } - if (animationType == ANIMATION_TYPE_USER) { - final @InsetsType int disabledTypes = types & mDisabledUserAnimationInsetsTypes; - if (DEBUG) Log.d(TAG, "user animation disabled types: " + disabledTypes); - types &= ~mDisabledUserAnimationInsetsTypes; - - if ((disabledTypes & ime()) != 0) { - ImeTracker.forLogging().onFailed(statsToken, - ImeTracker.PHASE_CLIENT_DISABLED_USER_ANIMATION); - - if (fromIme - && !mState.isSourceOrDefaultVisible(mImeSourceConsumer.getId(), ime())) { - // We've requested IMM to show IME, but the IME is not controllable. We need to - // cancel the request. - setRequestedVisibleTypes(0 /* visibleTypes */, ime()); - if (mImeSourceConsumer.onAnimationStateChanged(false /* running */)) { - notifyVisibilityChanged(); - } - } - } - } if (types == 0) { // nothing to animate. listener.onCancelled(null); @@ -1954,7 +1904,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); InsetsSource source = mState.peekSource(consumer.getId()); - if (consumer.getControl() != null && source != null && source.isUserControllable()) { + if (consumer.getControl() != null && source != null) { result |= consumer.getType(); } } diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java index ff009ed09329..0d5704eed4b3 100644 --- a/core/java/android/view/InsetsSource.java +++ b/core/java/android/view/InsetsSource.java @@ -187,11 +187,6 @@ public class InsetsSource implements Parcelable { return (mFlags & flags) == flags; } - boolean isUserControllable() { - // If mVisibleFrame is null, it will be the same area as mFrame. - return mVisibleFrame == null || !mVisibleFrame.isEmpty(); - } - /** * Calculates the insets this source will cause to a client window. * diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 92509c969055..5e19c675bb88 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -9279,8 +9279,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } while (parentGroup != null && !parentGroup.isImportantForAutofill()) { - ignoredParentLeft += parentGroup.mLeft; - ignoredParentTop += parentGroup.mTop; + ignoredParentLeft += parentGroup.mLeft - parentGroup.mScrollX; + ignoredParentTop += parentGroup.mTop - parentGroup.mScrollY; viewParent = parentGroup.getParent(); if (viewParent instanceof View) { diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 1b1098d9d57a..7bdff8c5b858 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -7361,6 +7361,90 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } + /** + * @hide + */ + @Override + public boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region, + @NonNull Matrix matrix, boolean isHover) { + if (!child.hasIdentityMatrix()) { + matrix.preConcat(child.getInverseMatrix()); + } + + final int dx = child.mLeft - mScrollX; + final int dy = child.mTop - mScrollY; + matrix.preTranslate(-dx, -dy); + + final int width = mRight - mLeft; + final int height = mBottom - mTop; + + // Map the bounds of this view into the region's coordinates and clip the region. + final RectF rect = mAttachInfo != null ? mAttachInfo.mTmpTransformRect : new RectF(); + rect.set(0, 0, width, height); + matrix.mapRect(rect); + + boolean notEmpty = region.op(Math.round(rect.left), Math.round(rect.top), + Math.round(rect.right), Math.round(rect.bottom), Region.Op.INTERSECT); + + if (isHover) { + HoverTarget target = mFirstHoverTarget; + boolean childIsHit = false; + while (target != null) { + final HoverTarget next = target.next; + if (target.child == child) { + childIsHit = true; + break; + } + target = next; + } + if (!childIsHit) { + target = mFirstHoverTarget; + while (notEmpty && target != null) { + final HoverTarget next = target.next; + final View hoveredView = target.child; + + rect.set(hoveredView.mLeft, hoveredView.mTop, hoveredView.mRight, + hoveredView.mBottom); + matrix.mapRect(rect); + notEmpty = region.op(Math.round(rect.left), Math.round(rect.top), + Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE); + target = next; + } + } + } else { + TouchTarget target = mFirstTouchTarget; + boolean childIsHit = false; + while (target != null) { + final TouchTarget next = target.next; + if (target.child == child) { + childIsHit = true; + break; + } + target = next; + } + if (!childIsHit) { + target = mFirstTouchTarget; + while (notEmpty && target != null) { + final TouchTarget next = target.next; + final View touchedView = target.child; + + rect.set(touchedView.mLeft, touchedView.mTop, touchedView.mRight, + touchedView.mBottom); + matrix.mapRect(rect); + notEmpty = region.op(Math.round(rect.left), Math.round(rect.top), + Math.round(rect.right), Math.round(rect.bottom), Region.Op.DIFFERENCE); + target = next; + } + } + } + + if (notEmpty && mParent != null) { + notEmpty = mParent.getChildLocalHitRegion(this, region, matrix, isHover); + } + return notEmpty; + } + + private static void applyOpToRegionByBounds(Region region, View view, Region.Op op) { final int[] locationInWindow = new int[2]; view.getLocationInWindow(locationInWindow); diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index 1020d2ef02be..54bc3484d295 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -18,6 +18,7 @@ package android.view; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.Region; import android.os.Bundle; @@ -686,6 +687,36 @@ public interface ViewParent { } /** + * Compute the region where the child can receive the {@link MotionEvent}s from the root view. + * + * <p> Given region where the child will accept {@link MotionEvent}s. + * Modify the region to the unblocked region where the child can receive the + * {@link MotionEvent}s from the view root. + * </p> + * + * <p> The given region is always clipped by the bounds of the parent views. When there are + * on-going {@link MotionEvent}s, this method also makes use of the event dispatching results to + * determine whether a sibling view will also block the child's hit region. + * </p> + * + * @param child a child View, whose hit region we want to compute. + * @param region the initial hit region where the child view will handle {@link MotionEvent}s, + * defined in the child coordinates. Will be overwritten to the result hit region. + * @param matrix the matrix that maps the given child view's coordinates to the region + * coordinates. It will be modified to a matrix that maps window coordinates to + * the result region's coordinates. + * @param isHover if true it will return the hover events' hit region, otherwise it will + * return the touch events' hit region. + * @return true if the returned region is not empty. + * @hide + */ + default boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region, + @NonNull Matrix matrix, boolean isHover) { + region.setEmpty(); + return false; + } + + /** * Unbuffered dispatch has been requested by a child of this view parent. * This method is called by the View hierarchy to signal ancestors that a View needs to * request unbuffered dispatch. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 3aa610af60b0..ddd3269925a4 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -128,6 +128,7 @@ import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.RecordingCanvas; import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.Region; import android.graphics.RenderNode; import android.graphics.drawable.Drawable; @@ -321,13 +322,6 @@ public final class ViewRootImpl implements ViewParent, SystemProperties.getBoolean("persist.wm.debug.client_immersive_confirmation", false); /** - * Whether the client should compute the window frame on its own. - * @hide - */ - public static final boolean LOCAL_LAYOUT = - SystemProperties.getBoolean("persist.debug.local_layout", true); - - /** * Set this system property to true to force the view hierarchy to render * at 60 Hz. This can be used to measure the potential framerate. */ @@ -1911,8 +1905,8 @@ public final class ViewRootImpl implements ViewParent, final float compatScale = frames.compatScale; final boolean frameChanged = !mWinFrame.equals(frame); final boolean configChanged = !mLastReportedMergedConfiguration.equals(mergedConfiguration); - final boolean attachedFrameChanged = LOCAL_LAYOUT - && !Objects.equals(mTmpFrames.attachedFrame, attachedFrame); + final boolean attachedFrameChanged = + !Objects.equals(mTmpFrames.attachedFrame, attachedFrame); final boolean displayChanged = mDisplay.getDisplayId() != displayId; final boolean compatScaleChanged = mTmpFrames.compatScale != compatScale; final boolean dragResizingChanged = mPendingDragResizing != dragResizing; @@ -2397,6 +2391,22 @@ public final class ViewRootImpl implements ViewParent, } @Override + public boolean getChildLocalHitRegion(@NonNull View child, @NonNull Region region, + @NonNull Matrix matrix, boolean isHover) { + if (child != mView) { + throw new IllegalArgumentException("child " + child + " is not the root view " + + mView + " managed by this ViewRootImpl"); + } + + RectF rectF = new RectF(0, 0, mWidth, mHeight); + matrix.mapRect(rectF); + // Note: don't apply scroll offset, because we want to know its + // visibility in the virtual canvas being given to the view hierarchy. + return region.op(Math.round(rectF.left), Math.round(rectF.top), + Math.round(rectF.right), Math.round(rectF.bottom), Region.Op.INTERSECT); + } + + @Override public void bringChildToFront(View child) { } @@ -8292,8 +8302,7 @@ public final class ViewRootImpl implements ViewParent, final int measuredWidth = mMeasuredWidth; final int measuredHeight = mMeasuredHeight; final boolean relayoutAsync; - if (LOCAL_LAYOUT - && (mViewFrameInfo.flags & FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED) == 0 + if ((mViewFrameInfo.flags & FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED) == 0 && mWindowAttributes.type != TYPE_APPLICATION_STARTING && mSyncSeqId <= mLastSyncSeqId && winConfigFromAm.diff(winConfigFromWm, false /* compareUndefined */) == 0) { diff --git a/core/java/com/android/internal/widget/BigPictureNotificationImageView.java b/core/java/com/android/internal/widget/BigPictureNotificationImageView.java index 3a7cf74df4a8..f95f5db8358e 100644 --- a/core/java/com/android/internal/widget/BigPictureNotificationImageView.java +++ b/core/java/com/android/internal/widget/BigPictureNotificationImageView.java @@ -37,13 +37,16 @@ import com.android.internal.R; * Icon.loadDrawable(). */ @RemoteViews.RemoteView -public class BigPictureNotificationImageView extends ImageView { +public class BigPictureNotificationImageView extends ImageView implements + NotificationDrawableConsumer { private static final String TAG = BigPictureNotificationImageView.class.getSimpleName(); private final int mMaximumDrawableWidth; private final int mMaximumDrawableHeight; + private NotificationIconManager mIconManager; + public BigPictureNotificationImageView(@NonNull Context context) { this(context, null, 0, 0); } @@ -69,6 +72,19 @@ public class BigPictureNotificationImageView extends ImageView { : R.dimen.notification_big_picture_max_height); } + + /** + * Sets an {@link NotificationIconManager} on this ImageView, which handles the loading of + * icons, instead of using the {@link LocalImageResolver} directly. + * If set, it overrides the behaviour of {@link #setImageIconAsync} and {@link #setImageIcon}, + * and it expects that the content of this imageView is only updated calling these two methods. + * + * @param iconManager to be called, when the icon is updated + */ + public void setIconManager(NotificationIconManager iconManager) { + mIconManager = iconManager; + } + @Override @android.view.RemotableViewMethod(asyncImpl = "setImageURIAsync") public void setImageURI(@Nullable Uri uri) { @@ -84,11 +100,20 @@ public class BigPictureNotificationImageView extends ImageView { @Override @android.view.RemotableViewMethod(asyncImpl = "setImageIconAsync") public void setImageIcon(@Nullable Icon icon) { + if (mIconManager != null) { + mIconManager.updateIcon(this, icon).run(); + return; + } + // old code path setImageDrawable(loadImage(icon)); } /** @hide **/ public Runnable setImageIconAsync(@Nullable Icon icon) { + if (mIconManager != null) { + return mIconManager.updateIcon(this, icon); + } + // old code path final Drawable drawable = loadImage(icon); return () -> setImageDrawable(drawable); } diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index 5b6b36043684..42be784d8baa 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -149,6 +149,7 @@ public class ConversationLayout extends FrameLayout private View mAppNameDivider; private TouchDelegateComposite mTouchDelegate = new TouchDelegateComposite(this); private ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>(); + private boolean mPrecomputedTextEnabled = false; public ConversationLayout(@NonNull Context context) { super(context); @@ -389,36 +390,37 @@ public class ConversationLayout extends FrameLayout */ @RemotableViewMethod(asyncImpl = "setDataAsync") public void setData(Bundle extras) { + bind(parseMessagingData(extras, /* usePrecomputedText= */ false)); + } + + @NonNull + private MessagingData parseMessagingData(Bundle extras, boolean usePrecomputedText) { Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); - List<Notification.MessagingStyle.Message> newMessages - = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); + List<Notification.MessagingStyle.Message> newMessages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES); - List<Notification.MessagingStyle.Message> newHistoricMessages - = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages); + List<Notification.MessagingStyle.Message> newHistoricMessages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages); // mUser now set (would be nice to avoid the side effect but WHATEVER) final Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, Person.class); // Append remote input history to newMessages (again, side effect is lame but WHATEVS) RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[]) - extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, android.app.RemoteInputHistoryItem.class); + extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, + RemoteInputHistoryItem.class); addRemoteInputHistoryToMessages(newMessages, history); boolean showSpinner = extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false); int unreadCount = extras.getInt(Notification.EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT); - // convert MessagingStyle.Message to MessagingMessage, re-using ones from a previous binding - // if they exist final List<MessagingMessage> newMessagingMessages = - createMessages(newMessages, /* isHistoric= */false, - /* usePrecomputedText= */false); + createMessages(newMessages, /* isHistoric= */false, usePrecomputedText); final List<MessagingMessage> newHistoricMessagingMessages = - createMessages(newHistoricMessages, /* isHistoric= */true, - /* usePrecomputedText= */false); - // bind it, baby - bindViews(user, showSpinner, unreadCount, - newMessagingMessages, - newHistoricMessagingMessages); + createMessages(newHistoricMessages, /* isHistoric= */true, usePrecomputedText); + + return new MessagingData(user, showSpinner, unreadCount, + newHistoricMessagingMessages, newMessagingMessages); } /** @@ -430,7 +432,33 @@ public class ConversationLayout extends FrameLayout */ @NonNull public Runnable setDataAsync(Bundle extras) { - return () -> setData(extras); + if (!mPrecomputedTextEnabled) { + return () -> setData(extras); + } + + final MessagingData messagingData = + parseMessagingData(extras, /* usePrecomputedText= */ true); + + return () -> { + finalizeInflate(messagingData.getHistoricMessagingMessages()); + finalizeInflate(messagingData.getNewMessagingMessages()); + + bind(messagingData); + }; + } + + /** + * enable/disable precomputed text usage + * @hide + */ + public void setPrecomputedTextEnabled(boolean precomputedTextEnabled) { + mPrecomputedTextEnabled = precomputedTextEnabled; + } + + private void finalizeInflate(List<MessagingMessage> historicMessagingMessages) { + for (MessagingMessage messagingMessage : historicMessagingMessages) { + messagingMessage.finalizeInflate(); + } } @Override @@ -460,17 +488,12 @@ public class ConversationLayout extends FrameLayout } } + private void bind(MessagingData messagingData) { + setUser(messagingData.getUser()); + setUnreadCount(messagingData.getUnreadCount()); - private void bindViews(Person user, - boolean showSpinner, int unreadCount, List<MessagingMessage> newMessagingMessages, - List<MessagingMessage> newHistoricMessagingMessages) { - setUser(user); - setUnreadCount(unreadCount); - bind(showSpinner, newMessagingMessages, newHistoricMessagingMessages); - } - - private void bind(boolean showSpinner, List<MessagingMessage> messages, - List<MessagingMessage> historicMessages) { + List<MessagingMessage> messages = messagingData.getNewMessagingMessages(); + List<MessagingMessage> historicMessages = messagingData.getHistoricMessagingMessages(); // Copy our groups, before they get clobbered ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups); @@ -483,7 +506,7 @@ public class ConversationLayout extends FrameLayout // Let's now create the views and reorder them accordingly // side-effect: updates mGroups, mAddedGroups - createGroupViews(groups, senders, showSpinner); + createGroupViews(groups, senders, messagingData.getShowSpinner()); // Let's first check which groups were removed altogether and remove them in one animation removeGroups(oldGroups); @@ -585,7 +608,7 @@ public class ConversationLayout extends FrameLayout // When collapsed, we're displaying the image message in a dedicated container // on the right of the layout instead of inline. Let's add the isolated image there - MessagingGroup messagingGroup = mGroups.get(mGroups.size() -1); + MessagingGroup messagingGroup = mGroups.get(mGroups.size() - 1); MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage(); if (isolatedMessage != null) { newMessage = isolatedMessage.getView(); @@ -1042,7 +1065,7 @@ public class ConversationLayout extends FrameLayout } if (visibleChildren > 0 && group.getVisibility() == GONE) { group.setVisibility(VISIBLE); - } else if (visibleChildren == 0 && group.getVisibility() != GONE) { + } else if (visibleChildren == 0 && group.getVisibility() != GONE) { group.setVisibility(GONE); } } @@ -1259,7 +1282,7 @@ public class ConversationLayout extends FrameLayout public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); - for (TouchDelegate delegate: mDelegates) { + for (TouchDelegate delegate : mDelegates) { event.setLocation(x, y); if (delegate.onTouchEvent(event)) { return true; diff --git a/core/java/com/android/internal/widget/MessagingData.java b/core/java/com/android/internal/widget/MessagingData.java new file mode 100644 index 000000000000..85b02018e7c7 --- /dev/null +++ b/core/java/com/android/internal/widget/MessagingData.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.app.Person; + +import java.util.List; + +/** + * @hide + */ +final class MessagingData { + private final Person mUser; + private final boolean mShowSpinner; + private final List<MessagingMessage> mHistoricMessagingMessages; + private final List<MessagingMessage> mNewMessagingMessages; + private final int mUnreadCount; + + MessagingData(Person user, boolean showSpinner, + List<MessagingMessage> historicMessagingMessages, + List<MessagingMessage> newMessagingMessages) { + this(user, showSpinner, /* unreadCount= */0, + historicMessagingMessages, newMessagingMessages); + } + + MessagingData(Person user, boolean showSpinner, + int unreadCount, + List<MessagingMessage> historicMessagingMessages, + List<MessagingMessage> newMessagingMessages) { + mUser = user; + mShowSpinner = showSpinner; + mUnreadCount = unreadCount; + mHistoricMessagingMessages = historicMessagingMessages; + mNewMessagingMessages = newMessagingMessages; + } + + public Person getUser() { + return mUser; + } + + public boolean getShowSpinner() { + return mShowSpinner; + } + + public List<MessagingMessage> getHistoricMessagingMessages() { + return mHistoricMessagingMessages; + } + + public List<MessagingMessage> getNewMessagingMessages() { + return mNewMessagingMessages; + } + + public int getUnreadCount() { + return mUnreadCount; + } +} diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java index 83557cd8a719..b6d7503119fe 100644 --- a/core/java/com/android/internal/widget/MessagingLayout.java +++ b/core/java/com/android/internal/widget/MessagingLayout.java @@ -87,7 +87,7 @@ public class MessagingLayout extends FrameLayout private ImageResolver mImageResolver; private CharSequence mConversationTitle; private ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>(); - + private boolean mPrecomputedTextEnabled = false; public MessagingLayout(@NonNull Context context) { super(context); } @@ -162,15 +162,23 @@ public class MessagingLayout extends FrameLayout */ @RemotableViewMethod(asyncImpl = "setDataAsync") public void setData(Bundle extras) { + bind(parseMessagingData(extras, /* usePrecomputedText= */false)); + } + + @NonNull + private MessagingData parseMessagingData(Bundle extras, boolean usePrecomputedText) { Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); - List<Notification.MessagingStyle.Message> newMessages - = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); + List<Notification.MessagingStyle.Message> newMessages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES); - List<Notification.MessagingStyle.Message> newHistoricMessages - = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages); - setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, android.app.Person.class)); - RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[]) - extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, android.app.RemoteInputHistoryItem.class); + List<Notification.MessagingStyle.Message> newHistoricMessages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages); + setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, + Person.class)); + RemoteInputHistoryItem[] history = + (RemoteInputHistoryItem[]) extras.getParcelableArray( + Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, + RemoteInputHistoryItem.class); addRemoteInputHistoryToMessages(newMessages, history); final Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, Person.class); @@ -178,10 +186,12 @@ public class MessagingLayout extends FrameLayout extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false); final List<MessagingMessage> historicMessagingMessages = createMessages(newHistoricMessages, - /* isHistoric= */true, /* usePrecomputedText= */ false); + /* isHistoric= */true, usePrecomputedText); final List<MessagingMessage> newMessagingMessages = - createMessages(newMessages, /* isHistoric= */false, /* usePrecomputedText= */false); - bindViews(user, showSpinner, historicMessagingMessages, newMessagingMessages); + createMessages(newMessages, /* isHistoric */false, usePrecomputedText); + + return new MessagingData(user, showSpinner, + historicMessagingMessages, newMessagingMessages); } /** @@ -193,7 +203,32 @@ public class MessagingLayout extends FrameLayout */ @NonNull public Runnable setDataAsync(Bundle extras) { - return () -> setData(extras); + if (!mPrecomputedTextEnabled) { + return () -> setData(extras); + } + + final MessagingData messagingData = + parseMessagingData(extras, /* usePrecomputedText= */true); + + return () -> { + finalizeInflate(messagingData.getHistoricMessagingMessages()); + finalizeInflate(messagingData.getNewMessagingMessages()); + bind(messagingData); + }; + } + + /** + * enable/disable precomputed text usage + * @hide + */ + public void setPrecomputedTextEnabled(boolean precomputedTextEnabled) { + mPrecomputedTextEnabled = precomputedTextEnabled; + } + + private void finalizeInflate(List<MessagingMessage> historicMessagingMessages) { + for (MessagingMessage messagingMessage: historicMessagingMessages) { + messagingMessage.finalizeInflate(); + } } @Override @@ -218,17 +253,13 @@ public class MessagingLayout extends FrameLayout } } - private void bindViews(Person user, boolean showSpinner, - List<MessagingMessage> historicMessagingMessages, - List<MessagingMessage> newMessagingMessages) { - setUser(user); - bind(showSpinner, historicMessagingMessages, newMessagingMessages); - } + private void bind(MessagingData messagingData) { + setUser(messagingData.getUser()); - private void bind(boolean showSpinner, List<MessagingMessage> historicMessages, - List<MessagingMessage> messages) { + List<MessagingMessage> historicMessages = messagingData.getHistoricMessagingMessages(); + List<MessagingMessage> messages = messagingData.getNewMessagingMessages(); ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups); - addMessagesToGroups(historicMessages, messages, showSpinner); + addMessagesToGroups(historicMessages, messages, messagingData.getShowSpinner()); // Let's first check which groups were removed altogether and remove them in one animation removeGroups(oldGroups); diff --git a/core/java/com/android/internal/widget/NotificationDrawableConsumer.java b/core/java/com/android/internal/widget/NotificationDrawableConsumer.java new file mode 100644 index 000000000000..7c4d92968afb --- /dev/null +++ b/core/java/com/android/internal/widget/NotificationDrawableConsumer.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.graphics.drawable.Drawable; + +import androidx.annotation.Nullable; + +/** + * An interface for the class, who will use {@link NotificationIconManager} to load icons. + */ +public interface NotificationDrawableConsumer { + + /** + * Sets a drawable as the content of this consumer. + * + * @param drawable the {@link Drawable} to set, or {@code null} to clear the content + */ + void setImageDrawable(@Nullable Drawable drawable); +} diff --git a/core/java/com/android/internal/widget/NotificationIconManager.java b/core/java/com/android/internal/widget/NotificationIconManager.java new file mode 100644 index 000000000000..221845cbbfd9 --- /dev/null +++ b/core/java/com/android/internal/widget/NotificationIconManager.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.graphics.drawable.Icon; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * An interface used for Notification views to delegate handling the loading of icons. + */ +public interface NotificationIconManager { + + /** + * Called when a new icon is provided to display. + * + * @param drawableConsumer a consumer, which can display the loaded drawable. + * @param icon the updated icon to be displayed. + * + * @return a {@link Runnable} that sets the drawable on the consumer + */ + @NonNull + Runnable updateIcon( + @NonNull NotificationDrawableConsumer drawableConsumer, + @Nullable Icon icon + ); +} diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index 9aa992bae72c..b5d70d379e0c 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -353,7 +353,7 @@ status_t AndroidRuntime::callMain(const String8& className, jclass clazz, JNIEnv* env; jmethodID methodId; - ALOGD("Calling main entry %s", className.string()); + ALOGD("Calling main entry %s", className.c_str()); env = getJNIEnv(); if (clazz == NULL || env == NULL) { @@ -362,7 +362,7 @@ status_t AndroidRuntime::callMain(const String8& className, jclass clazz, methodId = env->GetStaticMethodID(clazz, "main", "([Ljava/lang/String;)V"); if (methodId == NULL) { - ALOGE("ERROR: could not find method %s.main(String[])\n", className.string()); + ALOGE("ERROR: could not find method %s.main(String[])\n", className.c_str()); return UNKNOWN_ERROR; } @@ -378,7 +378,7 @@ status_t AndroidRuntime::callMain(const String8& className, jclass clazz, strArray = env->NewObjectArray(numArgs, stringClass, NULL); for (size_t i = 0; i < numArgs; i++) { - jstring argStr = env->NewStringUTF(args[i].string()); + jstring argStr = env->NewStringUTF(args[i].c_str()); env->SetObjectArrayElement(strArray, i, argStr); } @@ -1269,7 +1269,7 @@ void AndroidRuntime::start(const char* className, const Vector<String8>& options env->SetObjectArrayElement(strArray, 0, classNameStr); for (size_t i = 0; i < options.size(); ++i) { - jstring optionsStr = env->NewStringUTF(options.itemAt(i).string()); + jstring optionsStr = env->NewStringUTF(options.itemAt(i).c_str()); assert(optionsStr != NULL); env->SetObjectArrayElement(strArray, i + 1, optionsStr); } diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp index 624bd5f4da23..69fc515444b2 100644 --- a/core/jni/android_view_DisplayEventReceiver.cpp +++ b/core/jni/android_view_DisplayEventReceiver.cpp @@ -292,7 +292,7 @@ static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject if (status) { String8 message; message.appendFormat("Failed to initialize display event receiver. status=%d", status); - jniThrowRuntimeException(env, message.string()); + jniThrowRuntimeException(env, message.c_str()); return 0; } @@ -316,7 +316,7 @@ static void nativeScheduleVsync(JNIEnv* env, jclass clazz, jlong receiverPtr) { if (status) { String8 message; message.appendFormat("Failed to schedule next vertical sync pulse. status=%d", status); - jniThrowRuntimeException(env, message.string()); + jniThrowRuntimeException(env, message.c_str()); } } diff --git a/core/jni/android_view_InputEventSender.cpp b/core/jni/android_view_InputEventSender.cpp index 061f66958797..833952def02b 100644 --- a/core/jni/android_view_InputEventSender.cpp +++ b/core/jni/android_view_InputEventSender.cpp @@ -340,7 +340,7 @@ static jlong nativeInit(JNIEnv* env, jclass clazz, jobject senderWeak, if (status) { String8 message; message.appendFormat("Failed to initialize input event sender. status=%d", status); - jniThrowRuntimeException(env, message.string()); + jniThrowRuntimeException(env, message.c_str()); return 0; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index e129f7d22249..f55501ab1a8f 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7790,8 +7790,9 @@ android:process=":ui"> </activity> <activity android:name="com.android.internal.app.PlatLogoActivity" - android:theme="@style/Theme.Wallpaper.NoTitleBar.Fullscreen" + android:theme="@style/Theme.NoTitleBar.Fullscreen" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" + android:enableOnBackInvokedCallback="true" android:icon="@drawable/platlogo" android:process=":ui"> </activity> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 2f0b390d92c2..e0b656565ce1 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -542,6 +542,9 @@ <bool name="config_goToSleepOnButtonPressTheaterMode">true</bool> <!-- If this is true, long press on power button will be available from the non-interactive state --> <bool name="config_supportLongPressPowerWhenNonInteractive">false</bool> + <!-- If this is true, short press on power button will be available whenever the default display + is on even if the device is non-interactive (dreaming). --> + <bool name="config_supportShortPressPowerWhenDefaultDisplayOn">false</bool> <!-- If this is true, then keep dreaming when unplugging. This config was formerly known as config_keepDreamingWhenUndocking. diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index f1a78a6a1e7d..4918bbefa900 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1968,6 +1968,7 @@ <java-symbol type="integer" name="config_keyguardDrawnTimeout" /> <java-symbol type="bool" name="config_goToSleepOnButtonPressTheaterMode" /> <java-symbol type="bool" name="config_supportLongPressPowerWhenNonInteractive" /> + <java-symbol type="bool" name="config_supportShortPressPowerWhenDefaultDisplayOn" /> <java-symbol type="bool" name="config_wimaxEnabled" /> <java-symbol type="bool" name="show_ongoing_ime_switcher" /> <java-symbol type="color" name="config_defaultNotificationColor" /> diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index 31755efb88ed..a358c4f6f7e9 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -1749,6 +1749,15 @@ <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> </intent-filter> </activity> + + <activity android:name="android.view.ViewGroupTestActivity" + android:label="ViewGroup Test" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> + </intent-filter> + </activity> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/core/tests/coretests/res/layout/viewgroup_test.xml b/core/tests/coretests/res/layout/viewgroup_test.xml new file mode 100644 index 000000000000..04f4f5228b06 --- /dev/null +++ b/core/tests/coretests/res/layout/viewgroup_test.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- Demonstrates adding/removing views from ViewGroup. See corresponding Java code. --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/linear_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <EditText + android:id="@+id/view" + android:layout_width="20dp" + android:layout_height="10dp" + android:text="Hello World!" + android:background="#2F00FF00" /> + <EditText + android:id="@+id/view_scale" + android:layout_width="20dp" + android:layout_height="10dp" + android:scaleX="0.5" + android:scaleY="2" + android:transformPivotX="0dp" + android:transformPivotY="0dp" + android:text="Hello World!" + android:background="#2F00FF00" /> + <EditText + android:id="@+id/view_translate" + android:layout_width="20dp" + android:layout_height="10dp" + android:translationX="10dp" + android:translationY="20dp" + android:text="Hello World!" + android:background="#2F00FF00" /> + <FrameLayout + android:layout_width="20dp" + android:layout_height="10dp"> + <EditText + android:id="@+id/view_overlap_bottom" + android:layout_width="20dp" + android:layout_height="10dp" + android:text="Hello World!"/> + <Button + android:id="@+id/view_overlap_top" + android:layout_width="10dp" + android:layout_height="10dp"/> + </FrameLayout> + + <FrameLayout + android:layout_width="20dp" + android:layout_height="10dp"> + <EditText + android:id="@+id/view_cover_bottom" + android:layout_width="10dp" + android:layout_height="10dp" + android:text="Hello World!"/> + <Button + android:id="@+id/view_cover_top" + android:layout_width="10dp" + android:layout_height="10dp"/> + </FrameLayout> + +</LinearLayout> diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index a936ceafe5f3..f9377fc480bd 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -19,6 +19,8 @@ package android.app; import static android.app.Notification.CarExtender.UnreadConversation.KEY_ON_READ; import static android.app.Notification.CarExtender.UnreadConversation.KEY_ON_REPLY; import static android.app.Notification.CarExtender.UnreadConversation.KEY_REMOTE_INPUT; +import static android.app.Notification.DEFAULT_SOUND; +import static android.app.Notification.DEFAULT_VIBRATE; import static android.app.Notification.EXTRA_ANSWER_INTENT; import static android.app.Notification.EXTRA_BUILDER_APPLICATION_INFO; import static android.app.Notification.EXTRA_CALL_PERSON; @@ -35,6 +37,9 @@ import static android.app.Notification.EXTRA_PICTURE; import static android.app.Notification.EXTRA_PICTURE_ICON; import static android.app.Notification.EXTRA_SUMMARY_TEXT; import static android.app.Notification.EXTRA_TITLE; +import static android.app.Notification.GROUP_ALERT_CHILDREN; +import static android.app.Notification.GROUP_ALERT_SUMMARY; +import static android.app.Notification.GROUP_KEY_SILENT; import static android.app.Notification.MessagingStyle.Message.KEY_DATA_URI; import static android.app.Notification.MessagingStyle.Message.KEY_SENDER_PERSON; import static android.app.Notification.MessagingStyle.Message.KEY_TEXT; @@ -511,6 +516,75 @@ public class NotificationTest { } @Test + public void testBuilder_setSilent_summaryBehavior_groupAlertChildren() { + Notification summaryNotif = new Notification.Builder(mContext, "channelId") + .setGroupSummary(true) + .setGroup("groupKey") + .setSilent(true) + .build(); + assertEquals(GROUP_ALERT_CHILDREN, summaryNotif.getGroupAlertBehavior()); + } + + @Test + public void testBuilder_setSilent_childBehavior_groupAlertSummary() { + Notification childNotif = new Notification.Builder(mContext, "channelId") + .setGroupSummary(false) + .setGroup("groupKey") + .setSilent(true) + .build(); + assertEquals(GROUP_ALERT_SUMMARY, childNotif.getGroupAlertBehavior()); + } + + @Test + public void testBuilder_setSilent_emptyGroupKey_groupKeySilent() { + Notification emptyGroupKeyNotif = new Notification.Builder(mContext, "channelId") + .setGroup("") + .setSilent(true) + .build(); + assertEquals(GROUP_KEY_SILENT, emptyGroupKeyNotif.getGroup()); + } + + @Test + public void testBuilder_setSilent_vibrateNull() { + Notification silentNotif = new Notification.Builder(mContext, "channelId") + .setGroup("") + .setSilent(true) + .build(); + + assertNull(silentNotif.vibrate); + } + + @Test + public void testBuilder_setSilent_soundNull() { + Notification silentNotif = new Notification.Builder(mContext, "channelId") + .setGroup("") + .setSilent(true) + .build(); + + assertNull(silentNotif.sound); + } + + @Test + public void testBuilder_setSilent_noDefaultSound() { + Notification silentNotif = new Notification.Builder(mContext, "channelId") + .setGroup("") + .setSilent(true) + .build(); + + assertEquals(0, (silentNotif.defaults & DEFAULT_SOUND)); + } + + @Test + public void testBuilder_setSilent_noDefaultVibrate() { + Notification silentNotif = new Notification.Builder(mContext, "channelId") + .setGroup("") + .setSilent(true) + .build(); + + assertEquals(0, (silentNotif.defaults & DEFAULT_VIBRATE)); + } + + @Test public void testCallStyle_getSystemActions_forIncomingCall() { PendingIntent answerIntent = createPendingIntent("answer"); PendingIntent declineIntent = createPendingIntent("decline"); diff --git a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java index a84ac55f0d5a..55ded9c8813d 100644 --- a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java +++ b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java @@ -136,7 +136,11 @@ public class NotificationRankingUpdateTest { NotificationListenerService.RankingMap retrievedRankings = retrievedRankingUpdate.getRankingMap(); assertNotNull(retrievedRankings); - assertTrue(retrievedRankingUpdate.isFdNotNullAndClosed()); + // The rankingUpdate file descriptor is only non-null in the new path. + if (SystemUiSystemPropertiesFlags.getResolver().isEnabled( + SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM)) { + assertTrue(retrievedRankingUpdate.isFdNotNullAndClosed()); + } NotificationListenerService.Ranking retrievedRanking = new NotificationListenerService.Ranking(); assertTrue(retrievedRankings.getRanking(TEST_KEY, retrievedRanking)); diff --git a/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java b/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java new file mode 100644 index 000000000000..60a0a2adbbbe --- /dev/null +++ b/core/tests/coretests/src/android/view/ViewGroupGetChildLocalHitRegionTest.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static com.google.common.truth.Truth.assertThat; + + +import android.graphics.Matrix; +import android.graphics.Region; +import android.platform.test.annotations.Presubmit; +import android.widget.LinearLayout; + +import androidx.test.ext.junit.rules.ActivityScenarioRule; +import androidx.test.filters.SmallTest; + +import com.android.frameworks.coretests.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +/** + * Test basic functions of ViewGroup. + * + * Build/Install/Run: + * atest FrameworksCoreTests:ViewGroupTest + */ +@Presubmit +@SmallTest +public class ViewGroupGetChildLocalHitRegionTest { + @Rule + public ActivityScenarioRule<ViewGroupTestActivity> mScenarioRule = + new ActivityScenarioRule<>(ViewGroupTestActivity.class); + + private LinearLayout mRoot; + private final int[] mRootLocation = new int[2]; + + @Before + public void setup() { + mScenarioRule.getScenario().onActivity(activity -> { + mRoot = activity.findViewById(R.id.linear_layout); + mRoot.getLocationInWindow(mRootLocation); + }); + } + + @Test + public void testGetChildLocalHitRegion() { + assertGetChildLocalHitRegion(R.id.view); + } + + @Test + public void testGetChildLocalHitRegion_withScale() { + assertGetChildLocalHitRegion(R.id.view_scale); + } + + @Test + public void testGetChildLocalHitRegion_withTranslate() { + assertGetChildLocalHitRegion(R.id.view_translate); + } + + @Test + public void testGetChildLocalHitRegion_overlap_noMotionEvent() { + assertGetChildLocalHitRegion(R.id.view_overlap_bottom); + } + @Test + public void testGetChildLocalHitRegion_overlap_withMotionEvent() { + // In this case, view_cover_bottom is partially covered by the view_cover_top. + // The returned region is the bounds of the bottom view subtract the bounds of the top view. + assertGetChildLocalHitRegion(R.id.view_overlap_top, R.id.view_overlap_bottom); + } + + @Test + public void testGetChildLocalHitRegion_cover_withMotionEvent() { + // In this case, view_cover_bottom is completely covered by the view_cover_top. + // The returned region is expected to be empty. + assertGetChildLocalHitRegionEmpty(R.id.view_cover_top, R.id.view_cover_bottom); + } + + private void injectMotionEvent(View view, boolean isHover) { + int[] location = new int[2]; + view.getLocationInWindow(location); + + float x = location[0] + view.getWidth() / 2f; + float y = location[1] + view.getHeight() / 2f; + + int action = isHover ? MotionEvent.ACTION_HOVER_ENTER : MotionEvent.ACTION_DOWN; + MotionEvent motionEvent = MotionEvent.obtain(/* downtime= */ 0, /* eventTime= */ 0, action, + x, y, /* pressure= */ 0, /* size= */ 0, /* metaState= */ 0, + /* xPrecision= */ 1, /* yPrecision= */ 1, /* deviceId= */0, /* edgeFlags= */0); + + View rootView = view.getRootView(); + rootView.dispatchPointerEvent(motionEvent); + } + + private void assertGetChildLocalHitRegion(int viewId) { + assertGetChildLocalHitRegion(viewId, /* isHover= */ true); + assertGetChildLocalHitRegion(viewId, /* isHover= */ false); + } + + /** + * Assert ViewParent#getChildLocalHitRegion for a single view. + * @param viewId the viewId of the tested view. + * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit + * region of the touch events. + */ + private void assertGetChildLocalHitRegion(int viewId, boolean isHover) { + mScenarioRule.getScenario().onActivity(activity -> { + View view = activity.findViewById(viewId); + + Matrix actualMatrix = new Matrix(); + Region actualRegion = new Region(0, 0, view.getWidth(), view.getHeight()); + boolean actualNotEmpty = view.getParent() + .getChildLocalHitRegion(view, actualRegion, actualMatrix, isHover); + + int[] windowLocation = new int[2]; + view.getLocationInWindow(windowLocation); + Matrix expectMatrix = new Matrix(); + expectMatrix.preScale(1 / view.getScaleX(), 1 / view.getScaleY()); + expectMatrix.preTranslate(-windowLocation[0], -windowLocation[1]); + + Region expectRegion = new Region(0, 0, view.getWidth(), view.getHeight()); + + assertThat(actualNotEmpty).isTrue(); + assertThat(actualMatrix).isEqualTo(expectMatrix); + assertThat(actualRegion).isEqualTo(expectRegion); + }); + } + + private void assertGetChildLocalHitRegion(int viewIdTop, int viewIdBottom) { + assertGetChildLocalHitRegion(viewIdTop, viewIdBottom, /* isHover= */ true); + assertGetChildLocalHitRegion(viewIdTop, viewIdBottom, /* isHover= */ false); + } + + /** + * Assert ViewParent#getChildLocalHitRegion of a view that is covered by another view. It will + * inject {@link MotionEvent}s to the view on top first and then get the hit region of the + * bottom view. + * + * @param viewIdTop the view id of the test view on top. + * @param viewIdBottom the view id of the test view at the bottom. + * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit + * region of the touch events. + */ + private void assertGetChildLocalHitRegion(int viewIdTop, int viewIdBottom, boolean isHover) { + mScenarioRule.getScenario().onActivity(activity -> { + View viewTop = activity.findViewById(viewIdTop); + View viewBottom = activity.findViewById(viewIdBottom); + + injectMotionEvent(viewTop, isHover); + + Matrix actualMatrix = new Matrix(); + Region actualRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight()); + boolean actualNotEmpty = viewBottom.getParent() + .getChildLocalHitRegion(viewBottom, actualRegion, actualMatrix, isHover); + + int[] windowLocation = new int[2]; + viewBottom.getLocationInWindow(windowLocation); + Matrix expectMatrix = new Matrix(); + expectMatrix.preTranslate(-windowLocation[0], -windowLocation[1]); + + Region expectRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight()); + expectRegion.op(0, 0, viewTop.getWidth(), viewTop.getHeight(), Region.Op.DIFFERENCE); + + assertThat(actualNotEmpty).isTrue(); + assertThat(actualMatrix).isEqualTo(expectMatrix); + assertThat(actualRegion).isEqualTo(expectRegion); + }); + } + + private void assertGetChildLocalHitRegionEmpty(int viewIdTop, int viewIdBottom) { + assertGetChildLocalHitRegionEmpty(viewIdTop, viewIdBottom, /* isHover= */ true); + assertGetChildLocalHitRegionEmpty(viewIdTop, viewIdBottom, /* isHover= */ false); + } + + /** + * Assert ViewParent#getChildLocalHitRegion returns an empty region for a view that is + * completely covered by another view. It will inject {@link MotionEvent}s to the view on top + * first and then get the hit region of the + * bottom view. + * + * @param viewIdTop the view id of the test view on top. + * @param viewIdBottom the view id of the test view at the bottom. + * @param isHover if true, check the hit region of the hover events. Otherwise, check the hit + * region of the touch events. + */ + private void assertGetChildLocalHitRegionEmpty(int viewIdTop, int viewIdBottom, + boolean isHover) { + mScenarioRule.getScenario().onActivity(activity -> { + View viewTop = activity.findViewById(viewIdTop); + View viewBottom = activity.findViewById(viewIdBottom); + + injectMotionEvent(viewTop, isHover); + + Region actualRegion = new Region(0, 0, viewBottom.getWidth(), viewBottom.getHeight()); + boolean actualNotEmpty = viewBottom.getParent() + .getChildLocalHitRegion(viewBottom, actualRegion, new Matrix(), isHover); + + assertThat(actualNotEmpty).isFalse(); + assertThat(actualRegion.isEmpty()).isTrue(); + }); + } +} diff --git a/core/tests/coretests/src/android/view/ViewGroupTestActivity.java b/core/tests/coretests/src/android/view/ViewGroupTestActivity.java new file mode 100644 index 000000000000..b94bda5efba4 --- /dev/null +++ b/core/tests/coretests/src/android/view/ViewGroupTestActivity.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.annotation.Nullable; +import android.app.Activity; +import android.os.Bundle; + +import com.android.frameworks.coretests.R; + +public class ViewGroupTestActivity extends Activity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.viewgroup_test); + } +} diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java index 388a996d8b1c..b4c72ca3226b 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java @@ -21,7 +21,9 @@ import static org.mockito.Mockito.when; import android.app.Instrumentation; import android.content.Context; +import android.graphics.Matrix; import android.graphics.Rect; +import android.graphics.Region; import android.view.View; import android.view.ViewGroup; @@ -45,7 +47,7 @@ public class HandwritingTestUtil { float handwritingBoundsOffsetRight, float handwritingBoundsOffsetBottom) { final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); final Context context = instrumentation.getTargetContext(); - // mock a parent so that HandwritingInitiator can get + // mock a parent so that HandwritingInitiator can get visible rect and hit region. final ViewGroup parent = new ViewGroup(context) { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { @@ -56,6 +58,14 @@ public class HandwritingTestUtil { r.set(handwritingArea); return true; } + + @Override + public boolean getChildLocalHitRegion(View child, Region region, Matrix matrix, + boolean isHover) { + matrix.reset(); + region.set(handwritingArea); + return true; + } }; View view = spy(new View(context)); diff --git a/core/tests/vibrator/src/android/os/VibratorInfoTest.java b/core/tests/vibrator/src/android/os/VibratorInfoTest.java index 808c4ece9435..73cd4647415d 100644 --- a/core/tests/vibrator/src/android/os/VibratorInfoTest.java +++ b/core/tests/vibrator/src/android/os/VibratorInfoTest.java @@ -257,8 +257,13 @@ public class VibratorInfoTest { @Test public void testEquals() { - VibratorInfo.Builder completeBuilder = new VibratorInfo.Builder(TEST_VIBRATOR_ID) - .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) + VibratorInfo.Builder completeBuilder = new VibratorInfo.Builder(TEST_VIBRATOR_ID); + // Create a builder with a different ID, but same properties the same as the first one. + VibratorInfo.Builder completeBuilder2 = new VibratorInfo.Builder(TEST_VIBRATOR_ID + 2); + + for (VibratorInfo.Builder builder : + new VibratorInfo.Builder[] {completeBuilder, completeBuilder2}) { + builder.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) .setSupportedEffects(VibrationEffect.EFFECT_CLICK) .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20) .setPrimitiveDelayMax(100) @@ -268,31 +273,43 @@ public class VibratorInfoTest { .setPwleSizeMax(20) .setQFactor(2f) .setFrequencyProfile(TEST_FREQUENCY_PROFILE); + } VibratorInfo complete = completeBuilder.build(); assertEquals(complete, complete); + assertTrue(complete.equalContent(complete)); assertEquals(complete, completeBuilder.build()); + assertTrue(complete.equalContent(completeBuilder.build())); assertEquals(complete.hashCode(), completeBuilder.build().hashCode()); + // The infos from the two builders should have equal content, but should not be equal due to + // their different IDs. + assertNotEquals(complete, completeBuilder2.build()); + assertTrue(complete.equalContent(completeBuilder2.build())); + VibratorInfo completeWithComposeControl = completeBuilder .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) .build(); assertNotEquals(complete, completeWithComposeControl); + assertFalse(complete.equalContent(completeWithComposeControl)); VibratorInfo completeWithNoEffects = completeBuilder .setSupportedEffects(new int[0]) .build(); assertNotEquals(complete, completeWithNoEffects); + assertFalse(complete.equalContent(completeWithNoEffects)); VibratorInfo completeWithUnknownEffects = completeBuilder .setSupportedEffects(null) .build(); assertNotEquals(complete, completeWithUnknownEffects); + assertFalse(complete.equalContent(completeWithUnknownEffects)); VibratorInfo completeWithDifferentPrimitiveDuration = completeBuilder .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) .build(); assertNotEquals(complete, completeWithDifferentPrimitiveDuration); + assertFalse(complete.equalContent(completeWithDifferentPrimitiveDuration)); VibratorInfo completeWithDifferentFrequencyProfile = completeBuilder .setFrequencyProfile(new VibratorInfo.FrequencyProfile( @@ -302,31 +319,37 @@ public class VibratorInfoTest { TEST_AMPLITUDE_MAP)) .build(); assertNotEquals(complete, completeWithDifferentFrequencyProfile); + assertFalse(complete.equalContent(completeWithDifferentFrequencyProfile)); VibratorInfo completeWithEmptyFrequencyProfile = completeBuilder .setFrequencyProfile(EMPTY_FREQUENCY_PROFILE) .build(); assertNotEquals(complete, completeWithEmptyFrequencyProfile); + assertFalse(complete.equalContent(completeWithEmptyFrequencyProfile)); VibratorInfo completeWithUnknownQFactor = completeBuilder.setQFactor(Float.NaN).build(); assertNotEquals(complete, completeWithUnknownQFactor); + assertFalse(complete.equalContent(completeWithUnknownQFactor)); VibratorInfo completeWithDifferentQFactor = completeBuilder .setQFactor(complete.getQFactor() + 3f) .build(); assertNotEquals(complete, completeWithDifferentQFactor); + assertFalse(complete.equalContent(completeWithDifferentQFactor)); VibratorInfo unknownEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build(); VibratorInfo knownEmptyEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID) .setSupportedEffects(new int[0]) .build(); assertNotEquals(unknownEffectSupport, knownEmptyEffectSupport); + assertFalse(unknownEffectSupport.equalContent(knownEmptyEffectSupport)); VibratorInfo unknownBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build(); VibratorInfo knownEmptyBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID) .setSupportedBraking(new int[0]) .build(); assertNotEquals(unknownBrakingSupport, knownEmptyBrakingSupport); + assertFalse(unknownBrakingSupport.equalContent(knownEmptyBrakingSupport)); } @Test diff --git a/core/tests/vibrator/src/android/os/VibratorTest.java b/core/tests/vibrator/src/android/os/VibratorTest.java index 8141ca4b22a8..cfa12bb5b504 100644 --- a/core/tests/vibrator/src/android/os/VibratorTest.java +++ b/core/tests/vibrator/src/android/os/VibratorTest.java @@ -37,7 +37,6 @@ import static org.mockito.Mockito.when; import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; -import android.hardware.vibrator.IVibrator; import android.media.AudioAttributes; import android.os.test.TestLooper; @@ -60,8 +59,6 @@ public class VibratorTest { @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); - private static final float TEST_TOLERANCE = 1e-5f; - private Context mContextSpy; private Vibrator mVibratorSpy; private TestLooper mTestLooper; @@ -79,9 +76,6 @@ public class VibratorTest { @Test public void getId_returnsDefaultId() { assertEquals(-1, mVibratorSpy.getId()); - assertEquals(-1, new SystemVibrator.NoVibratorInfo().getId()); - assertEquals(-1, new SystemVibrator.MultiVibratorInfo(new VibratorInfo[] { - VibratorInfo.EMPTY_VIBRATOR_INFO, VibratorInfo.EMPTY_VIBRATOR_INFO }).getId()); } @Test @@ -95,53 +89,6 @@ public class VibratorTest { } @Test - public void areEffectsSupported_noVibrator_returnsAlwaysNo() { - VibratorInfo info = new SystemVibrator.NoVibratorInfo(); - assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_NO, - info.isEffectSupported(VibrationEffect.EFFECT_CLICK)); - } - - @Test - public void areEffectsSupported_unsupportedInOneVibrator_returnsNo() { - VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1) - .setSupportedEffects(VibrationEffect.EFFECT_CLICK) - .build(); - VibratorInfo unsupportedVibrator = new VibratorInfo.Builder(/* id= */ 2) - .setSupportedEffects(new int[0]) - .build(); - VibratorInfo info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{supportedVibrator, unsupportedVibrator}); - assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_NO, - info.isEffectSupported(VibrationEffect.EFFECT_CLICK)); - } - - @Test - public void areEffectsSupported_unknownInOneVibrator_returnsUnknown() { - VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1) - .setSupportedEffects(VibrationEffect.EFFECT_CLICK) - .build(); - VibratorInfo unknownSupportVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO; - VibratorInfo info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{supportedVibrator, unknownSupportVibrator}); - assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_UNKNOWN, - info.isEffectSupported(VibrationEffect.EFFECT_CLICK)); - } - - @Test - public void arePrimitivesSupported_supportedInAllVibrators_returnsYes() { - VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1) - .setSupportedEffects(VibrationEffect.EFFECT_CLICK) - .build(); - VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2) - .setSupportedEffects(VibrationEffect.EFFECT_CLICK) - .build(); - VibratorInfo info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, secondVibrator}); - assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_YES, - info.isEffectSupported(VibrationEffect.EFFECT_CLICK)); - } - - @Test public void arePrimitivesSupported_returnsArrayOfSameSize() { assertEquals(0, mVibratorSpy.arePrimitivesSupported(new int[0]).length); assertEquals(1, mVibratorSpy.arePrimitivesSupported( @@ -152,39 +99,6 @@ public class VibratorTest { } @Test - public void arePrimitivesSupported_noVibrator_returnsAlwaysFalse() { - VibratorInfo info = new SystemVibrator.NoVibratorInfo(); - assertFalse(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK)); - } - - @Test - public void arePrimitivesSupported_unsupportedInOneVibrator_returnsFalse() { - VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) - .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) - .build(); - VibratorInfo unsupportedVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO; - VibratorInfo info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{supportedVibrator, unsupportedVibrator}); - assertFalse(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK)); - } - - @Test - public void arePrimitivesSupported_supportedInAllVibrators_returnsTrue() { - VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) - .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 5) - .build(); - VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) - .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 15) - .build(); - VibratorInfo info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, secondVibrator}); - assertTrue(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK)); - } - - @Test public void getPrimitivesDurations_returnsArrayOfSameSize() { assertEquals(0, mVibratorSpy.getPrimitiveDurations(new int[0]).length); assertEquals(1, mVibratorSpy.getPrimitiveDurations( @@ -195,245 +109,6 @@ public class VibratorTest { } @Test - public void getPrimitivesDurations_noVibrator_returnsAlwaysZero() { - VibratorInfo info = new SystemVibrator.NoVibratorInfo(); - assertEquals(0, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK)); - } - - @Test - public void getPrimitivesDurations_unsupportedInOneVibrator_returnsZero() { - VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) - .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) - .build(); - VibratorInfo unsupportedVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO; - VibratorInfo info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{supportedVibrator, unsupportedVibrator}); - assertEquals(0, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK)); - } - - @Test - public void getPrimitivesDurations_supportedInAllVibrators_returnsMaxDuration() { - VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) - .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) - .build(); - VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2) - .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) - .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20) - .build(); - VibratorInfo info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, secondVibrator}); - assertEquals(20, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK)); - } - - @Test - public void getQFactorAndResonantFrequency_noVibrator_returnsNaN() { - VibratorInfo info = new SystemVibrator.NoVibratorInfo(); - - assertTrue(Float.isNaN(info.getQFactor())); - assertTrue(Float.isNaN(info.getResonantFrequencyHz())); - } - - @Test - public void getQFactorAndResonantFrequency_differentValues_returnsNaN() { - VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setQFactor(1f) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null)) - .build(); - VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setQFactor(2f) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 2, 2, null)) - .build(); - VibratorInfo info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, secondVibrator}); - - assertTrue(Float.isNaN(info.getQFactor())); - assertTrue(Float.isNaN(info.getResonantFrequencyHz())); - assertEmptyFrequencyProfileAndControl(info); - - // One vibrator with values undefined. - VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3).build(); - info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, thirdVibrator}); - - assertTrue(Float.isNaN(info.getQFactor())); - assertTrue(Float.isNaN(info.getResonantFrequencyHz())); - assertEmptyFrequencyProfileAndControl(info); - } - - @Test - public void getQFactorAndResonantFrequency_sameValues_returnsValue() { - VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setQFactor(10f) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile( - /* resonantFrequencyHz= */ 11, 10, 0.5f, null)) - .build(); - VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setQFactor(10f) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile( - /* resonantFrequencyHz= */ 11, 5, 1, null)) - .build(); - VibratorInfo info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, secondVibrator}); - - assertEquals(10f, info.getQFactor(), TEST_TOLERANCE); - assertEquals(11f, info.getResonantFrequencyHz(), TEST_TOLERANCE); - - // No frequency range defined. - assertTrue(info.getFrequencyProfile().isEmpty()); - assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)); - } - - @Test - public void getFrequencyProfile_noVibrator_returnsEmpty() { - VibratorInfo info = new SystemVibrator.NoVibratorInfo(); - - assertEmptyFrequencyProfileAndControl(info); - } - - @Test - public void getFrequencyProfile_differentResonantFrequencyOrResolutionValues_returnsEmpty() { - VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, - new float[] { 0, 1 })) - .build(); - VibratorInfo differentResonantFrequency = new VibratorInfo.Builder(/* id= */ 2) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 1, 1, - new float[] { 0, 1 })) - .build(); - VibratorInfo info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, differentResonantFrequency}); - - assertEmptyFrequencyProfileAndControl(info); - - VibratorInfo differentFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 2, - new float[] { 0, 1 })) - .build(); - info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, differentFrequencyResolution}); - - assertEmptyFrequencyProfileAndControl(info); - } - - @Test - public void getFrequencyProfile_missingValues_returnsEmpty() { - VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, - new float[] { 0, 1 })) - .build(); - VibratorInfo missingResonantFrequency = new VibratorInfo.Builder(/* id= */ 2) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(Float.NaN, 1, 1, - new float[] { 0, 1 })) - .build(); - VibratorInfo info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, missingResonantFrequency}); - - assertEmptyFrequencyProfileAndControl(info); - - VibratorInfo missingMinFrequency = new VibratorInfo.Builder(/* id= */ 2) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, Float.NaN, 1, - new float[] { 0, 1 })) - .build(); - info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, missingMinFrequency}); - - assertEmptyFrequencyProfileAndControl(info); - - VibratorInfo missingFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, Float.NaN, - new float[] { 0, 1 })) - .build(); - info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, missingFrequencyResolution}); - - assertEmptyFrequencyProfileAndControl(info); - - VibratorInfo missingMaxAmplitudes = new VibratorInfo.Builder(/* id= */ 2) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null)) - .build(); - info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, missingMaxAmplitudes}); - - assertEmptyFrequencyProfileAndControl(info); - } - - @Test - public void getFrequencyProfile_unalignedMaxAmplitudes_returnsEmpty() { - VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f, - new float[] { 0, 1, 1, 0 })) - .build(); - VibratorInfo unalignedMinFrequency = new VibratorInfo.Builder(/* id= */ 2) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.1f, 0.5f, - new float[] { 0, 1, 1, 0 })) - .build(); - VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 2) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, - new float[] { 0, 1, 1, 0 })) - .build(); - VibratorInfo info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, unalignedMinFrequency, thirdVibrator}); - - assertEmptyFrequencyProfileAndControl(info); - } - - @Test - public void getFrequencyProfile_alignedProfiles_returnsIntersection() { - VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f, - new float[] { 0.5f, 1, 1, 0.5f })) - .build(); - VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, - new float[] { 1, 1, 1 })) - .build(); - VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3) - .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, - new float[] { 0.8f, 1, 0.8f, 0.5f })) - .build(); - VibratorInfo info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, secondVibrator, thirdVibrator}); - - assertEquals( - new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }), - info.getFrequencyProfile()); - assertEquals(true, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)); - - // Third vibrator without frequency control capability. - thirdVibrator = new VibratorInfo.Builder(/* id= */ 3) - .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, - new float[] { 0.8f, 1, 0.8f, 0.5f })) - .build(); - info = new SystemVibrator.MultiVibratorInfo( - new VibratorInfo[]{firstVibrator, secondVibrator, thirdVibrator}); - - assertEquals( - new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }), - info.getFrequencyProfile()); - assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)); - } - - @Test public void onVibratorStateChanged_noVibrator_registersNoListenerToVibratorManager() { VibratorManager mockVibratorManager = mock(VibratorManager.class); when(mockVibratorManager.getVibratorIds()).thenReturn(new int[0]); @@ -577,12 +252,4 @@ public class VibratorTest { VibrationAttributes vibrationAttributes = captor.getValue(); assertEquals(new VibrationAttributes.Builder().build(), vibrationAttributes); } - - /** - * Asserts that the frequency profile is empty, and therefore frequency control isn't supported. - */ - void assertEmptyFrequencyProfileAndControl(VibratorInfo info) { - assertTrue(info.getFrequencyProfile().isEmpty()); - assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)); - } } diff --git a/core/tests/vibrator/src/android/os/vibrator/MultiVibratorInfoTest.java b/core/tests/vibrator/src/android/os/vibrator/MultiVibratorInfoTest.java new file mode 100644 index 000000000000..fc31ac44b362 --- /dev/null +++ b/core/tests/vibrator/src/android/os/vibrator/MultiVibratorInfoTest.java @@ -0,0 +1,361 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os.vibrator; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; +import static junit.framework.TestCase.assertEquals; + +import android.hardware.vibrator.IVibrator; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.os.VibratorInfo; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class MultiVibratorInfoTest { + private static final float TEST_TOLERANCE = 1e-5f; + + @Test + public void testGetId() { + VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1) + .setSupportedEffects(VibrationEffect.EFFECT_CLICK) + .build(); + VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2) + .setSupportedEffects(new int[0]) + .build(); + + VibratorInfo info = new MultiVibratorInfo(/* id= */ 3, + new VibratorInfo[]{firstInfo, secondInfo}); + + assertEquals(3, info.getId()); + } + + @Test + public void testIsEffectSupported_supportedInAllVibrators_returnsYes() { + VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedEffects(VibrationEffect.EFFECT_CLICK) + .build(); + VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2) + .setSupportedEffects(VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_TICK) + .build(); + + VibratorInfo info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, secondInfo}); + + assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_YES, + info.isEffectSupported(VibrationEffect.EFFECT_CLICK)); + } + + @Test + public void testIsEffectSupported_unsupportedInOneVibrator_returnsNo() { + VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1) + .setSupportedEffects(VibrationEffect.EFFECT_CLICK) + .build(); + VibratorInfo unsupportedVibrator = new VibratorInfo.Builder(/* id= */ 2) + .setSupportedEffects(new int[0]) + .build(); + + VibratorInfo info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{supportedVibrator, unsupportedVibrator}); + + assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_NO, + info.isEffectSupported(VibrationEffect.EFFECT_CLICK)); + } + + @Test + public void testIsEffectSupported_unknownInOneVibrator_returnsUnknown() { + VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1) + .setSupportedEffects(VibrationEffect.EFFECT_CLICK) + .build(); + VibratorInfo unknownSupportVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO; + + VibratorInfo info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{supportedVibrator, unknownSupportVibrator}); + assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_UNKNOWN, + info.isEffectSupported(VibrationEffect.EFFECT_CLICK)); + } + + @Test + public void testIsPrimitiveSupported_unsupportedInOneVibrator_returnsFalse() { + VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .build(); + VibratorInfo unsupportedVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO; + + VibratorInfo info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{supportedVibrator, unsupportedVibrator}); + + assertFalse(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK)); + } + + @Test + public void testIsPrimitiveSupported_supportedInAllVibrators_returnsTrue() { + VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 5) + .build(); + VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 15) + .build(); + + VibratorInfo info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, secondInfo}); + + assertTrue(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK)); + } + + @Test + public void testGetPrimitiveDuration_unsupportedInOneVibrator_returnsZero() { + VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .build(); + VibratorInfo unsupportedVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO; + + VibratorInfo info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{supportedVibrator, unsupportedVibrator}); + + assertEquals(0, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK)); + } + + @Test + public void testGetPrimitiveDuration_supportedInAllVibrators_returnsMaxDuration() { + VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .build(); + VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20) + .build(); + + VibratorInfo info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, secondInfo}); + + assertEquals(20, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK)); + } + + @Test + public void testGetQFactorAndResonantFrequency_differentValues_returnsNaN() { + VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setQFactor(1f) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null)) + .build(); + VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setQFactor(2f) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 2, 2, null)) + .build(); + + VibratorInfo info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, secondInfo}); + + assertTrue(Float.isNaN(info.getQFactor())); + assertTrue(Float.isNaN(info.getResonantFrequencyHz())); + assertEmptyFrequencyProfileAndControl(info); + + // One vibrator with values undefined. + VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3).build(); + info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, thirdVibrator}); + + assertTrue(Float.isNaN(info.getQFactor())); + assertTrue(Float.isNaN(info.getResonantFrequencyHz())); + assertEmptyFrequencyProfileAndControl(info); + } + + @Test + public void testGetQFactorAndResonantFrequency_sameValues_returnsValue() { + VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setQFactor(10f) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile( + /* resonantFrequencyHz= */ 11, 10, 0.5f, null)) + .build(); + VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setQFactor(10f) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile( + /* resonantFrequencyHz= */ 11, 5, 1, null)) + .build(); + + VibratorInfo info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, secondInfo}); + + assertEquals(10f, info.getQFactor(), TEST_TOLERANCE); + assertEquals(11f, info.getResonantFrequencyHz(), TEST_TOLERANCE); + // No frequency range defined. + assertTrue(info.getFrequencyProfile().isEmpty()); + assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)); + } + + @Test + public void testGetFrequencyProfile_differentResonantFrequencyOrResolutions_returnsEmpty() { + VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, + new float[] { 0, 1 })) + .build(); + VibratorInfo differentResonantFrequency = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 1, 1, + new float[] { 0, 1 })) + .build(); + VibratorInfo info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, differentResonantFrequency}); + + assertEmptyFrequencyProfileAndControl(info); + + VibratorInfo differentFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 2, + new float[] { 0, 1 })) + .build(); + info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, differentFrequencyResolution}); + + assertEmptyFrequencyProfileAndControl(info); + } + + @Test + public void testGetFrequencyProfile_missingValues_returnsEmpty() { + VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, + new float[] { 0, 1 })) + .build(); + VibratorInfo missingResonantFrequency = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(Float.NaN, 1, 1, + new float[] { 0, 1 })) + .build(); + + VibratorInfo info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, missingResonantFrequency}); + + assertEmptyFrequencyProfileAndControl(info); + + VibratorInfo missingMinFrequency = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, Float.NaN, 1, + new float[] { 0, 1 })) + .build(); + info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, missingMinFrequency}); + + assertEmptyFrequencyProfileAndControl(info); + + VibratorInfo missingFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, Float.NaN, + new float[] { 0, 1 })) + .build(); + info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, missingFrequencyResolution}); + + assertEmptyFrequencyProfileAndControl(info); + + VibratorInfo missingMaxAmplitudes = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null)) + .build(); + info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, missingMaxAmplitudes}); + + assertEmptyFrequencyProfileAndControl(info); + } + + @Test + public void testGetFrequencyProfile_unalignedMaxAmplitudes_returnsEmpty() { + VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f, + new float[] { 0, 1, 1, 0 })) + .build(); + VibratorInfo unalignedMinFrequency = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.1f, 0.5f, + new float[] { 0, 1, 1, 0 })) + .build(); + VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, + new float[] { 0, 1, 1, 0 })) + .build(); + + VibratorInfo info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, unalignedMinFrequency, thirdVibrator}); + + assertEmptyFrequencyProfileAndControl(info); + } + + @Test + public void testGetFrequencyProfile_alignedProfiles_returnsIntersection() { + VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f, + new float[] { 0.5f, 1, 1, 0.5f })) + .build(); + VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, + new float[] { 1, 1, 1 })) + .build(); + VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, + new float[] { 0.8f, 1, 0.8f, 0.5f })) + .build(); + + VibratorInfo info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, secondInfo, thirdVibrator}); + + assertEquals( + new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }), + info.getFrequencyProfile()); + assertEquals(true, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)); + + // Third vibrator without frequency control capability. + thirdVibrator = new VibratorInfo.Builder(/* id= */ 3) + .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, + new float[] { 0.8f, 1, 0.8f, 0.5f })) + .build(); + info = new MultiVibratorInfo(/* id= */ 1, + new VibratorInfo[]{firstInfo, secondInfo, thirdVibrator}); + + assertEquals( + new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }), + info.getFrequencyProfile()); + assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)); + } + + /** + * Asserts that the frequency profile is empty, and therefore frequency control isn't supported. + */ + private void assertEmptyFrequencyProfileAndControl(VibratorInfo info) { + assertTrue(info.getFrequencyProfile().isEmpty()); + assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)); + } +} diff --git a/core/tests/vibrator/src/android/os/vibrator/VibratorInfoFactoryTest.java b/core/tests/vibrator/src/android/os/vibrator/VibratorInfoFactoryTest.java new file mode 100644 index 000000000000..df4822fc8b04 --- /dev/null +++ b/core/tests/vibrator/src/android/os/vibrator/VibratorInfoFactoryTest.java @@ -0,0 +1,130 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os.vibrator; + +import static junit.framework.Assert.assertTrue; +import static junit.framework.TestCase.assertEquals; + +import android.hardware.vibrator.IVibrator; +import android.os.VibrationEffect; +import android.os.VibratorInfo; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class VibratorInfoFactoryTest { + + @Test + public void testCreatedInfo_hasTheRequestedId() { + // Empty info list. + VibratorInfo infoFromEmptyInfos = + VibratorInfoFactory.create(/* id= */ 3, new VibratorInfo[] {}); + VibratorInfo info1 = new VibratorInfo.Builder(/* id= */ 1) + .setSupportedEffects(VibrationEffect.EFFECT_CLICK) + .build(); + VibratorInfo info2 = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .build(); + VibratorInfo infoFromOneInfo = + VibratorInfoFactory.create(/* id= */ -1, new VibratorInfo[] {info1}); + VibratorInfo infoFromTwoInfos = + VibratorInfoFactory.create(/* id= */ -3, new VibratorInfo[] {info1, info2}); + + assertEquals(3, infoFromEmptyInfos.getId()); + assertEquals(-1, infoFromOneInfo.getId()); + assertEquals(-3, infoFromTwoInfos.getId()); + } + + @Test + public void testCreatedInfo_fromEmptyVibratorInfos_returnsEmptyVibratorInfo() { + VibratorInfo info = VibratorInfoFactory.create(/* id= */ 2, new VibratorInfo[] {}); + + assertEqualContent(VibratorInfo.EMPTY_VIBRATOR_INFO, info); + } + + @Test + public void testCreatedInfo_fromSingleVibratorInfo_hasEqualContent() { + VibratorInfo info = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_FREQUENCY_CONTROL) + .setSupportedEffects(VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_THUD) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 30) + .build(); + + VibratorInfo createdInfo = + VibratorInfoFactory.create(/* id= */ -1, new VibratorInfo[] {info}); + + assertEqualContent(info, createdInfo); + } + + @Test + public void testCreatedInfo_hasEqualContentRegardlessOfSourceInfoOrder() { + VibratorInfo info1 = new VibratorInfo.Builder(/* id= */ 1) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) + .setSupportedEffects(VibrationEffect.EFFECT_CLICK) + .build(); + VibratorInfo info2 = new VibratorInfo.Builder(/* id= */ 2) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .build(); + + assertEqualContent( + VibratorInfoFactory.create(/* id= */ -1, new VibratorInfo[] {info1, info2}), + VibratorInfoFactory.create(/* id= */ -1, new VibratorInfo[] {info2, info1})); + } + + @Test + public void testCreatedInfoContents() { + VibratorInfo info1 = new VibratorInfo.Builder(/* id= */ -1) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_FREQUENCY_CONTROL) + .setSupportedEffects(VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_POP) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 5) + .build(); + VibratorInfo info2 = new VibratorInfo.Builder(/* id= */ -2) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL) + .setSupportedEffects(VibrationEffect.EFFECT_POP, VibrationEffect.EFFECT_THUD) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 20) + .build(); + VibratorInfo info3 = new VibratorInfo.Builder(/* id= */ -3) + .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) + .build(); + + assertEquals( + new VibratorInfo.Builder(/* id= */ 3) + .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) + .setSupportedEffects(VibrationEffect.EFFECT_POP) + .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 20) + .build(), + VibratorInfoFactory.create(/* id= */ 3, new VibratorInfo[] {info1, info2})); + assertEquals( + new VibratorInfo.Builder(/* id= */ 3) + .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) + .build(), + VibratorInfoFactory.create(/* id= */ 3, new VibratorInfo[] {info2, info3})); + assertEquals( + new VibratorInfo.Builder(/* id= */ 3).build(), + VibratorInfoFactory.create(/* id= */ 3, new VibratorInfo[] {info1, info3})); + } + + private static void assertEqualContent(VibratorInfo info1, VibratorInfo info2) { + assertTrue(info1.equalContent(info2)); + } +} diff --git a/graphics/java/android/graphics/Gainmap.java b/graphics/java/android/graphics/Gainmap.java index f639521ff250..b5fb13db4ae4 100644 --- a/graphics/java/android/graphics/Gainmap.java +++ b/graphics/java/android/graphics/Gainmap.java @@ -124,8 +124,6 @@ public final class Gainmap implements Parcelable { /** * Creates a new gainmap using the provided gainmap as the metadata source and the provided * bitmap as the replacement for the gainmapContents - * TODO: Make public, it's useful - * @hide */ public Gainmap(@NonNull Gainmap gainmap, @NonNull Bitmap gainmapContents) { this(gainmapContents, nCreateCopy(gainmap.mNativePtr)); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java index ef93a336305f..be1b9b1227de 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java @@ -16,6 +16,7 @@ package com.android.wm.shell.common.split; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; @@ -60,7 +61,8 @@ public class SplitScreenConstants { public static final int[] CONTROLLED_WINDOWING_MODES = {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED}; public static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE = - {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW}; + {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW, + WINDOWING_MODE_FREEFORM}; /** Flag applied to a transition change to identify it as a divider bar for animation. */ public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 1d46e755ec9d..633f627e8e71 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode import android.R import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM @@ -301,6 +302,24 @@ class DesktopTasksController( } } + /** Move a desktop app to split screen. */ + fun moveToSplit(task: RunningTaskInfo) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: moveToSplit taskId=%d", + task.taskId + ) + val wct = WindowContainerTransaction() + wct.setWindowingMode(task.token, WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW) + wct.setBounds(task.token, null) + wct.setDensityDpi(task.token, getDefaultDensityDpi()) + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + /** * The second part of the animated move to desktop transition, called after * {@link startMoveToDesktop}. Move a task to fullscreen after being dragged from fullscreen 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 2be7a491fdf2..29fff03500a5 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 @@ -71,6 +71,7 @@ import com.android.wm.shell.desktopmode.DesktopModeController; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellController; @@ -200,6 +201,19 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { @Override public void setSplitScreenController(SplitScreenController splitScreenController) { mSplitScreenController = splitScreenController; + mSplitScreenController.registerSplitScreenListener(new SplitScreen.SplitScreenListener() { + @Override + public void onTaskStageChanged(int taskId, int stage, boolean visible) { + if (visible) { + DesktopModeWindowDecoration decor = mWindowDecorByTaskId.get(taskId); + if (decor != null && DesktopModeStatus.isActive(mContext) + && decor.mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(false)); + mDesktopTasksController.ifPresent(c -> c.moveToSplit(decor.mTaskInfo)); + } + } + } + }); } @Override diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/Utils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/Utils.kt index 610cedefe594..fa723e3110e0 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/Utils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/Utils.kt @@ -23,6 +23,7 @@ import android.platform.test.rule.UnlockScreenRule import android.tools.common.NavBar import android.tools.common.Rotation import android.tools.device.apphelpers.MessagingAppHelper +import android.tools.device.flicker.rules.ArtifactSaverRule import android.tools.device.flicker.rules.ChangeDisplayOrientationRule import android.tools.device.flicker.rules.LaunchAppRule import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule @@ -33,9 +34,10 @@ object Utils { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() fun testSetupRule(navigationMode: NavBar, rotation: Rotation): RuleChain { - return RuleChain.outerRule(UnlockScreenRule()) + return RuleChain.outerRule(ArtifactSaverRule()) + .around(UnlockScreenRule()) .around( - NavigationModeRule(navigationMode.value, /* changeNavigationModeAfterTest */ false) + NavigationModeRule(navigationMode.value, false) ) .around( LaunchAppRule(MessagingAppHelper(instrumentation), clearCacheAfterParsing = false) diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h index b5b828e50cdd..529a49e9277e 100644 --- a/libs/hwui/RenderNode.h +++ b/libs/hwui/RenderNode.h @@ -114,7 +114,7 @@ public: return mDisplayList.containsProjectionReceiver(); } - const char* getName() const { return mName.string(); } + const char* getName() const { return mName.c_str(); } void setName(const char* name) { if (name) { diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 3d77877cf6eb..6679f8ff4f24 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -201,7 +201,7 @@ bool SkiaPipeline::createOrUpdateLayer(RenderNode* node, const DamageAccumulator String8 cachesOutput; mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput, &mRenderThread.renderState()); - ALOGE("%s", cachesOutput.string()); + ALOGE("%s", cachesOutput.c_str()); if (errorHandler) { std::ostringstream err; err << "Unable to create layer for " << node->getName(); diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp index c7400743a14b..94ed06c806e5 100644 --- a/libs/hwui/renderthread/RenderThread.cpp +++ b/libs/hwui/renderthread/RenderThread.cpp @@ -357,7 +357,7 @@ void RenderThread::dumpGraphicsMemory(int fd, bool includeProfileData) { String8 cachesOutput; mCacheManager->dumpMemoryUsage(cachesOutput, mRenderState); - dprintf(fd, "\nPipeline=%s\n%s", pipelineToString(), cachesOutput.string()); + dprintf(fd, "\nPipeline=%s\n%s", pipelineToString(), cachesOutput.c_str()); for (auto&& context : mCacheManager->mCanvasContexts) { context->visitAllRenderNodes([&](const RenderNode& node) { if (node.isTextureView()) { diff --git a/packages/CtsShim/build/Android.bp b/packages/CtsShim/build/Android.bp index d778febe9faf..d6b7ecf5819d 100644 --- a/packages/CtsShim/build/Android.bp +++ b/packages/CtsShim/build/Android.bp @@ -208,3 +208,22 @@ android_app { ], min_sdk_version: "24", } + +//########################################################## +// Variant: Add apk to an apex +android_app { + name: "CtsShimAddApkToApex", + sdk_version: "current", + srcs: ["shim_add_apk_to_apex/src/android/addapktoapex/app/AddApkToApexDeviceActivity.java"], + optimize: { + enabled: false, + }, + dex_preopt: { + enabled: false, + }, + manifest: "shim_add_apk_to_apex/AndroidManifestAddApkToApex.xml", + apex_available: [ + "//apex_available:platform", + "com.android.apex.cts.shim.v2_add_apk_to_apex", + ], +} diff --git a/packages/CtsShim/build/shim_add_apk_to_apex/AndroidManifestAddApkToApex.xml b/packages/CtsShim/build/shim_add_apk_to_apex/AndroidManifestAddApkToApex.xml new file mode 100644 index 000000000000..0e620b062ed6 --- /dev/null +++ b/packages/CtsShim/build/shim_add_apk_to_apex/AndroidManifestAddApkToApex.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.addapktoapex.app"> + + <application> + <activity android:name=".AddApkToApexDeviceActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + </application> + +</manifest>
\ No newline at end of file diff --git a/packages/CtsShim/build/shim_add_apk_to_apex/src/android/addapktoapex/app/AddApkToApexDeviceActivity.java b/packages/CtsShim/build/shim_add_apk_to_apex/src/android/addapktoapex/app/AddApkToApexDeviceActivity.java new file mode 100644 index 000000000000..c68904b30d6a --- /dev/null +++ b/packages/CtsShim/build/shim_add_apk_to_apex/src/android/addapktoapex/app/AddApkToApexDeviceActivity.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.addapktoapex.app; + +import android.app.Activity; +import android.os.Bundle; +import android.util.Log; + +/** + * A simple activity which logs to Logcat. + */ +public class AddApkToApexDeviceActivity extends Activity { + + private static final String TAG = AddApkToApexDeviceActivity.class.getSimpleName(); + + /** + * The test string to log. + */ + private static final String TEST_STRING = "AddApkToApexTestString"; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + // Log the test string to Logcat. + Log.i(TAG, TEST_STRING); + } + +} diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java index 9ab84d254ed6..f90a17ae8761 100644 --- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java @@ -45,6 +45,7 @@ public class BatteryStatus { private static final int EXTREME_LOW_BATTERY_THRESHOLD = 3; private static final int DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT = 5000000; + public static final int BATTERY_LEVEL_UNKNOWN = -1; public static final int CHARGING_UNKNOWN = -1; public static final int CHARGING_SLOWLY = 0; public static final int CHARGING_REGULAR = 1; @@ -186,12 +187,13 @@ public class BatteryStatus { /** Gets the battery level from the intent. */ public static int getBatteryLevel(Intent batteryChangedIntent) { if (batteryChangedIntent == null) { - return -1; /*invalid battery level*/ + return BATTERY_LEVEL_UNKNOWN; } - final int level = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + final int level = + batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, BATTERY_LEVEL_UNKNOWN); final int scale = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 0); return scale == 0 - ? -1 /*invalid battery level*/ + ? BATTERY_LEVEL_UNKNOWN : Math.round((level / (float) scale) * 100f); } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt index 798bdec49f11..ab0225d63ac5 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -409,6 +409,18 @@ open class ClockRegistry( scope.launch(bgDispatcher) { mutateSetting { it.copy(seedColor = value) } } } + // Returns currentClockId if clock is connected, otherwise DEFAULT_CLOCK_ID. Since this + // is dependent on which clocks are connected, it may change when a clock is installed or + // removed from the device (unlike currentClockId). + // TODO: Merge w/ CurrentClockId when we convert to a flow. We shouldn't need both behaviors. + val activeClockId: String + get() { + if (!availableClocks.containsKey(currentClockId)) { + return DEFAULT_CLOCK_ID + } + return currentClockId + } + init { // Register default clock designs for (clock in defaultClockProvider.getClocks()) { diff --git a/packages/SystemUI/res/drawable/auth_credential_emergency_button_background.xml b/packages/SystemUI/res/drawable/auth_credential_emergency_button_background.xml new file mode 100644 index 000000000000..85450b446d48 --- /dev/null +++ b/packages/SystemUI/res/drawable/auth_credential_emergency_button_background.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<inset xmlns:android="http://schemas.android.com/apk/res/android"> + <shape android:shape="rectangle"> + <corners android:radius="25dp"/> + <solid android:color="@android:color/system_accent3_100" /> + </shape> +</inset> diff --git a/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml b/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml index 4a9d41fae1d5..b83f15a1a247 100644 --- a/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml +++ b/packages/SystemUI/res/drawable/stat_sys_ringer_silent.xml @@ -14,6 +14,4 @@ Copyright (C) 2015 The Android Open Source Project limitations under the License. --> <inset xmlns:android="http://schemas.android.com/apk/res/android" - android:insetLeft="3dp" - android:insetRight="3dp" android:drawable="@drawable/ic_speaker_mute" /> diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml index e2ce34f5008e..e439f775f8ea 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml @@ -61,29 +61,46 @@ </RelativeLayout> - <LinearLayout + <FrameLayout android:id="@+id/auth_credential_input" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> - <ImeAwareEditText - android:id="@+id/lockPassword" - style="?passwordTextAppearance" - android:layout_width="208dp" + <LinearLayout + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="center" - android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii" - android:inputType="textPassword" - android:minHeight="48dp" /> + android:layout_gravity="center_horizontal|top" + android:orientation="vertical"> - <TextView - android:id="@+id/error" - style="?errorTextAppearance" - android:layout_gravity="center" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> + <ImeAwareEditText + android:id="@+id/lockPassword" + style="?passwordTextAppearance" + android:layout_width="208dp" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii" + android:inputType="textPassword" + android:minHeight="48dp"/> + + <TextView + android:id="@+id/error" + style="?errorTextAppearance" + android:layout_gravity="center_horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + </LinearLayout> - </LinearLayout> + <Button + android:id="@+id/emergencyCallButton" + style="@style/AuthCredentialEmergencyButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + android:layout_gravity="center_horizontal|bottom" + android:layout_marginTop="12dp" + android:layout_marginBottom="12dp" + android:text="@string/work_challenge_emergency_button_text"/> + </FrameLayout> </com.android.systemui.biometrics.ui.CredentialPasswordView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml index 88f138f9b093..d5af37733b3b 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml @@ -60,27 +60,44 @@ android:layout_width="wrap_content" android:layout_height="wrap_content"/> + <TextView + android:id="@+id/error" + style="?errorTextAppearanceLand" + android:layout_below="@id/description" + android:layout_alignParentLeft="true" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + </RelativeLayout> - <FrameLayout + <RelativeLayout android:layout_weight="1" - style="?containerStyle" android:layout_width="0dp" android:layout_height="match_parent"> - <com.android.internal.widget.LockPatternView - android:id="@+id/lockPattern" - android:layout_gravity="center" - android:layout_width="@dimen/biometric_auth_pattern_view_size" - android:layout_height="@dimen/biometric_auth_pattern_view_size"/> - - <TextView - android:id="@+id/error" - style="?errorTextAppearance" + <FrameLayout + style="?containerStyle" + android:layout_above="@id/emergencyCallButton" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_gravity="center_horizontal|bottom"/> + android:layout_height="match_parent"> + + <com.android.internal.widget.LockPatternView + android:id="@+id/lockPattern" + android:layout_gravity="center" + android:layout_width="@dimen/biometric_auth_pattern_view_size" + android:layout_height="@dimen/biometric_auth_pattern_view_size"/> + </FrameLayout> - </FrameLayout> + <Button + android:id="@+id/emergencyCallButton" + style="@style/AuthCredentialEmergencyButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="35dp" + android:visibility="gone" + android:layout_alignParentBottom="true" + android:layout_centerHorizontal="true" + android:text="@string/work_challenge_emergency_button_text"/> + </RelativeLayout> </com.android.systemui.biometrics.ui.CredentialPatternView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_credential_password_view.xml b/packages/SystemUI/res/layout/auth_credential_password_view.xml index 33f1b10b123b..9336845f20f7 100644 --- a/packages/SystemUI/res/layout/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml @@ -65,29 +65,46 @@ </ScrollView> - <LinearLayout + <FrameLayout android:id="@+id/auth_credential_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> - <ImeAwareEditText - android:id="@+id/lockPassword" - style="?passwordTextAppearance" - android:layout_width="208dp" + <LinearLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii" - android:inputType="textPassword" - android:minHeight="48dp" /> + android:layout_gravity="center_horizontal|top" + android:orientation="vertical"> - <TextView - android:id="@+id/error" - style="?errorTextAppearance" - android:layout_gravity="center_horizontal" - android:layout_width="match_parent" - android:layout_height="wrap_content" /> + <ImeAwareEditText + android:id="@+id/lockPassword" + style="?passwordTextAppearance" + android:layout_width="208dp" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii" + android:inputType="textPassword" + android:minHeight="48dp"/> - </LinearLayout> + <TextView + android:id="@+id/error" + style="?errorTextAppearance" + android:layout_gravity="center_horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + </LinearLayout> + + <Button + android:id="@+id/emergencyCallButton" + style="@style/AuthCredentialEmergencyButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + android:layout_gravity="center_horizontal|bottom" + android:layout_marginTop="12dp" + android:layout_marginBottom="12dp" + android:text="@string/work_challenge_emergency_button_text"/> + </FrameLayout> </com.android.systemui.biometrics.ui.CredentialPasswordView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml index 81ca37189ac4..59828fde309f 100644 --- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml @@ -58,24 +58,42 @@ android:layout_height="wrap_content"/> </RelativeLayout> - <FrameLayout + <RelativeLayout android:id="@+id/auth_credential_container" - style="?containerStyle" android:layout_width="match_parent" android:layout_height="match_parent"> - <com.android.internal.widget.LockPatternView - android:id="@+id/lockPattern" - android:layout_gravity="center" - android:layout_width="@dimen/biometric_auth_pattern_view_size" - android:layout_height="@dimen/biometric_auth_pattern_view_size"/> + <FrameLayout + android:layout_centerInParent="true" + android:layout_above="@id/emergencyCallButton" + style="?containerStyle" + android:layout_width="wrap_content" + android:layout_height="match_parent"> + + <com.android.internal.widget.LockPatternView + android:id="@+id/lockPattern" + android:layout_gravity="center" + android:layout_width="@dimen/biometric_auth_pattern_view_size" + android:layout_height="@dimen/biometric_auth_pattern_view_size"/> - <TextView - android:id="@+id/error" - style="?errorTextAppearance" - android:layout_width="match_parent" + <TextView + android:id="@+id/error" + style="?errorTextAppearance" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal|bottom"/> + </FrameLayout> + + <Button + android:id="@+id/emergencyCallButton" + style="@style/AuthCredentialEmergencyButtonStyle" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="center_horizontal|bottom"/> - </FrameLayout> + android:layout_alignParentBottom="true" + android:visibility="gone" + android:layout_marginBottom="35dp" + android:layout_centerHorizontal="true" + android:text="@string/work_challenge_emergency_button_text"/> + </RelativeLayout> </com.android.systemui.biometrics.ui.CredentialPatternView> diff --git a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml index 78cd7184b485..39ec09b14157 100644 --- a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml +++ b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml @@ -34,8 +34,8 @@ android:layout_height="@dimen/overlay_dismiss_button_tappable_size" android:contentDescription="@string/screenshot_dismiss_work_profile"> <ImageView - android:layout_width="16dp" - android:layout_height="16dp" + android:layout_width="24dp" + android:layout_height="24dp" android:layout_gravity="center" android:background="@drawable/circular_background" android:backgroundTint="?androidprv:attr/materialColorSurfaceContainerHigh" diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml index d693631080af..8bc3eed59062 100644 --- a/packages/SystemUI/res/values/attrs.xml +++ b/packages/SystemUI/res/values/attrs.xml @@ -221,6 +221,7 @@ <attr name="descriptionTextAppearance" format="reference" /> <attr name="passwordTextAppearance" format="reference" /> <attr name="errorTextAppearance" format="reference"/> + <attr name="errorTextAppearanceLand" format="reference"/> </declare-styleable> <declare-styleable name="LogAccessPermissionGrantDialog"> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index cddfda2f9ce7..2003fa32615a 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -386,6 +386,8 @@ <string name="biometric_dialog_wrong_password">Wrong password</string> <!-- Error string shown when the user enters too many incorrect attempts [CHAR LIMIT=120]--> <string name="biometric_dialog_credential_too_many_attempts">Too many incorrect attempts.\nTry again in <xliff:g id="number">%d</xliff:g> seconds.</string> + <!-- Button text shown on an authentication screen giving the user the option to make an emergency call without unlocking their device [CHAR LIMIT=20] --> + <string name="work_challenge_emergency_button_text">Emergency</string> <!-- Error string shown when the user enters an incorrect PIN/pattern/password and it counts towards the max attempts before the data on the device is wiped. [CHAR LIMIT=NONE]--> <string name="biometric_dialog_credential_attempts_before_wipe">Try again. Attempt <xliff:g id="attempts" example="1">%1$d</xliff:g> of <xliff:g id="max_attempts" example="3">%2$d</xliff:g>.</string> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 10340c6847f7..6991b964c504 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -236,6 +236,13 @@ <item name="android:gravity">center</item> </style> + <style name="TextAppearance.AuthNonBioCredential.ErrorLand"> + <item name="android:layout_marginTop">20dp</item> + <item name="android:textSize">14sp</item> + <item name="android:textColor">?android:attr/colorError</item> + <item name="android:gravity">start</item> + </style> + <style name="TextAppearance.AuthCredential.PasswordEntry" parent="@android:style/TextAppearance.DeviceDefault"> <item name="android:gravity">center</item> <item name="android:paddingTop">28dp</item> @@ -276,6 +283,17 @@ <item name="android:minWidth">200dp</item> </style> + <style name="AuthCredentialEmergencyButtonStyle"> + <item name="android:background">@drawable/auth_credential_emergency_button_background</item> + <item name="android:textColor">@android:color/system_accent3_900</item> + <item name="android:outlineProvider">none</item> + <item name="android:paddingTop">15dp</item> + <item name="android:paddingBottom">15dp</item> + <item name="android:paddingLeft">30dp</item> + <item name="android:paddingRight">30dp</item> + <item name="android:textSize">16sp</item> + </style> + <style name="DeviceManagementDialogTitle"> <item name="android:gravity">center</item> <item name="android:textAppearance">@style/TextAppearance.Dialog.Title</item> @@ -353,6 +371,7 @@ <item name="descriptionTextAppearance">@style/TextAppearance.AuthNonBioCredential.Description</item> <item name="passwordTextAppearance">@style/TextAppearance.AuthCredential.PasswordEntry</item> <item name="errorTextAppearance">@style/TextAppearance.AuthNonBioCredential.Error</item> + <item name="errorTextAppearanceLand">@style/TextAppearance.AuthNonBioCredential.ErrorLand</item> </style> <style name="LockPatternViewStyle" > diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index dd39f1d6ed28..05ace74306bc 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -222,8 +222,10 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mSmallClockFrame = mView.findViewById(R.id.lockscreen_clock_view); mLargeClockFrame = mView.findViewById(R.id.lockscreen_clock_view_large); - mDumpManager.unregisterDumpable(getClass().getSimpleName()); // unregister previous clocks - mDumpManager.registerDumpable(getClass().getSimpleName(), this); + if (!mOnlyClock) { + mDumpManager.unregisterDumpable(getClass().getSimpleName()); // unregister previous + mDumpManager.registerDumpable(getClass().getSimpleName(), this); + } if (mFeatureFlags.isEnabled(LOCKSCREEN_WALLPAPER_DREAM_ENABLED)) { mStatusArea = mView.findViewById(R.id.keyguard_status_area); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 04692c48a123..9f3908a4ab92 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -27,6 +27,7 @@ import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_PRIMARY; import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER; import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_WORK_PROFILE; import static com.android.systemui.DejankUtils.whitelistIpcs; +import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE; import static com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES; import android.app.ActivityManager; @@ -370,8 +371,12 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard @Override public void onOrientationChanged(int orientation) { - KeyguardSecurityContainerController.this + if (mFeatureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE)) { + // TODO(b/295603468) + // Fix reinflation of views when flag is enabled. + KeyguardSecurityContainerController.this .onDensityOrFontScaleOrOrientationChanged(); + } } }; private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt index caebc30d1c07..a3ee220d3c3f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt @@ -16,6 +16,7 @@ sealed class BiometricPromptRequest( val description: String, val userInfo: BiometricUserInfo, val operationInfo: BiometricOperationInfo, + val showEmergencyCallButton: Boolean, ) { /** Prompt using one or more biometrics. */ class Biometric( @@ -29,7 +30,8 @@ sealed class BiometricPromptRequest( subtitle = info.subtitle?.toString() ?: "", description = info.description?.toString() ?: "", userInfo = userInfo, - operationInfo = operationInfo + operationInfo = operationInfo, + showEmergencyCallButton = info.isShowEmergencyCallButton ) { val negativeButtonText: String = info.negativeButtonText?.toString() ?: "" } @@ -46,6 +48,7 @@ sealed class BiometricPromptRequest( description = (info.deviceCredentialDescription ?: info.description)?.toString() ?: "", userInfo = userInfo, operationInfo = operationInfo, + showEmergencyCallButton = info.isShowEmergencyCallButton ) { /** PIN prompt. */ diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt index 9292bd7ecd60..4ac9f967920f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt @@ -2,6 +2,7 @@ package com.android.systemui.biometrics.ui.binder import android.view.View import android.view.ViewGroup +import android.widget.Button import android.widget.ImageView import android.widget.TextView import androidx.lifecycle.Lifecycle @@ -46,6 +47,7 @@ object CredentialViewBinder { val descriptionView: TextView = view.requireViewById(R.id.description) val iconView: ImageView? = view.findViewById(R.id.icon) val errorView: TextView = view.requireViewById(R.id.error) + val emergencyButtonView: Button = view.requireViewById(R.id.emergencyCallButton) var errorTimer: Job? = null @@ -75,6 +77,13 @@ object CredentialViewBinder { iconView?.setImageDrawable(header.icon) + if (header.showEmergencyCallButton) { + emergencyButtonView.visibility = View.VISIBLE + emergencyButtonView.setOnClickListener { + viewModel.doEmergencyCall(view.context) + } + } + // Only animate this if we're transitioning from a biometric view. if (viewModel.animateContents.value) { view.animateCredentialViewIn() diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt index 3257f20815d8..c6d90855e7d2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt @@ -10,4 +10,5 @@ interface CredentialHeaderViewModel { val subtitle: String val description: String val icon: Drawable + val showEmergencyCallButton: Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt index a3b23ca89cad..6212ef000ff1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt @@ -41,6 +41,7 @@ constructor( subtitle = request.subtitle, description = request.description, icon = applicationContext.asLockIcon(request.userInfo.deviceCredentialOwnerId), + showEmergencyCallButton = request.showEmergencyCallButton ) } @@ -136,6 +137,18 @@ constructor( } } } + + fun doEmergencyCall(context: Context) { + val intent = + context + .getSystemService(android.telecom.TelecomManager::class.java)!! + .createLaunchEmergencyDialerIntent(null) + .setFlags( + android.content.Intent.FLAG_ACTIVITY_NEW_TASK or + android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP + ) + context.startActivity(intent) + } } private fun Context.asBadCredentialErrorMessage(prompt: BiometricPromptRequest?): String = @@ -174,6 +187,7 @@ private class BiometricPromptHeaderViewModelImpl( override val subtitle: String, override val description: String, override val icon: Drawable, + override val showEmergencyCallButton: Boolean, ) : CredentialHeaderViewModel private fun CredentialHeaderViewModel.asRequest(): BiometricPromptRequest.Credential = diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 2fc45740c133..1516f3e4b817 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -355,7 +355,7 @@ object Flags { // TODO(b/278068252): Tracking Bug @JvmField - val QS_PIPELINE_AUTO_ADD = unreleasedFlag("qs_pipeline_auto_add", teamfood = false) + val QS_PIPELINE_AUTO_ADD = unreleasedFlag("qs_pipeline_auto_add", teamfood = true) // TODO(b/254512383): Tracking Bug @JvmField @@ -623,6 +623,10 @@ object Flags { // TODO(b/251205791): Tracking Bug @JvmField val SCREENSHOT_APP_CLIPS = releasedFlag("screenshot_app_clips") + /** TODO(b/295143676): Tracking bug. When enable, captures a screenshot for each display. */ + @JvmField + val MULTI_DISPLAY_SCREENSHOT = unreleasedFlag("multi_display_screenshot") + // 1400 - columbus // TODO(b/254512756): Tracking Bug val QUICK_TAP_IN_PCC = releasedFlag("quick_tap_in_pcc") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt index 635961b0ea01..e501ece626b4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt @@ -54,7 +54,11 @@ constructor( if (event.handleAction()) { when (event.keyCode) { KeyEvent.KEYCODE_MENU -> return dispatchMenuKeyEvent() - KeyEvent.KEYCODE_SPACE -> return dispatchSpaceEvent() + KeyEvent.KEYCODE_SPACE, + KeyEvent.KEYCODE_ENTER -> + if (isDeviceInteractive()) { + return collapseShadeLockedOrShowPrimaryBouncer() + } } } return false @@ -90,16 +94,22 @@ constructor( (statusBarStateController.state != StatusBarState.SHADE) && statusBarKeyguardViewManager.shouldDismissOnMenuPressed() if (shouldUnlockOnMenuPressed) { - shadeController.animateCollapseShadeForced() - return true + return collapseShadeLockedOrShowPrimaryBouncer() } return false } - private fun dispatchSpaceEvent(): Boolean { - if (isDeviceInteractive() && statusBarStateController.state != StatusBarState.SHADE) { - shadeController.animateCollapseShadeForced() - return true + private fun collapseShadeLockedOrShowPrimaryBouncer(): Boolean { + when (statusBarStateController.state) { + StatusBarState.SHADE -> return false + StatusBarState.SHADE_LOCKED -> { + shadeController.animateCollapseShadeForced() + return true + } + StatusBarState.KEYGUARD -> { + statusBarKeyguardViewManager.showPrimaryBouncer(true) + return true + } } return false } diff --git a/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java b/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java index f8f784ff948c..3d4fca1b8945 100644 --- a/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java +++ b/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java @@ -216,6 +216,58 @@ public class NotificationPlayer implements OnCompletionListener, OnErrorListener } } + private void stopSound(Command cmd) { + final MediaPlayer mp; + synchronized (mPlayerLock) { + mp = mPlayer; + mPlayer = null; + } + if (mp == null) { + Log.w(mTag, "STOP command without a player"); + return; + } + + long delay = SystemClock.uptimeMillis() - cmd.requestTime; + if (delay > 1000) { + Log.w(mTag, "Notification stop delayed by " + delay + "msecs"); + } + try { + mp.stop(); + } catch (Exception e) { + Log.w(mTag, "Failed to stop MediaPlayer", e); + } + if (DEBUG) { + Log.i(mTag, "About to release MediaPlayer piid:" + + mp.getPlayerIId() + " due to notif cancelled"); + } + try { + mp.release(); + } catch (Exception e) { + Log.w(mTag, "Failed to release MediaPlayer", e); + } + synchronized (mQueueAudioFocusLock) { + if (mAudioManagerWithAudioFocus != null) { + if (DEBUG) { + Log.d(mTag, "in STOP: abandonning AudioFocus"); + } + try { + mAudioManagerWithAudioFocus.abandonAudioFocus(null); + } catch (Exception e) { + Log.w(mTag, "Failed to abandon audio focus", e); + } + mAudioManagerWithAudioFocus = null; + } + } + synchronized (mCompletionHandlingLock) { + if ((mLooper != null) && (mLooper.getThread().getState() != Thread.State.TERMINATED)) { + if (DEBUG) { + Log.d(mTag, "in STOP: quitting looper " + mLooper); + } + mLooper.quit(); + } + } + } + private final class CmdThread extends java.lang.Thread { CmdThread() { super("NotificationPlayer-" + mTag); @@ -229,62 +281,28 @@ public class NotificationPlayer implements OnCompletionListener, OnErrorListener if (DEBUG) Log.d(mTag, "RemoveFirst"); cmd = mCmdQueue.removeFirst(); } - - switch (cmd.code) { - case PLAY: - if (DEBUG) Log.d(mTag, "PLAY"); - startSound(cmd); - break; - case STOP: - if (DEBUG) Log.d(mTag, "STOP"); - final MediaPlayer mp; - synchronized (mPlayerLock) { - mp = mPlayer; - mPlayer = null; + try { + switch (cmd.code) { + case PLAY: + if (DEBUG) Log.d(mTag, "PLAY"); + startSound(cmd); + break; + case STOP: + if (DEBUG) Log.d(mTag, "STOP"); + stopSound(cmd); + break; } - if (mp != null) { - long delay = SystemClock.uptimeMillis() - cmd.requestTime; - if (delay > 1000) { - Log.w(mTag, "Notification stop delayed by " + delay + "msecs"); + } finally { + synchronized (mCmdQueue) { + if (mCmdQueue.size() == 0) { + // nothing left to do, quit + // doing this check after we're done prevents the case where they + // added it during the operation from spawning two threads and + // trying to do them in parallel. + mThread = null; + releaseWakeLock(); + return; } - try { - mp.stop(); - } catch (Exception e) { } - if (DEBUG) { - Log.i(mTag, "About to release MediaPlayer piid:" - + mp.getPlayerIId() + " due to notif cancelled"); - } - mp.release(); - synchronized(mQueueAudioFocusLock) { - if (mAudioManagerWithAudioFocus != null) { - if (DEBUG) { Log.d(mTag, "in STOP: abandonning AudioFocus"); } - mAudioManagerWithAudioFocus.abandonAudioFocus(null); - mAudioManagerWithAudioFocus = null; - } - } - synchronized (mCompletionHandlingLock) { - if ((mLooper != null) && - (mLooper.getThread().getState() != Thread.State.TERMINATED)) - { - if (DEBUG) { Log.d(mTag, "in STOP: quitting looper "+ mLooper); } - mLooper.quit(); - } - } - } else { - Log.w(mTag, "STOP command without a player"); - } - break; - } - - synchronized (mCmdQueue) { - if (mCmdQueue.size() == 0) { - // nothing left to do, quit - // doing this check after we're done prevents the case where they - // added it during the operation from spawning two threads and - // trying to do them in parallel. - mThread = null; - releaseWakeLock(); - return; } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index a3d1d8c6959d..d88249830739 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -515,11 +515,13 @@ public abstract class MediaOutputBaseAdapter extends mSeekBar.setOnTouchListener((v, event) -> false); updateIconAreaClickListener((v) -> { if (device.getCurrentVolume() == 0) { + mController.logInteractionUnmuteDevice(device); mSeekBar.setVolume(UNMUTE_DEFAULT_VOLUME); mController.adjustVolume(device, UNMUTE_DEFAULT_VOLUME); updateUnmutedVolumeIcon(); mIconAreaLayout.setOnTouchListener(((iconV, event) -> false)); } else { + mController.logInteractionMuteDevice(device); mSeekBar.resetVolume(); mController.adjustVolume(device, 0); updateMutedVolumeIcon(); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index d281f50a292c..bb0e9d182579 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -837,6 +837,14 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mMetricLogger.logInteractionAdjustVolume(device); } + void logInteractionMuteDevice(MediaDevice device) { + mMetricLogger.logInteractionMute(device); + } + + void logInteractionUnmuteDevice(MediaDevice device) { + mMetricLogger.logInteractionUnmute(device); + } + String getPackageName() { return mPackageName; } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java index 412d1a3a5013..ffd626abbfe8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java @@ -154,6 +154,38 @@ public class MediaOutputMetricLogger { } /** + * Do the metric logging of muting device. + */ + public void logInteractionMute(MediaDevice source) { + if (DEBUG) { + Log.d(TAG, "logInteraction - Mute"); + } + + SysUiStatsLog.write( + SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT, + SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__MUTE, + getInteractionDeviceType(source), + getLoggingPackageName(), + source.isSuggestedDevice()); + } + + /** + * Do the metric logging of unmuting device. + */ + public void logInteractionUnmute(MediaDevice source) { + if (DEBUG) { + Log.d(TAG, "logInteraction - Unmute"); + } + + SysUiStatsLog.write( + SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT, + SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__UNMUTE, + getInteractionDeviceType(source), + getLoggingPackageName(), + source.isSuggestedDevice()); + } + + /** * Do the metric logging of content switching failure. * * @param deviceItemList media item list for device count updating diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt index 67927a4153b3..cb87e3cc142a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt @@ -22,13 +22,12 @@ import androidx.annotation.GuardedBy import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.plugins.qs.QSTile import com.android.systemui.plugins.qs.QSTileView import com.android.systemui.qs.external.TileServiceRequestController import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor +import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository import com.android.systemui.qs.pipeline.shared.TileSpec import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -37,10 +36,11 @@ import kotlinx.coroutines.launch /** * Adapter to determine what real class to use for classes that depend on [QSHost]. - * * When [Flags.QS_PIPELINE_NEW_HOST] is off, all calls will be routed to [QSTileHost]. - * * When [Flags.QS_PIPELINE_NEW_HOST] is on, calls regarding the current set of tiles will be - * routed to [CurrentTilesInteractor]. Other calls (like [createTileView]) will still be routed to + * * When [QSPipelineFlagsRepository.pipelineHostEnabled] is false, all calls will be routed to * [QSTileHost]. + * * When [QSPipelineFlagsRepository.pipelineHostEnabled] is true, calls regarding the current set + * of tiles will be routed to [CurrentTilesInteractor]. Other calls (like [createTileView]) will + * still be routed to [QSTileHost]. * * This routing also includes dumps. */ @@ -53,7 +53,7 @@ constructor( private val context: Context, private val tileServiceRequestControllerBuilder: TileServiceRequestController.Builder, @Application private val scope: CoroutineScope, - private val featureFlags: FeatureFlags, + flags: QSPipelineFlagsRepository, dumpManager: DumpManager, ) : QSHost { @@ -61,7 +61,7 @@ constructor( private const val TAG = "QSTileHost" } - private val useNewHost = featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST) + private val useNewHost = flags.pipelineHostEnabled @GuardedBy("callbacksMap") private val callbacksMap = mutableMapOf<QSHost.Callback, Job>() diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 432147f00186..e57db562b27a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -35,8 +35,6 @@ import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.nano.SystemUIProtoDump; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.qs.QSFactory; @@ -50,6 +48,7 @@ import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.nano.QsTileState; import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository; import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; +import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository; import com.android.systemui.settings.UserFileManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; @@ -119,7 +118,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P private TileLifecycleManager.Factory mTileLifeCycleManagerFactory; - private final FeatureFlags mFeatureFlags; + private final QSPipelineFlagsRepository mFeatureFlags; @Inject public QSTileHost(Context context, @@ -135,7 +134,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P CustomTileStatePersister customTileStatePersister, TileLifecycleManager.Factory tileLifecycleManagerFactory, UserFileManager userFileManager, - FeatureFlags featureFlags + QSPipelineFlagsRepository featureFlags ) { mContext = context; mUserContext = context; @@ -162,7 +161,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P // finishes before creating any tiles. tunerService.addTunable(this, TILES_SETTING); // AutoTileManager can modify mTiles so make sure mTiles has already been initialized. - if (!mFeatureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD)) { + if (!mFeatureFlags.getPipelineAutoAddEnabled()) { mAutoTiles = autoTiles.get(); } }); @@ -283,7 +282,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P } } // Do not process tiles if the flag is enabled. - if (mFeatureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { + if (mFeatureFlags.getPipelineHostEnabled()) { return; } if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt index 1f63f5da2f2b..b2111d765a9d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt @@ -16,8 +16,6 @@ package com.android.systemui.qs.dagger -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.qs.QSHost import com.android.systemui.qs.QSHostAdapter import com.android.systemui.qs.QSTileHost @@ -27,6 +25,7 @@ import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepositor import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedSharedPrefsRepository import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractorImpl +import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository import dagger.Binds import dagger.Module import dagger.Provides @@ -45,11 +44,11 @@ interface QSHostModule { @Provides @JvmStatic fun providePanelInteractor( - featureFlags: FeatureFlags, + featureFlags: QSPipelineFlagsRepository, qsHost: QSTileHost, panelInteractorImpl: PanelInteractorImpl ): PanelInteractor { - return if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { + return if (featureFlags.pipelineHostEnabled) { panelInteractorImpl } else { qsHost @@ -59,11 +58,11 @@ interface QSHostModule { @Provides @JvmStatic fun provideCustomTileAddedRepository( - featureFlags: FeatureFlags, + featureFlags: QSPipelineFlagsRepository, qsHost: QSTileHost, customTileAddedRepository: CustomTileAddedSharedPrefsRepository ): CustomTileAddedRepository { - return if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { + return if (featureFlags.pipelineHostEnabled) { customTileAddedRepository } else { qsHost diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt index 966f37014bd2..5a5e47af73c9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt @@ -27,8 +27,6 @@ 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.dump.nano.SystemUIProtoDump -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.plugins.qs.QSFactory import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.external.CustomTile @@ -39,6 +37,7 @@ import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepositor import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepository import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository import com.android.systemui.qs.pipeline.domain.model.TileModel +import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.qs.toProto @@ -139,7 +138,7 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, private val logger: QSPipelineLogger, - featureFlags: FeatureFlags, + featureFlags: QSPipelineFlagsRepository, ) : CurrentTilesInteractor { private val _currentSpecsAndTiles: MutableStateFlow<List<TileModel>> = @@ -171,7 +170,7 @@ constructor( } init { - if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { + if (featureFlags.pipelineHostEnabled) { startTileCollection() } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt index 224fc1ae864f..0743ba0b121a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt @@ -18,10 +18,9 @@ package com.android.systemui.qs.pipeline.domain.startable import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.qs.pipeline.domain.interactor.AutoAddInteractor import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor +import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository import javax.inject.Inject @SysUISingleton @@ -30,14 +29,11 @@ class QSPipelineCoreStartable constructor( private val currentTilesInteractor: CurrentTilesInteractor, private val autoAddInteractor: AutoAddInteractor, - private val featureFlags: FeatureFlags, + private val featureFlags: QSPipelineFlagsRepository, ) : CoreStartable { override fun start() { - if ( - featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST) && - featureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD) - ) { + if (featureFlags.pipelineAutoAddEnabled) { autoAddInteractor.init(currentTilesInteractor) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepository.kt new file mode 100644 index 000000000000..551b0f4890a4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepository.kt @@ -0,0 +1,23 @@ +package com.android.systemui.qs.pipeline.shared + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import javax.inject.Inject + +/** Encapsulate the different QS pipeline flags and their dependencies */ +@SysUISingleton +class QSPipelineFlagsRepository +@Inject +constructor( + private val featureFlags: FeatureFlags, +) { + + /** @see Flags.QS_PIPELINE_NEW_HOST */ + val pipelineHostEnabled: Boolean + get() = featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST) + + /** @see Flags.QS_PIPELINE_AUTO_ADD */ + val pipelineAutoAddEnabled: Boolean + get() = pipelineHostEnabled && featureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD) +} diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt index e8683fbea735..fb99775b6549 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt @@ -63,6 +63,7 @@ class ScreenRecordPermissionDialog( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setDialogTitle(R.string.screenrecord_permission_dialog_title) + setTitle(R.string.screenrecord_title) setStartButtonText(R.string.screenrecord_permission_dialog_continue) setStartButtonOnClickListener { v: View? -> onStartRecordingClicked?.run() diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index cabbeb1075f9..014093de62bd 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -985,6 +985,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Make sure the clock is in the correct position after the unlock animation // so that it's not in the wrong place when we show the keyguard again. positionClockAndNotifications(true /* forceClockUpdate */); + mScrimController.onUnlockAnimationFinished(); } private void unlockAnimationStarted( @@ -1243,6 +1244,13 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mKeyguardStatusViewController.init(); } mKeyguardStatusViewController.setSplitShadeEnabled(mSplitShadeEnabled); + mKeyguardStatusViewController.getView().addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + int oldHeight = oldBottom - oldTop; + if (v.getHeight() != oldHeight) { + mNotificationStackScrollLayoutController.animateNextTopPaddingChange(); + } + }); updateClockAppearance(); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 832a25bfbc41..802ed8069a69 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -21,8 +21,6 @@ import static com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.app.StatusBarManager; -import android.media.AudioManager; -import android.media.session.MediaSessionLegacyHelper; import android.os.PowerManager; import android.util.Log; import android.view.GestureDetector; @@ -47,6 +45,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dock.DockManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.TransitionState; @@ -101,6 +100,7 @@ public class NotificationShadeWindowViewController { private final NotificationInsetsController mNotificationInsetsController; private final boolean mIsTrackpadCommonEnabled; private final FeatureFlags mFeatureFlags; + private final KeyEventInteractor mKeyEventInteractor; private GestureDetector mPulsingWakeupGestureHandler; private GestureDetector mDreamingWakeupGestureHandler; private View mBrightnessMirror; @@ -164,7 +164,8 @@ public class NotificationShadeWindowViewController { FeatureFlags featureFlags, SystemClock clock, BouncerMessageInteractor bouncerMessageInteractor, - BouncerLogger bouncerLogger) { + BouncerLogger bouncerLogger, + KeyEventInteractor keyEventInteractor) { mLockscreenShadeTransitionController = transitionController; mFalsingCollector = falsingCollector; mStatusBarStateController = statusBarStateController; @@ -190,6 +191,7 @@ public class NotificationShadeWindowViewController { mNotificationInsetsController = notificationInsetsController; mIsTrackpadCommonEnabled = featureFlags.isEnabled(TRACKPAD_GESTURE_COMMON); mFeatureFlags = featureFlags; + mKeyEventInteractor = keyEventInteractor; // This view is not part of the newly inflated expanded status bar. mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container); @@ -457,44 +459,17 @@ public class NotificationShadeWindowViewController { @Override public boolean interceptMediaKey(KeyEvent event) { - return mService.interceptMediaKey(event); + return mKeyEventInteractor.interceptMediaKey(event); } @Override public boolean dispatchKeyEventPreIme(KeyEvent event) { - return mService.dispatchKeyEventPreIme(event); + return mKeyEventInteractor.dispatchKeyEventPreIme(event); } @Override public boolean dispatchKeyEvent(KeyEvent event) { - boolean down = event.getAction() == KeyEvent.ACTION_DOWN; - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_BACK: - if (!down) { - mBackActionInteractor.onBackRequested(); - } - return true; - case KeyEvent.KEYCODE_MENU: - if (!down) { - return mService.onMenuPressed(); - } - break; - case KeyEvent.KEYCODE_SPACE: - if (!down) { - return mService.onSpacePressed(); - } - break; - case KeyEvent.KEYCODE_VOLUME_DOWN: - case KeyEvent.KEYCODE_VOLUME_UP: - if (mStatusBarStateController.isDozing()) { - MediaSessionLegacyHelper.getHelper(mView.getContext()) - .sendVolumeKeyEvent( - event, AudioManager.USE_DEFAULT_STREAM_TYPE, true); - return true; - } - break; - } - return false; + return mKeyEventInteractor.dispatchKeyEvent(event); } }); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt index b0023305ecdd..c27682726da5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt @@ -20,7 +20,9 @@ import android.content.Context import android.util.AttributeSet import android.view.View import android.widget.TextView +import com.android.internal.widget.ConversationLayout import com.android.internal.widget.ImageFloatingTextView +import com.android.internal.widget.MessagingLayout import javax.inject.Inject class PrecomputedTextViewFactory @Inject constructor() : NotifRemoteViewsFactory { @@ -35,6 +37,10 @@ class PrecomputedTextViewFactory @Inject constructor() : NotifRemoteViewsFactory TextView::class.java.simpleName -> PrecomputedTextView(context, attrs) ImageFloatingTextView::class.java.name -> PrecomputedImageFloatingTextView(context, attrs) + MessagingLayout::class.java.name -> + MessagingLayout(context, attrs).apply { setPrecomputedTextEnabled(true) } + ConversationLayout::class.java.name -> + ConversationLayout(context, attrs).apply { setPrecomputedTextEnabled(true) } else -> null } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 5c28be3bc678..af09bf281c0c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -25,7 +25,6 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.UserHandle; -import android.view.KeyEvent; import android.view.MotionEvent; import android.view.RemoteAnimationAdapter; import android.view.View; @@ -276,19 +275,11 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { void userActivity(); - boolean interceptMediaKey(KeyEvent event); - - boolean dispatchKeyEventPreIme(KeyEvent event); - - boolean onMenuPressed(); - void endAffordanceLaunch(); /** Should the keyguard be hidden immediately in response to a back press/gesture. */ boolean shouldKeyguardHideImmediately(); - boolean onSpacePressed(); - void showBouncerWithDimissAndCancelIfKeyguard(OnDismissAction performAction, Runnable cancelAction); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 8ffd43a6eb89..ccb87bf44dc8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -89,7 +89,6 @@ import android.util.MathUtils; import android.view.Display; import android.view.IRemoteAnimationRunner; import android.view.IWindowManager; -import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ThreadedRenderer; import android.view.View; @@ -2636,44 +2635,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } @Override - public boolean interceptMediaKey(KeyEvent event) { - return mState == StatusBarState.KEYGUARD - && mStatusBarKeyguardViewManager.interceptMediaKey(event); - } - - /** - * While IME is active and a BACK event is detected, check with - * {@link StatusBarKeyguardViewManager#dispatchBackKeyEventPreIme()} to see if the event - * should be handled before routing to IME, in order to prevent the user having to hit back - * twice to exit bouncer. - */ - @Override - public boolean dispatchKeyEventPreIme(KeyEvent event) { - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_BACK: - if (mState == StatusBarState.KEYGUARD - && mStatusBarKeyguardViewManager.dispatchBackKeyEventPreIme()) { - return mBackActionInteractor.onBackRequested(); - } - } - return false; - } - - protected boolean shouldUnlockOnMenuPressed() { - return mDeviceInteractive && mState != StatusBarState.SHADE - && mStatusBarKeyguardViewManager.shouldDismissOnMenuPressed(); - } - - @Override - public boolean onMenuPressed() { - if (shouldUnlockOnMenuPressed()) { - mShadeController.animateCollapseShadeForced(); - return true; - } - return false; - } - - @Override public void endAffordanceLaunch() { releaseGestureWakeLock(); mCameraLauncherLazy.get().setLaunchingAffordance(false); @@ -2692,15 +2653,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return (isScrimmedBouncer || isBouncerOverDream); } - @Override - public boolean onSpacePressed() { - if (mDeviceInteractive && mState != StatusBarState.SHADE) { - mShadeController.animateCollapseShadeForced(); - return true; - } - return false; - } - private void showBouncerOrLockScreenIfKeyguard() { // If the keyguard is animating away, we aren't really the keyguard anymore and should not // show the bouncer/lockscreen. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index fc661384146c..62a8cfde80da 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -709,6 +709,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } } + public void onUnlockAnimationFinished() { + mAnimatingPanelExpansionOnUnlock = false; + applyAndDispatchState(); + } + /** * Set the amount of progress we are currently in if we're transitioning to the full shade. * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index ad4bd584b5fc..dff058c49188 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -621,6 +621,51 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { configurationListenerArgumentCaptor.value.onUiModeChanged() verify(view).reloadColors() } + @Test + fun onOrientationChanged_landscapeKeyguardFlagDisabled_blockReinflate() { + featureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false) + + // Run onOrientationChanged + val configurationListenerArgumentCaptor = + ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) + underTest.onViewAttached() + verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture()) + clearInvocations(viewFlipperController) + configurationListenerArgumentCaptor.value.onOrientationChanged( + Configuration.ORIENTATION_LANDSCAPE + ) + // Verify view is reinflated when flag is on + verify(viewFlipperController, never()).clearViews() + verify(viewFlipperController, never()) + .asynchronouslyInflateView( + eq(SecurityMode.PIN), + any(), + onViewInflatedCallbackArgumentCaptor.capture() + ) + } + + @Test + fun onOrientationChanged_landscapeKeyguardFlagEnabled_doesReinflate() { + featureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, true) + + // Run onOrientationChanged + val configurationListenerArgumentCaptor = + ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) + underTest.onViewAttached() + verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture()) + clearInvocations(viewFlipperController) + configurationListenerArgumentCaptor.value.onOrientationChanged( + Configuration.ORIENTATION_LANDSCAPE + ) + // Verify view is reinflated when flag is on + verify(viewFlipperController).clearViews() + verify(viewFlipperController) + .asynchronouslyInflateView( + eq(SecurityMode.PIN), + any(), + onViewInflatedCallbackArgumentCaptor.capture() + ) + } @Test fun hasDismissActions() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt index a3f7fc5fc8cf..e0ae0c359f07 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt @@ -131,14 +131,12 @@ class KeyguardKeyEventInteractorTest : SysuiTestCase() { } @Test - fun dispatchKeyEvent_menuActionUp_interactiveKeyguard_collapsesShade() { + fun dispatchKeyEvent_menuActionUp_interactiveKeyguard_showsPrimaryBouncer() { keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode) whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true) - val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU) - assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue() - verify(shadeController).animateCollapseShadeForced() + verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_MENU) } @Test @@ -147,42 +145,48 @@ class KeyguardKeyEventInteractorTest : SysuiTestCase() { whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED) whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true) - // action down: does NOT collapse the shade - val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU) - assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse() - verify(shadeController, never()).animateCollapseShadeForced() - - // action up: collapses the shade - val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU) - assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue() - verify(shadeController).animateCollapseShadeForced() + verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_MENU) } @Test - fun dispatchKeyEvent_menuActionUp_nonInteractiveKeyguard_neverCollapsesShade() { + fun dispatchKeyEvent_menuActionUp_nonInteractiveKeyguard_doNothing() { keyguardInteractorWithDependencies.repository.setWakefulnessModel(asleepWakefulnessMode) whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true) - val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU) - assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse() - verify(shadeController, never()).animateCollapseShadeForced() + verifyActionsDoNothing(KeyEvent.KEYCODE_MENU) } @Test - fun dispatchKeyEvent_spaceActionUp_interactiveKeyguard_collapsesShade() { + fun dispatchKeyEvent_spaceActionUp_interactiveKeyguard_showsPrimaryBouncer() { keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode) whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) - // action down: does NOT collapse the shade - val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SPACE) - assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse() - verify(shadeController, never()).animateCollapseShadeForced() + verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_SPACE) + } - // action up: collapses the shade - val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SPACE) - assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue() - verify(shadeController).animateCollapseShadeForced() + @Test + fun dispatchKeyEvent_spaceActionUp_shadeLocked_collapsesShade() { + keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode) + whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED) + + verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_SPACE) + } + + @Test + fun dispatchKeyEvent_enterActionUp_interactiveKeyguard_showsPrimaryBouncer() { + keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + + verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_ENTER) + } + + @Test + fun dispatchKeyEvent_enterActionUp_shadeLocked_collapsesShade() { + keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode) + whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED) + + verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_ENTER) } @Test @@ -252,4 +256,42 @@ class KeyguardKeyEventInteractorTest : SysuiTestCase() { .isFalse() verify(statusBarKeyguardViewManager, never()).interceptMediaKey(any()) } + + private fun verifyActionUpCollapsesTheShade(keycode: Int) { + // action down: does NOT collapse the shade + val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode) + assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse() + verify(shadeController, never()).animateCollapseShadeForced() + + // action up: collapses the shade + val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode) + assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue() + verify(shadeController).animateCollapseShadeForced() + } + + private fun verifyActionUpShowsPrimaryBouncer(keycode: Int) { + // action down: does NOT collapse the shade + val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode) + assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse() + verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any()) + + // action up: collapses the shade + val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode) + assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue() + verify(statusBarKeyguardViewManager).showPrimaryBouncer(eq(true)) + } + + private fun verifyActionsDoNothing(keycode: Int) { + // action down: does nothing + val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode) + assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse() + verify(shadeController, never()).animateCollapseShadeForced() + verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any()) + + // action up: doesNothing + val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode) + assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse() + verify(shadeController, never()).animateCollapseShadeForced() + verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any()) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java index 9781baae2f89..64d3b822f13c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java @@ -53,7 +53,6 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.dump.nano.SystemUIProtoDump; import com.android.systemui.flags.FakeFeatureFlags; -import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.PluginManager; @@ -65,6 +64,7 @@ import com.android.systemui.qs.external.CustomTileStatePersister; import com.android.systemui.qs.external.TileLifecycleManager; import com.android.systemui.qs.external.TileServiceKey; import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.settings.UserFileManager; import com.android.systemui.settings.UserTracker; @@ -131,6 +131,8 @@ public class QSTileHostTest extends SysuiTestCase { private FakeFeatureFlags mFeatureFlags; + private QSPipelineFlagsRepository mQSPipelineFlagsRepository; + private FakeExecutor mMainExecutor; private QSTileHost mQSTileHost; @@ -142,6 +144,7 @@ public class QSTileHostTest extends SysuiTestCase { mFeatureFlags.set(Flags.QS_PIPELINE_NEW_HOST, false); mFeatureFlags.set(Flags.QS_PIPELINE_AUTO_ADD, false); + mQSPipelineFlagsRepository = new QSPipelineFlagsRepository(mFeatureFlags); mMainExecutor = new FakeExecutor(new FakeSystemClock()); @@ -164,7 +167,7 @@ public class QSTileHostTest extends SysuiTestCase { mQSTileHost = new TestQSTileHost(mContext, mDefaultFactory, mMainExecutor, mPluginManager, mTunerService, () -> mAutoTiles, mShadeController, mQSLogger, mUserTracker, mSecureSettings, mCustomTileStatePersister, - mTileLifecycleManagerFactory, mUserFileManager, mFeatureFlags); + mTileLifecycleManagerFactory, mUserFileManager, mQSPipelineFlagsRepository); mMainExecutor.runAllReady(); mSecureSettings.registerContentObserverForUser(SETTING, new ContentObserver(null) { @@ -708,7 +711,7 @@ public class QSTileHostTest extends SysuiTestCase { UserTracker userTracker, SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister, TileLifecycleManager.Factory tileLifecycleManagerFactory, - UserFileManager userFileManager, FeatureFlags featureFlags) { + UserFileManager userFileManager, QSPipelineFlagsRepository featureFlags) { super(context, defaultFactory, mainExecutor, pluginManager, tunerService, autoTiles, shadeController, qsLogger, userTracker, secureSettings, customTileStatePersister, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt index 66895145b5b9..54a93608a10d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt @@ -42,6 +42,7 @@ import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesCompon import com.android.systemui.qs.pipeline.data.repository.FakeTileSpecRepository import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository import com.android.systemui.qs.pipeline.domain.model.TileModel +import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.qs.toProto @@ -79,6 +80,7 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() { private val customTileAddedRepository: CustomTileAddedRepository = FakeCustomTileAddedRepository() private val featureFlags = FakeFeatureFlags() + private val pipelineFlags = QSPipelineFlagsRepository(featureFlags) private val tileLifecycleManagerFactory = TLMFactory() @Mock private lateinit var customTileStatePersister: CustomTileStatePersister @@ -120,7 +122,7 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() { backgroundDispatcher = testDispatcher, scope = testScope.backgroundScope, logger = logger, - featureFlags = featureFlags, + featureFlags = pipelineFlags, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt new file mode 100644 index 000000000000..489221e86d0b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt @@ -0,0 +1,61 @@ +package com.android.systemui.qs.pipeline.shared + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameter +import org.junit.runners.Parameterized.Parameters + +@SmallTest +@RunWith(Parameterized::class) +class QSPipelineFlagsRepositoryTest : SysuiTestCase() { + companion object { + @Parameters( + name = + """ +WHEN: qs_pipeline_new_host = {0}, qs_pipeline_auto_add = {1} +THEN: pipelineNewHost = {2}, pipelineAutoAdd = {3} + """ + ) + @JvmStatic + fun data(): List<Array<Boolean>> = + (0 until 4).map { combination -> + val qs_pipeline_new_host = combination and 0b10 != 0 + val qs_pipeline_auto_add = combination and 0b01 != 0 + arrayOf( + qs_pipeline_new_host, + qs_pipeline_auto_add, + /* pipelineNewHost = */ qs_pipeline_new_host, + /* pipelineAutoAdd = */ qs_pipeline_new_host && qs_pipeline_auto_add, + ) + } + } + + @JvmField @Parameter(0) var qsPipelineNewHostFlag: Boolean = false + @JvmField @Parameter(1) var qsPipelineAutoAddFlag: Boolean = false + @JvmField @Parameter(2) var pipelineNewHostExpected: Boolean = false + @JvmField @Parameter(3) var pipelineAutoAddExpected: Boolean = false + + private val featureFlags = FakeFeatureFlags() + private val pipelineFlags = QSPipelineFlagsRepository(featureFlags) + + @Before + fun setUp() { + featureFlags.apply { + set(Flags.QS_PIPELINE_NEW_HOST, qsPipelineNewHostFlag) + set(Flags.QS_PIPELINE_AUTO_ADD, qsPipelineAutoAddFlag) + } + } + + @Test + fun flagLogic() { + assertThat(pipelineFlags.pipelineHostEnabled).isEqualTo(pipelineNewHostExpected) + assertThat(pipelineFlags.pipelineAutoAddEnabled).isEqualTo(pipelineAutoAddExpected) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index eb4ae1a743ef..7aeafeb2a752 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -539,6 +539,23 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + public void onKeyguardStatusViewHeightChange_animatesNextTopPaddingChangeForNSSL() { + ArgumentCaptor<View.OnLayoutChangeListener> captor = + ArgumentCaptor.forClass(View.OnLayoutChangeListener.class); + verify(mKeyguardStatusView).addOnLayoutChangeListener(captor.capture()); + View.OnLayoutChangeListener listener = captor.getValue(); + + clearInvocations(mNotificationStackScrollLayoutController); + + when(mKeyguardStatusView.getHeight()).thenReturn(0); + listener.onLayoutChange(mKeyguardStatusView, /* left= */ 0, /* top= */ 0, /* right= */ + 0, /* bottom= */ 0, /* oldLeft= */ 0, /* oldTop= */ 0, /* oldRight= */ + 0, /* oldBottom = */ 200); + + verify(mNotificationStackScrollLayoutController).animateNextTopPaddingChange(); + } + + @Test public void testCanCollapsePanelOnTouch_trueForKeyGuard() { mStatusBarStateController.setState(KEYGUARD); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 1edeeffe5217..dc506a5b1d25 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.shade import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper +import android.view.KeyEvent import android.view.MotionEvent import android.view.ViewGroup import androidx.test.filters.SmallTest @@ -38,6 +39,7 @@ import com.android.systemui.dock.DockManager import com.android.systemui.dump.logcatLogBuffer import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.TransitionStep @@ -118,6 +120,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor @Mock lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel + @Mock lateinit var keyEventInteractor: KeyEventInteractor private val notificationExpansionRepository = NotificationExpansionRepository() private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler> @@ -188,7 +191,8 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { CountDownTimerUtil(), featureFlags ), - BouncerLogger(logcatLogBuffer("BouncerLog")) + BouncerLogger(logcatLogBuffer("BouncerLog")), + keyEventInteractor, ) underTest.setupExpandedStatusBar() @@ -345,6 +349,27 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { verify(view).findViewById<ViewGroup>(R.id.keyguard_message_area) } + @Test + fun forwardsDispatchKeyEvent() { + val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B) + interactionEventHandler.dispatchKeyEvent(keyEvent) + verify(keyEventInteractor).dispatchKeyEvent(keyEvent) + } + + @Test + fun forwardsDispatchKeyEventPreIme() { + val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B) + interactionEventHandler.dispatchKeyEventPreIme(keyEvent) + verify(keyEventInteractor).dispatchKeyEventPreIme(keyEvent) + } + + @Test + fun forwardsInterceptMediaKey() { + val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP) + interactionEventHandler.interceptMediaKey(keyEvent) + verify(keyEventInteractor).interceptMediaKey(keyEvent) + } + companion object { private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0) private const val VIEW_BOTTOM = 100 diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index 829184c4f05a..66d48d64b3a3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.dock.DockManager import com.android.systemui.dump.logcatLogBuffer import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel @@ -198,7 +199,8 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { CountDownTimerUtil(), featureFlags ), - BouncerLogger(logcatLogBuffer("BouncerLog")) + BouncerLogger(logcatLogBuffer("BouncerLog")), + Mockito.mock(KeyEventInteractor::class.java), ) controller.setupExpandedStatusBar() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt index 9495fdd50a7e..ecaf13711b52 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt @@ -227,6 +227,25 @@ class ClockRegistryTest : SysuiTestCase() { } @Test + fun activeClockId_changeAfterPluginConnected() { + val plugin1 = FakeClockPlugin() + .addClock("clock_1", "clock 1") + .addClock("clock_2", "clock 2") + + val plugin2 = FakeClockPlugin() + .addClock("clock_3", "clock 3", { mockClock }) + .addClock("clock_4", "clock 4") + + registry.applySettings(ClockSettings("clock_3", null)) + + pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle) + assertEquals(DEFAULT_CLOCK_ID, registry.activeClockId) + + pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle) + assertEquals("clock_3", registry.activeClockId) + } + + @Test fun createDefaultClock_pluginDisconnected() { val plugin1 = FakeClockPlugin() .addClock("clock_1", "clock 1") diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 0dc1d9a4b177..6b3bd22d5e62 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -1802,6 +1802,15 @@ public class ScrimControllerTest extends SysuiTestCase { assertFalse(ScrimState.UNLOCKED.mAnimateChange); } + @Test + public void testNotifScrimAlpha_1f_afterUnlockFinishedAndExpanded() { + mScrimController.transitionTo(ScrimState.KEYGUARD); + when(mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()).thenReturn(true); + mScrimController.transitionTo(ScrimState.UNLOCKED); + mScrimController.onUnlockAnimationFinished(); + assertAlphaAfterExpansion(mNotificationsScrim, 1f, 1f); + } + private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) { mScrimController.setRawPanelExpansionFraction(expansion); finishAnimationsImmediately(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index 5f0011bc809a..fdc4f372b329 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -32,7 +32,6 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assume.assumeNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; @@ -500,123 +499,45 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void ifPortraitHalfOpen_drawVerticallyTop() { - DevicePostureController devicePostureController = mock(DevicePostureController.class); - when(devicePostureController.getDevicePosture()) - .thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED); - - VolumeDialogImpl dialog = new VolumeDialogImpl( - getContext(), - mVolumeDialogController, - mAccessibilityMgr, - mDeviceProvisionedController, - mConfigurationController, - mMediaOutputDialogFactory, - mVolumePanelFactory, - mActivityStarter, - mInteractionJankMonitor, - false, - mCsdWarningDialogFactory, - devicePostureController, - mTestableLooper.getLooper(), - mDumpManager, - mFeatureFlags - ); - dialog.init(0 , null); - - verify(devicePostureController).addCallback(any()); - dialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED); + mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED); mTestableLooper.processAllMessages(); // let dismiss() finish setOrientation(Configuration.ORIENTATION_PORTRAIT); // Call show() to trigger layout updates before verifying position - dialog.show(SHOW_REASON_UNKNOWN); + mDialog.show(SHOW_REASON_UNKNOWN); mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect - int gravity = dialog.getWindowGravity(); + int gravity = mDialog.getWindowGravity(); assertEquals(Gravity.TOP, gravity & Gravity.VERTICAL_GRAVITY_MASK); - - cleanUp(dialog); } @Test public void ifPortraitAndOpen_drawCenterVertically() { - DevicePostureController devicePostureController = mock(DevicePostureController.class); - when(devicePostureController.getDevicePosture()) - .thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED); - - VolumeDialogImpl dialog = new VolumeDialogImpl( - getContext(), - mVolumeDialogController, - mAccessibilityMgr, - mDeviceProvisionedController, - mConfigurationController, - mMediaOutputDialogFactory, - mVolumePanelFactory, - mActivityStarter, - mInteractionJankMonitor, - false, - mCsdWarningDialogFactory, - devicePostureController, - mTestableLooper.getLooper(), - mDumpManager, - mFeatureFlags - ); - dialog.init(0, null); - - verify(devicePostureController).addCallback(any()); - dialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_OPENED); + mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_OPENED); mTestableLooper.processAllMessages(); // let dismiss() finish setOrientation(Configuration.ORIENTATION_PORTRAIT); - dialog.show(SHOW_REASON_UNKNOWN); + mDialog.show(SHOW_REASON_UNKNOWN); mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect - int gravity = dialog.getWindowGravity(); + int gravity = mDialog.getWindowGravity(); assertEquals(Gravity.CENTER_VERTICAL, gravity & Gravity.VERTICAL_GRAVITY_MASK); - - cleanUp(dialog); } @Test public void ifLandscapeAndHalfOpen_drawCenterVertically() { - DevicePostureController devicePostureController = mock(DevicePostureController.class); - when(devicePostureController.getDevicePosture()) - .thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED); - - VolumeDialogImpl dialog = new VolumeDialogImpl( - getContext(), - mVolumeDialogController, - mAccessibilityMgr, - mDeviceProvisionedController, - mConfigurationController, - mMediaOutputDialogFactory, - mVolumePanelFactory, - mActivityStarter, - mInteractionJankMonitor, - false, - mCsdWarningDialogFactory, - devicePostureController, - mTestableLooper.getLooper(), - mDumpManager, - mFeatureFlags - ); - dialog.init(0, null); - - verify(devicePostureController).addCallback(any()); - dialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED); + mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED); mTestableLooper.processAllMessages(); // let dismiss() finish setOrientation(Configuration.ORIENTATION_LANDSCAPE); - dialog.show(SHOW_REASON_UNKNOWN); + mDialog.show(SHOW_REASON_UNKNOWN); mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect - int gravity = dialog.getWindowGravity(); + int gravity = mDialog.getWindowGravity(); assertEquals(Gravity.CENTER_VERTICAL, gravity & Gravity.VERTICAL_GRAVITY_MASK); - - cleanUp(dialog); } @Test @@ -627,31 +548,9 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void dialogDestroy_removesPostureControllerCallback() { - VolumeDialogImpl dialog = new VolumeDialogImpl( - getContext(), - mVolumeDialogController, - mAccessibilityMgr, - mDeviceProvisionedController, - mConfigurationController, - mMediaOutputDialogFactory, - mVolumePanelFactory, - mActivityStarter, - mInteractionJankMonitor, - false, - mCsdWarningDialogFactory, - mPostureController, - mTestableLooper.getLooper(), - mDumpManager, - mFeatureFlags - ); - dialog.init(0, null); - verify(mPostureController, never()).removeCallback(any()); - dialog.destroy(); - + mDialog.destroy(); verify(mPostureController).removeCallback(any()); - - cleanUp(dialog); } private void setOrientation(int orientation) { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index ee6c28ed3fe1..d1be5a9e971d 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -43,6 +43,7 @@ import android.companion.virtual.audio.IAudioConfigChangedCallback; import android.companion.virtual.audio.IAudioRoutingCallback; import android.companion.virtual.sensor.VirtualSensor; import android.companion.virtual.sensor.VirtualSensorEvent; +import android.content.AttributionSource; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -125,6 +126,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private final VirtualDeviceManagerService mService; private final PendingTrampolineCallback mPendingTrampolineCallback; private final int mOwnerUid; + private final String mOwnerPackageName; private int mDeviceId; private @Nullable String mPersistentDeviceId; // Thou shall not hold the mVirtualDeviceLock over the mInputController calls. @@ -196,7 +198,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub AssociationInfo associationInfo, VirtualDeviceManagerService service, IBinder token, - int ownerUid, + AttributionSource attributionSource, int deviceId, CameraAccessController cameraAccessController, PendingTrampolineCallback pendingTrampolineCallback, @@ -209,7 +211,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub associationInfo, service, token, - ownerUid, + attributionSource, deviceId, /* inputController= */ null, cameraAccessController, @@ -227,7 +229,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub AssociationInfo associationInfo, VirtualDeviceManagerService service, IBinder token, - int ownerUid, + AttributionSource attributionSource, int deviceId, InputController inputController, CameraAccessController cameraAccessController, @@ -238,7 +240,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub VirtualDeviceParams params, DisplayManagerGlobal displayManager) { super(PermissionEnforcer.fromContext(context)); - UserHandle ownerUserHandle = UserHandle.getUserHandleForUid(ownerUid); + mOwnerPackageName = attributionSource.getPackageName(); + UserHandle ownerUserHandle = UserHandle.getUserHandleForUid(attributionSource.getUid()); mContext = context.createContextAsUser(ownerUserHandle, 0); mAssociationInfo = associationInfo; mPersistentDeviceId = PERSISTENT_ID_PREFIX_CDM_ASSOCIATION + associationInfo.getId(); @@ -247,7 +250,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub mActivityListener = activityListener; mSoundEffectListener = soundEffectListener; mRunningAppsChangedCallback = runningAppsChangedCallback; - mOwnerUid = ownerUid; + mOwnerUid = attributionSource.getUid(); mDeviceId = deviceId; mAppToken = token; mParams = params; @@ -771,7 +774,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub fout.println(" VirtualDevice: "); fout.println(" mDeviceId: " + mDeviceId); fout.println(" mAssociationId: " + mAssociationInfo.getId()); - fout.println(" mParams: " + mParams); + fout.println(" mOwnerPackageName: " + mOwnerPackageName); + fout.println(" mParams: "); + mParams.dump(fout, " "); fout.println(" mVirtualDisplayIds: "); synchronized (mVirtualDeviceLock) { for (int i = 0; i < mVirtualDisplays.size(); i++) { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index 77508a8fcc0d..e558498e4715 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -35,6 +35,7 @@ import android.companion.virtual.VirtualDevice; import android.companion.virtual.VirtualDeviceManager; import android.companion.virtual.VirtualDeviceParams; import android.companion.virtual.sensor.VirtualSensor; +import android.content.AttributionSource; import android.content.Context; import android.content.Intent; import android.hardware.display.IVirtualDisplayCallback; @@ -314,13 +315,15 @@ public class VirtualDeviceManagerService extends SystemService { @Override // Binder call public IVirtualDevice createVirtualDevice( IBinder token, - String packageName, + AttributionSource attributionSource, int associationId, @NonNull VirtualDeviceParams params, @NonNull IVirtualDeviceActivityListener activityListener, @NonNull IVirtualDeviceSoundEffectListener soundEffectListener) { createVirtualDevice_enforcePermission(); + attributionSource.enforceCallingUid(); final int callingUid = getCallingUid(); + final String packageName = attributionSource.getPackageName(); if (!PermissionUtils.validateCallingPackageName(getContext(), packageName)) { throw new SecurityException( "Package name " + packageName + " does not belong to calling uid " @@ -340,10 +343,9 @@ public class VirtualDeviceManagerService extends SystemService { final int deviceId = sNextUniqueIndex.getAndIncrement(); final Consumer<ArraySet<Integer>> runningAppsChangedCallback = runningUids -> notifyRunningAppsChanged(deviceId, runningUids); - VirtualDeviceImpl virtualDevice = new VirtualDeviceImpl(getContext(), - associationInfo, VirtualDeviceManagerService.this, token, callingUid, - deviceId, cameraAccessController, - mPendingTrampolineCallback, activityListener, + VirtualDeviceImpl virtualDevice = new VirtualDeviceImpl(getContext(), associationInfo, + VirtualDeviceManagerService.this, token, attributionSource, deviceId, + cameraAccessController, mPendingTrampolineCallback, activityListener, soundEffectListener, runningAppsChangedCallback, params); synchronized (mVirtualDeviceManagerLock) { if (mVirtualDevices.size() == 0) { diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 0e4465dd7f6a..1d09dce1132a 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -87,7 +87,6 @@ public class SettingsToPropertiesMapper { DeviceConfig.NAMESPACE_CAMERA_NATIVE, DeviceConfig.NAMESPACE_CONFIGURATION, DeviceConfig.NAMESPACE_CONNECTIVITY, - DeviceConfig.NAMESPACE_CORE_EXPERIMENTS_TEAM_INTERNAL, DeviceConfig.NAMESPACE_EDGETPU_NATIVE, DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT, DeviceConfig.NAMESPACE_INTELLIGENCE_CONTENT_SUGGESTIONS, @@ -115,19 +114,29 @@ public class SettingsToPropertiesMapper { NAMESPACE_TETHERING_U_OR_LATER_NATIVE }; + // All the aconfig flags under the listed DeviceConfig scopes will be synced to native level. + @VisibleForTesting + static final String[] sDeviceConfigAconfigScopes = new String[] { + DeviceConfig.NAMESPACE_CORE_EXPERIMENTS_TEAM_INTERNAL, + }; + private final String[] mGlobalSettings; private final String[] mDeviceConfigScopes; + private final String[] mDeviceConfigAconfigScopes; + private final ContentResolver mContentResolver; @VisibleForTesting protected SettingsToPropertiesMapper(ContentResolver contentResolver, String[] globalSettings, - String[] deviceConfigScopes) { + String[] deviceConfigScopes, + String[] deviceConfigAconfigScopes) { mContentResolver = contentResolver; mGlobalSettings = globalSettings; mDeviceConfigScopes = deviceConfigScopes; + mDeviceConfigAconfigScopes = deviceConfigAconfigScopes; } @VisibleForTesting @@ -173,6 +182,36 @@ public class SettingsToPropertiesMapper { return; } setProperty(propertyName, properties.getString(key, null)); + + // for legacy namespaces, they can also be used for trunk stable + // purposes. so push flag also into trunk stable slot in sys prop, + // later all legacy usage will be refactored and the sync to old + // sys prop slot can be removed. + String aconfigPropertyName = makeAconfigFlagPropertyName(scope, key); + if (aconfigPropertyName == null) { + log("unable to construct system property for " + scope + "/" + + key); + return; + } + setProperty(aconfigPropertyName, properties.getString(key, null)); + } + }); + } + + for (String deviceConfigAconfigScope : mDeviceConfigAconfigScopes) { + DeviceConfig.addOnPropertiesChangedListener( + deviceConfigAconfigScope, + AsyncTask.THREAD_POOL_EXECUTOR, + (DeviceConfig.Properties properties) -> { + String scope = properties.getNamespace(); + for (String key : properties.getKeyset()) { + String aconfigPropertyName = makeAconfigFlagPropertyName(scope, key); + if (aconfigPropertyName == null) { + log("unable to construct system property for " + scope + "/" + + key); + return; + } + setProperty(aconfigPropertyName, properties.getString(key, null)); } }); } @@ -180,7 +219,10 @@ public class SettingsToPropertiesMapper { public static SettingsToPropertiesMapper start(ContentResolver contentResolver) { SettingsToPropertiesMapper mapper = new SettingsToPropertiesMapper( - contentResolver, sGlobalSettings, sDeviceConfigScopes); + contentResolver, + sGlobalSettings, + sDeviceConfigScopes, + sDeviceConfigAconfigScopes); mapper.updatePropertiesFromSettings(); return mapper; } @@ -243,6 +285,28 @@ public class SettingsToPropertiesMapper { return propertyName; } + /** + * system property name constructing rule for aconfig flags: + * "persist.device_config.aconfig_flags.[category_name].[flag_name]". + * If the name contains invalid characters or substrings for system property name, + * will return null. + * @param categoryName + * @param flagName + * @return + */ + @VisibleForTesting + static String makeAconfigFlagPropertyName(String categoryName, String flagName) { + String propertyName = SYSTEM_PROPERTY_PREFIX + "aconfig_flags." + + categoryName + "." + flagName; + + if (!propertyName.matches(SYSTEM_PROPERTY_VALID_CHARACTERS_REGEX) + || propertyName.contains(SYSTEM_PROPERTY_INVALID_SUBSTRING)) { + return null; + } + + return propertyName; + } + private void setProperty(String key, String value) { // Check if need to clear the property if (value == null) { diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index d89171d94478..4e01997e678f 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -391,7 +391,6 @@ public class AudioDeviceBroker { final boolean wasBtScoRequested = isBluetoothScoRequested(); CommunicationRouteClient client; - // Save previous client route in case of failure to start BT SCO audio AudioDeviceAttributes prevClientDevice = null; boolean prevPrivileged = false; @@ -1043,7 +1042,7 @@ public class AudioDeviceBroker { synchronized (mBluetoothAudioStateLock) { mBluetoothScoOn = on; updateAudioHalBluetoothState(); - postUpdateCommunicationRouteClient(eventSource); + postUpdateCommunicationRouteClient(isBluetoothScoRequested(), eventSource); } } @@ -1395,8 +1394,10 @@ public class AudioDeviceBroker { MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET, SENDMSG_QUEUE, capturePreset); } - /*package*/ void postUpdateCommunicationRouteClient(String eventSource) { - sendLMsgNoDelay(MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT, SENDMSG_QUEUE, eventSource); + /*package*/ void postUpdateCommunicationRouteClient( + boolean wasBtScoRequested, String eventSource) { + sendILMsgNoDelay(MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT, SENDMSG_QUEUE, + wasBtScoRequested ? 1 : 0, eventSource); } /*package*/ void postSetCommunicationDeviceForClient(CommunicationDeviceInfo info) { @@ -1708,7 +1709,8 @@ public class AudioDeviceBroker { : AudioSystem.STREAM_DEFAULT); if (btInfo.mProfile == BluetoothProfile.LE_AUDIO || btInfo.mProfile == BluetoothProfile.HEARING_AID) { - onUpdateCommunicationRouteClient("setBluetoothActiveDevice"); + onUpdateCommunicationRouteClient(isBluetoothScoRequested(), + "setBluetoothActiveDevice"); } } } @@ -1762,9 +1764,11 @@ public class AudioDeviceBroker { case MSG_I_SET_MODE_OWNER: synchronized (mSetModeLock) { synchronized (mDeviceStateLock) { + boolean wasBtScoRequested = isBluetoothScoRequested(); mAudioModeOwner = (AudioModeInfo) msg.obj; if (mAudioModeOwner.mMode != AudioSystem.MODE_RINGTONE) { - onUpdateCommunicationRouteClient("setNewModeOwner"); + onUpdateCommunicationRouteClient( + wasBtScoRequested, "setNewModeOwner"); } } } @@ -1787,10 +1791,10 @@ public class AudioDeviceBroker { } break; - case MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT: + case MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT: synchronized (mSetModeLock) { synchronized (mDeviceStateLock) { - onUpdateCommunicationRouteClient((String) msg.obj); + onUpdateCommunicationRouteClient(msg.arg1 == 1, (String) msg.obj); } } break; @@ -1971,7 +1975,7 @@ public class AudioDeviceBroker { private static final int MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET = 38; private static final int MSG_L_SET_COMMUNICATION_DEVICE_FOR_CLIENT = 42; - private static final int MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT = 43; + private static final int MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT = 43; private static final int MSG_I_SCO_AUDIO_STATE_CHANGED = 44; private static final int MSG_L_BT_ACTIVE_DEVICE_CHANGE_EXT = 45; @@ -2328,16 +2332,20 @@ public class AudioDeviceBroker { */ // @GuardedBy("mSetModeLock") @GuardedBy("mDeviceStateLock") - private void onUpdateCommunicationRouteClient(String eventSource) { - updateCommunicationRoute(eventSource); + private void onUpdateCommunicationRouteClient(boolean wasBtScoRequested, String eventSource) { CommunicationRouteClient crc = topCommunicationRouteClient(); if (AudioService.DEBUG_COMM_RTE) { - Log.v(TAG, "onUpdateCommunicationRouteClient, crc: " - + crc + " eventSource: " + eventSource); + Log.v(TAG, "onUpdateCommunicationRouteClient, crc: " + crc + + " wasBtScoRequested: " + wasBtScoRequested + " eventSource: " + eventSource); } if (crc != null) { setCommunicationRouteForClient(crc.getBinder(), crc.getUid(), crc.getDevice(), BtHelper.SCO_MODE_UNDEFINED, crc.isPrivileged(), eventSource); + } else { + if (!isBluetoothScoRequested() && wasBtScoRequested) { + mBtHelper.stopBluetoothSco(eventSource); + } + updateCommunicationRoute(eventSource); } } @@ -2431,6 +2439,7 @@ public class AudioDeviceBroker { List<AudioRecordingConfiguration> recordConfigs) { synchronized (mSetModeLock) { synchronized (mDeviceStateLock) { + final boolean wasBtScoRequested = isBluetoothScoRequested(); boolean updateCommunicationRoute = false; for (CommunicationRouteClient crc : mCommunicationRouteClients) { boolean wasActive = crc.isActive(); @@ -2459,7 +2468,8 @@ public class AudioDeviceBroker { } } if (updateCommunicationRoute) { - postUpdateCommunicationRouteClient("updateCommunicationRouteClientsActivity"); + postUpdateCommunicationRouteClient( + wasBtScoRequested, "updateCommunicationRouteClientsActivity"); } } } diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 3560797ce2cf..a4d26d3e96c0 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -329,7 +329,7 @@ public class BtHelper { default: break; } - if(broadcast) { + if (broadcast) { broadcastScoConnectionState(scoAudioState); //FIXME: this is to maintain compatibility with deprecated intent // AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate. @@ -718,8 +718,10 @@ public class BtHelper { checkScoAudioState(); if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { // Make sure that the state transitions to CONNECTING even if we cannot initiate - // the connection. - broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING); + // the connection except if already connected internally + if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL) { + broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING); + } switch (mScoAudioState) { case SCO_STATE_INACTIVE: mScoAudioMode = scoAudioMode; @@ -775,7 +777,7 @@ public class BtHelper { broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTED); break; case SCO_STATE_ACTIVE_INTERNAL: - Log.w(TAG, "requestScoState: already in ACTIVE mode, simply return"); + // Already in ACTIVE mode, simply return break; case SCO_STATE_ACTIVE_EXTERNAL: /* Confirm SCO Audio connection to requesting app as it is already connected diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java index d57dc471694e..8642fb888556 100644 --- a/services/core/java/com/android/server/display/DisplayDevice.java +++ b/services/core/java/com/android/server/display/DisplayDevice.java @@ -16,6 +16,9 @@ package com.android.server.display; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + import android.annotation.Nullable; import android.content.Context; import android.graphics.Point; @@ -132,12 +135,15 @@ abstract class DisplayDevice { /** * Returns the default size of the surface associated with the display, or null if the surface * is not provided for layer mirroring by SurfaceFlinger. For non virtual displays, this will - * be the actual display device's size. + * be the actual display device's size, reflecting the current rotation. */ @Nullable public Point getDisplaySurfaceDefaultSizeLocked() { DisplayDeviceInfo displayDeviceInfo = getDisplayDeviceInfoLocked(); - return new Point(displayDeviceInfo.width, displayDeviceInfo.height); + final boolean isRotated = mCurrentOrientation == ROTATION_90 + || mCurrentOrientation == ROTATION_270; + return isRotated ? new Point(displayDeviceInfo.height, displayDeviceInfo.width) + : new Point(displayDeviceInfo.width, displayDeviceInfo.height); } /** @@ -358,7 +364,7 @@ abstract class DisplayDevice { } boolean isRotated = (mCurrentOrientation == Surface.ROTATION_90 - || mCurrentOrientation == Surface.ROTATION_270); + || mCurrentOrientation == ROTATION_270); DisplayDeviceInfo info = getDisplayDeviceInfoLocked(); viewport.deviceWidth = isRotated ? info.height : info.width; viewport.deviceHeight = isRotated ? info.width : info.height; diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index c131226a60dd..01eceda202d2 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -2093,7 +2093,7 @@ public class DisplayDeviceConfig { /** Loads the refresh rate profiles. */ private void loadRefreshRateZoneProfiles(RefreshRateConfigs refreshRateConfigs) { - if (refreshRateConfigs == null) { + if (refreshRateConfigs == null || refreshRateConfigs.getRefreshRateZoneProfiles() == null) { return; } for (RefreshRateZone zone : diff --git a/services/core/java/com/android/server/input/FocusEventDebugView.java b/services/core/java/com/android/server/input/FocusEventDebugView.java index 39519ef79b1a..4b8fabde7d35 100644 --- a/services/core/java/com/android/server/input/FocusEventDebugView.java +++ b/services/core/java/com/android/server/input/FocusEventDebugView.java @@ -24,9 +24,11 @@ import android.animation.LayoutTransition; import android.annotation.AnyThread; import android.annotation.Nullable; import android.content.Context; +import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.ColorMatrixColorFilter; +import android.graphics.Paint; import android.graphics.Typeface; import android.util.DisplayMetrics; import android.util.Pair; @@ -50,7 +52,10 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.function.Supplier; /** @@ -70,9 +75,11 @@ class FocusEventDebugView extends RelativeLayout { private static final int KEY_VIEW_VERTICAL_PADDING_DP = 8; private static final int KEY_VIEW_MIN_WIDTH_DP = 32; private static final int KEY_VIEW_TEXT_SIZE_SP = 12; + private static final double ROTATY_GRAPH_HEIGHT_FRACTION = 0.5; private final InputManagerService mService; private final int mOuterPadding; + private final DisplayMetrics mDm; // Tracks all keys that are currently pressed/down. private final Map<Pair<Integer /*deviceId*/, Integer /*scanCode*/>, PressedKeyView> @@ -87,21 +94,26 @@ class FocusEventDebugView extends RelativeLayout { private final Supplier<RotaryInputValueView> mRotaryInputValueViewFactory; @Nullable private RotaryInputValueView mRotaryInputValueView; + private final Supplier<RotaryInputGraphView> mRotaryInputGraphViewFactory; + @Nullable + private RotaryInputGraphView mRotaryInputGraphView; @VisibleForTesting FocusEventDebugView(Context c, InputManagerService service, - Supplier<RotaryInputValueView> rotaryInputValueViewFactory) { + Supplier<RotaryInputValueView> rotaryInputValueViewFactory, + Supplier<RotaryInputGraphView> rotaryInputGraphViewFactory) { super(c); setFocusableInTouchMode(true); mService = service; mRotaryInputValueViewFactory = rotaryInputValueViewFactory; - final var dm = mContext.getResources().getDisplayMetrics(); - mOuterPadding = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, OUTER_PADDING_DP, dm); + mRotaryInputGraphViewFactory = rotaryInputGraphViewFactory; + mDm = mContext.getResources().getDisplayMetrics(); + mOuterPadding = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, OUTER_PADDING_DP, mDm); } FocusEventDebugView(Context c, InputManagerService service) { - this(c, service, () -> new RotaryInputValueView(c)); + this(c, service, () -> new RotaryInputValueView(c), () -> new RotaryInputGraphView(c)); } @Override @@ -196,6 +208,8 @@ class FocusEventDebugView extends RelativeLayout { mFocusEventDebugGlobalMonitor = null; removeView(mRotaryInputValueView); mRotaryInputValueView = null; + removeView(mRotaryInputGraphView); + mRotaryInputGraphView = null; return; } @@ -206,6 +220,12 @@ class FocusEventDebugView extends RelativeLayout { valueLayoutParams.addRule(CENTER_HORIZONTAL); valueLayoutParams.addRule(ALIGN_PARENT_BOTTOM); addView(mRotaryInputValueView, valueLayoutParams); + + mRotaryInputGraphView = mRotaryInputGraphViewFactory.get(); + LayoutParams graphLayoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, + (int) (ROTATY_GRAPH_HEIGHT_FRACTION * mDm.heightPixels)); + graphLayoutParams.addRule(CENTER_IN_PARENT); + addView(mRotaryInputGraphView, graphLayoutParams); } /** Report a key event to the debug view. */ @@ -276,6 +296,7 @@ class FocusEventDebugView extends RelativeLayout { float scrollAxisValue = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL); mRotaryInputValueView.updateValue(scrollAxisValue); + mRotaryInputGraphView.addValue(scrollAxisValue, motionEvent.getEventTime()); motionEvent.recycle(); } @@ -453,6 +474,8 @@ class FocusEventDebugView extends RelativeLayout { } } + // TODO(b/286086154): move RotaryInputGraphView and RotaryInputValueView to a subpackage. + /** Draws the most recent rotary input value and indicates whether the source is active. */ @VisibleForTesting static class RotaryInputValueView extends TextView { @@ -514,4 +537,312 @@ class FocusEventDebugView extends RelativeLayout { return String.format("%s%.1f", value < 0 ? "-" : "+", Math.abs(value)); } } + + /** + * Shows a graph with the rotary input values as a function of time. + * The graph gets reset if no action is received for a certain amount of time. + */ + @VisibleForTesting + static class RotaryInputGraphView extends View { + + private static final int FRAME_COLOR = 0xbf741b47; + private static final int FRAME_WIDTH_SP = 2; + private static final int FRAME_BORDER_GAP_SP = 10; + private static final int FRAME_TEXT_SIZE_SP = 10; + private static final int FRAME_TEXT_OFFSET_SP = 2; + private static final int GRAPH_COLOR = 0xffff00ff; + private static final int GRAPH_LINE_WIDTH_SP = 1; + private static final int GRAPH_POINT_RADIUS_SP = 4; + private static final long MAX_SHOWN_TIME_INTERVAL = TimeUnit.SECONDS.toMillis(5); + private static final float DEFAULT_FRAME_CENTER_POSITION = 0; + private static final int MAX_GRAPH_VALUES_SIZE = 400; + /** Maximum time between values so that they are considered part of the same gesture. */ + private static final long MAX_GESTURE_TIME = TimeUnit.SECONDS.toMillis(1); + + private final DisplayMetrics mDm; + /** + * Distance in position units (amount scrolled in display pixels) from the center to the + * top/bottom frame lines. + */ + private final float mFrameCenterToBorderDistance; + private final float mScaledVerticalScrollFactor; + private final Locale mDefaultLocale; + private final Paint mFramePaint = new Paint(); + private final Paint mFrameTextPaint = new Paint(); + private final Paint mGraphLinePaint = new Paint(); + private final Paint mGraphPointPaint = new Paint(); + + private final CyclicBuffer mGraphValues = new CyclicBuffer(MAX_GRAPH_VALUES_SIZE); + /** Position at which graph values are placed at the center of the graph. */ + private float mFrameCenterPosition = DEFAULT_FRAME_CENTER_POSITION; + + @VisibleForTesting + RotaryInputGraphView(Context c) { + super(c); + + mDm = mContext.getResources().getDisplayMetrics(); + // This makes the center-to-border distance equivalent to the display height, meaning + // that the total height of the graph is equivalent to 2x the display height. + mFrameCenterToBorderDistance = mDm.heightPixels; + mScaledVerticalScrollFactor = ViewConfiguration.get(c).getScaledVerticalScrollFactor(); + mDefaultLocale = Locale.getDefault(); + + mFramePaint.setColor(FRAME_COLOR); + mFramePaint.setStrokeWidth(applyDimensionSp(FRAME_WIDTH_SP, mDm)); + + mFrameTextPaint.setColor(GRAPH_COLOR); + mFrameTextPaint.setTextSize(applyDimensionSp(FRAME_TEXT_SIZE_SP, mDm)); + + mGraphLinePaint.setColor(GRAPH_COLOR); + mGraphLinePaint.setStrokeWidth(applyDimensionSp(GRAPH_LINE_WIDTH_SP, mDm)); + mGraphLinePaint.setStrokeCap(Paint.Cap.ROUND); + mGraphLinePaint.setStrokeJoin(Paint.Join.ROUND); + + mGraphPointPaint.setColor(GRAPH_COLOR); + mGraphPointPaint.setStrokeWidth(applyDimensionSp(GRAPH_POINT_RADIUS_SP, mDm)); + mGraphPointPaint.setStrokeCap(Paint.Cap.ROUND); + mGraphPointPaint.setStrokeJoin(Paint.Join.ROUND); + } + + /** + * Reads new scroll axis value and updates the list accordingly. Old positions are + * kept at the front (what you would get with getFirst), while the recent positions are + * kept at the back (what you would get with getLast). Also updates the frame center + * position to handle out-of-bounds cases. + */ + void addValue(float scrollAxisValue, long eventTime) { + // Remove values that are too old. + while (mGraphValues.getSize() > 0 + && (eventTime - mGraphValues.getFirst().mTime) > MAX_SHOWN_TIME_INTERVAL) { + mGraphValues.removeFirst(); + } + + // If there are no recent values, reset the frame center. + if (mGraphValues.getSize() == 0) { + mFrameCenterPosition = DEFAULT_FRAME_CENTER_POSITION; + } + + // Handle new value. We multiply the scroll axis value by the scaled scroll factor to + // get the amount of pixels to be scrolled. We also compute the accumulated position + // by adding the current value to the last one (if not empty). + final float displacement = scrollAxisValue * mScaledVerticalScrollFactor; + final float prevPos = (mGraphValues.getSize() == 0 ? 0 : mGraphValues.getLast().mPos); + final float pos = prevPos + displacement; + + mGraphValues.add(pos, eventTime); + + // The difference between the distance of the most recent position from the center + // frame (pos - mFrameCenterPosition) and the maximum allowed distance from the center + // frame (mFrameCenterToBorderDistance). + final float verticalDiff = Math.abs(pos - mFrameCenterPosition) + - mFrameCenterToBorderDistance; + // If needed, translate frame. + if (verticalDiff > 0) { + final int sign = pos - mFrameCenterPosition < 0 ? -1 : 1; + // Here, we update the center frame position by the exact amount needed for us to + // stay within the maximum allowed distance from the center frame. + mFrameCenterPosition += sign * verticalDiff; + } + + // Redraw canvas. + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Note: vertical coordinates in Canvas go from top to bottom, + // that is bottomY > middleY > topY. + final int verticalMargin = applyDimensionSp(FRAME_BORDER_GAP_SP, mDm); + final int topY = verticalMargin; + final int bottomY = getHeight() - verticalMargin; + final int middleY = (topY + bottomY) / 2; + + // Note: horizontal coordinates in Canvas go from left to right, + // that is rightX > leftX. + final int leftX = 0; + final int rightX = getWidth(); + + // Draw the frame, which includes 3 lines that show the maximum, + // minimum and middle positions of the graph. + canvas.drawLine(leftX, topY, rightX, topY, mFramePaint); + canvas.drawLine(leftX, middleY, rightX, middleY, mFramePaint); + canvas.drawLine(leftX, bottomY, rightX, bottomY, mFramePaint); + + // Draw the position that each frame line corresponds to. + final int frameTextOffset = applyDimensionSp(FRAME_TEXT_OFFSET_SP, mDm); + canvas.drawText( + String.format(mDefaultLocale, "%.1f", + mFrameCenterPosition + mFrameCenterToBorderDistance), + leftX, + topY - frameTextOffset, mFrameTextPaint + ); + canvas.drawText( + String.format(mDefaultLocale, "%.1f", mFrameCenterPosition), + leftX, + middleY - frameTextOffset, mFrameTextPaint + ); + canvas.drawText( + String.format(mDefaultLocale, "%.1f", + mFrameCenterPosition - mFrameCenterToBorderDistance), + leftX, + bottomY - frameTextOffset, mFrameTextPaint + ); + + // If there are no graph values to be drawn, stop here. + if (mGraphValues.getSize() == 0) { + return; + } + + // Draw the graph using the times and positions. + // We start at the most recent value (which should be drawn at the right) and move + // to the older values (which should be drawn to the left of more recent ones). Negative + // indices are handled by circuling back to the end of the buffer. + final long mostRecentTime = mGraphValues.getLast().mTime; + float prevCoordX = 0; + float prevCoordY = 0; + float prevAge = 0; + for (Iterator<GraphValue> iter = mGraphValues.reverseIterator(); iter.hasNext();) { + final GraphValue value = iter.next(); + + final int age = (int) (mostRecentTime - value.mTime); + final float pos = value.mPos; + + // We get the horizontal coordinate in time units from left to right with + // (MAX_SHOWN_TIME_INTERVAL - age). Then, we rescale it to match the canvas + // units by dividing it by the time-domain length (MAX_SHOWN_TIME_INTERVAL) + // and by multiplying it by the canvas length (rightX - leftX). Finally, we + // offset the coordinate by adding it to leftX. + final float coordX = leftX + ((float) (MAX_SHOWN_TIME_INTERVAL - age) + / MAX_SHOWN_TIME_INTERVAL) * (rightX - leftX); + + // We get the vertical coordinate in position units from middle to top with + // (pos - mFrameCenterPosition). Then, we rescale it to match the canvas + // units by dividing it by half of the position-domain length + // (mFrameCenterToBorderDistance) and by multiplying it by half of the canvas + // length (middleY - topY). Finally, we offset the coordinate by subtracting + // it from middleY (we can't "add" here because the coordinate grows from top + // to bottom). + final float coordY = middleY - ((pos - mFrameCenterPosition) + / mFrameCenterToBorderDistance) * (middleY - topY); + + // Draw a point for this value. + canvas.drawPoint(coordX, coordY, mGraphPointPaint); + + // If this value is part of the same gesture as the previous one, draw a line + // between them. We ignore the first value (with age = 0). + if (age != 0 && (age - prevAge) <= MAX_GESTURE_TIME) { + canvas.drawLine(prevCoordX, prevCoordY, coordX, coordY, mGraphLinePaint); + } + + prevCoordX = coordX; + prevCoordY = coordY; + prevAge = age; + } + } + + @VisibleForTesting + float getFrameCenterPosition() { + return mFrameCenterPosition; + } + + /** + * Holds data needed to draw each entry in the graph. + */ + private static class GraphValue { + /** Position. */ + float mPos; + /** Time when this value was added. */ + long mTime; + + GraphValue(float pos, long time) { + this.mPos = pos; + this.mTime = time; + } + } + + /** + * Holds the graph values as a cyclic buffer. It has a fixed capacity, and it replaces the + * old values with new ones to avoid creating new objects. + */ + private static class CyclicBuffer { + private final GraphValue[] mValues; + private final int mCapacity; + private int mSize = 0; + private int mLastIndex = 0; + + // The iteration index and counter are here to make it easier to reset them. + /** Determines the value currently pointed by the iterator. */ + private int mIteratorIndex; + /** Counts how many values have been iterated through. */ + private int mIteratorCount; + + /** Used traverse the values in reverse order. */ + private final Iterator<GraphValue> mReverseIterator = new Iterator<GraphValue>() { + @Override + public boolean hasNext() { + return mIteratorCount <= mSize; + } + + @Override + public GraphValue next() { + // Returns the value currently pointed by the iterator and moves the iterator to + // the previous one. + mIteratorCount++; + return mValues[(mIteratorIndex-- + mCapacity) % mCapacity]; + } + }; + + CyclicBuffer(int capacity) { + mCapacity = capacity; + mValues = new GraphValue[capacity]; + } + + /** + * Add new graph value. If there is an existing object, we replace its data with the + * new one. With this, we re-use old objects instead of creating new ones. + */ + void add(float pos, long time) { + mLastIndex = (mLastIndex + 1) % mCapacity; + if (mValues[mLastIndex] == null) { + mValues[mLastIndex] = new GraphValue(pos, time); + } else { + final GraphValue oldValue = mValues[mLastIndex]; + oldValue.mPos = pos; + oldValue.mTime = time; + } + + // If needed, account for new value in the buffer size. + if (mSize != mCapacity) { + mSize++; + } + } + + int getSize() { + return mSize; + } + + GraphValue getFirst() { + final int distanceBetweenLastAndFirst = (mCapacity - mSize) + 1; + final int firstIndex = (mLastIndex + distanceBetweenLastAndFirst) % mCapacity; + return mValues[firstIndex]; + } + + GraphValue getLast() { + return mValues[mLastIndex]; + } + + void removeFirst() { + mSize--; + } + + /** Returns an iterator pointing at the last value. */ + Iterator<GraphValue> reverseIterator() { + mIteratorIndex = mLastIndex; + mIteratorCount = 1; + return mReverseIterator; + } + } + } } diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 303f3216ba75..627065587825 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -68,6 +68,7 @@ import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManagerInternal; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; +import android.compat.annotation.Disabled; import android.compat.annotation.EnabledSince; import android.content.ComponentName; import android.content.Context; @@ -312,6 +313,19 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private static final long SILENT_INSTALL_ALLOWED = 265131695L; /** + * The system supports pre-approval and update ownership features from + * {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE API 34}. The change id is used to make sure + * the system includes the fix of pre-approval with update ownership case. When checking the + * change id, if it is disabled, it means the build includes the fix. The more detail is on + * b/293644536. + * See {@link PackageInstaller.SessionParams#setRequestUpdateOwnership(boolean)} and + * {@link #requestUserPreapproval(PreapprovalDetails, IntentSender)} for more details. + */ + @Disabled + @ChangeId + private static final long PRE_APPROVAL_WITH_UPDATE_OWNERSHIP_FIX = 293644536L; + + /** * The default value of {@link #mValidatedTargetSdk} is {@link Integer#MAX_VALUE}. If {@link * #mValidatedTargetSdk} is compared with {@link Build.VERSION_CODES#S} before getting the * target sdk version from a validated apk in {@link #validateApkInstallLocked()}, the compared @@ -927,16 +941,27 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (mPermissionsManuallyAccepted) { return USER_ACTION_NOT_NEEDED; } - packageName = mPackageName; + // For pre-pappvoal case, the mPackageName would be null. + if (mPackageName != null) { + packageName = mPackageName; + } else if (mPreapprovalRequested.get() && mPreapprovalDetails != null) { + packageName = mPreapprovalDetails.getPackageName(); + } else { + packageName = null; + } hasDeviceAdminReceiver = mHasDeviceAdminReceiver; } - final boolean forcePermissionPrompt = + // For the below cases, force user action prompt + // 1. installFlags includes INSTALL_FORCE_PERMISSION_PROMPT + // 2. params.requireUserAction is USER_ACTION_REQUIRED + final boolean forceUserActionPrompt = (params.installFlags & PackageManager.INSTALL_FORCE_PERMISSION_PROMPT) != 0 || params.requireUserAction == SessionParams.USER_ACTION_REQUIRED; - if (forcePermissionPrompt) { - return USER_ACTION_REQUIRED; - } + final int userActionNotTypicallyNeededResponse = forceUserActionPrompt + ? USER_ACTION_REQUIRED + : USER_ACTION_NOT_NEEDED; + // It is safe to access mInstallerUid and mInstallSource without lock // because they are immutable after sealing. final Computer snapshot = mPm.snapshotComputer(); @@ -990,7 +1015,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { || isInstallerDeviceOwnerOrAffiliatedProfileOwner(); if (noUserActionNecessary) { - return USER_ACTION_NOT_NEEDED; + return userActionNotTypicallyNeededResponse; } if (isUpdateOwnershipEnforcementEnabled @@ -1003,7 +1028,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } if (isPermissionGranted) { - return USER_ACTION_NOT_NEEDED; + return userActionNotTypicallyNeededResponse; } if (snapshot.isInstallDisabledForPackage(getInstallerPackageName(), mInstallerUid, diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index faf132e0d80d..2f68021bf356 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -208,7 +208,6 @@ import com.android.internal.policy.LogDecelerateInterpolator; import com.android.internal.policy.PhoneWindow; import com.android.internal.policy.TransitionAnimation; import com.android.internal.statusbar.IStatusBarService; -import com.android.internal.util.FrameworkStatsLog; import com.android.internal.widget.LockPatternUtils; import com.android.server.AccessibilityManagerInternal; import com.android.server.ExtconStateObserver; @@ -622,9 +621,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { private boolean mAllowTheaterModeWakeFromLidSwitch; private boolean mAllowTheaterModeWakeFromWakeGesture; - // Whether to support long press from power button in non-interactive mode + // If true, the power button long press behavior will be invoked even if the default display is + // non-interactive. If false, the power button long press behavior will be skipped if the + // default display is non-interactive. private boolean mSupportLongPressPowerWhenNonInteractive; + // If true, the power button short press behavior will be always invoked as long as the default + // display is on, even if the display is not interactive. If false, the power button short press + // behavior will be skipped if the default display is non-interactive. + private boolean mSupportShortPressPowerWhenDefaultDisplayOn; + // Whether to go to sleep entering theater mode from power button private boolean mGoToSleepOnButtonPressTheaterMode; @@ -1041,7 +1047,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } - private void powerPress(long eventTime, int count, boolean beganFromNonInteractive) { + private void powerPress(long eventTime, int count) { // SideFPS still needs to know about suppressed power buttons, in case it needs to block // an auth attempt. if (count == 1) { @@ -1055,9 +1061,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { final boolean interactive = mDefaultDisplayPolicy.isAwake(); - Slog.d(TAG, "powerPress: eventTime=" + eventTime + " interactive=" + interactive - + " count=" + count + " beganFromNonInteractive=" + beganFromNonInteractive - + " mShortPressOnPowerBehavior=" + mShortPressOnPowerBehavior); + Slog.d( + TAG, + "powerPress: eventTime=" + + eventTime + + " interactive=" + + interactive + + " count=" + + count + + " mShortPressOnPowerBehavior=" + + mShortPressOnPowerBehavior); if (count == 2) { powerMultiPressAction(eventTime, interactive, mDoublePressOnPowerBehavior); @@ -1065,12 +1078,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { powerMultiPressAction(eventTime, interactive, mTriplePressOnPowerBehavior); } else if (count > 3 && count <= getMaxMultiPressPowerCount()) { Slog.d(TAG, "No behavior defined for power press count " + count); - } else if (count == 1 && interactive && !beganFromNonInteractive) { - if (mSideFpsEventHandler.shouldConsumeSinglePress(eventTime)) { - Slog.i(TAG, "Suppressing power key because the user is interacting with the " - + "fingerprint sensor"); - return; - } + } else if (count == 1 && shouldHandleShortPressPowerAction(interactive, eventTime)) { switch (mShortPressOnPowerBehavior) { case SHORT_PRESS_POWER_NOTHING: break; @@ -1118,6 +1126,44 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + private boolean shouldHandleShortPressPowerAction(boolean interactive, long eventTime) { + if (mSupportShortPressPowerWhenDefaultDisplayOn) { + final boolean defaultDisplayOn = Display.isOnState(mDefaultDisplay.getState()); + final boolean beganFromDefaultDisplayOn = + mSingleKeyGestureDetector.beganFromDefaultDisplayOn(); + if (!defaultDisplayOn || !beganFromDefaultDisplayOn) { + Slog.v( + TAG, + "Ignoring short press of power button because the default display is not" + + " on. defaultDisplayOn=" + + defaultDisplayOn + + ", beganFromDefaultDisplayOn=" + + beganFromDefaultDisplayOn); + return false; + } + return true; + } + final boolean beganFromNonInteractive = mSingleKeyGestureDetector.beganFromNonInteractive(); + if (!interactive || beganFromNonInteractive) { + Slog.v( + TAG, + "Ignoring short press of power button because the device is not interactive." + + " interactive=" + + interactive + + ", beganFromNonInteractive=" + + beganFromNonInteractive); + return false; + } + if (mSideFpsEventHandler.shouldConsumeSinglePress(eventTime)) { + Slog.i( + TAG, + "Suppressing power key because the user is interacting with the " + + "fingerprint sensor"); + return false; + } + return true; + } + /** * Attempt to dream from a power button press. * @@ -2231,6 +2277,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { mSupportLongPressPowerWhenNonInteractive = mContext.getResources().getBoolean( com.android.internal.R.bool.config_supportLongPressPowerWhenNonInteractive); + mSupportShortPressPowerWhenDefaultDisplayOn = + mContext.getResources() + .getBoolean( + com.android.internal.R.bool + .config_supportShortPressPowerWhenDefaultDisplayOn); mLongPressOnBackBehavior = mContext.getResources().getInteger( com.android.internal.R.integer.config_longPressOnBackBehavior); @@ -2518,8 +2569,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Override void onPress(long downTime) { - powerPress(downTime, 1 /*count*/, - mSingleKeyGestureDetector.beganFromNonInteractive()); + powerPress(downTime, 1 /*count*/); } @Override @@ -2550,7 +2600,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Override void onMultiPress(long downTime, int count) { - powerPress(downTime, count, mSingleKeyGestureDetector.beganFromNonInteractive()); + powerPress(downTime, count); } } @@ -4323,10 +4373,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { // This could prevent some wrong state in multi-displays environment, // the default display may turned off but interactive is true. - final boolean isDefaultDisplayOn = mDefaultDisplayPolicy.isAwake(); - final boolean interactiveAndOn = interactive && isDefaultDisplayOn; + final boolean isDefaultDisplayOn = Display.isOnState(mDefaultDisplay.getState()); + final boolean isDefaultDisplayAwake = mDefaultDisplayPolicy.isAwake(); + final boolean interactiveAndAwake = interactive && isDefaultDisplayAwake; if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) { - handleKeyGesture(event, interactiveAndOn); + handleKeyGesture(event, interactiveAndAwake, isDefaultDisplayOn); } // Enable haptics if down and virtual key without multiple repetitions. If this is a hard @@ -4479,7 +4530,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { result &= ~ACTION_PASS_TO_USER; isWakeKey = false; // wake-up will be handled separately if (down) { - interceptPowerKeyDown(event, interactiveAndOn); + interceptPowerKeyDown(event, interactiveAndAwake); } else { interceptPowerKeyUp(event, canceled); } @@ -4695,7 +4746,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { return result; } - private void handleKeyGesture(KeyEvent event, boolean interactive) { + private void handleKeyGesture(KeyEvent event, boolean interactive, boolean defaultDisplayOn) { if (mKeyCombinationManager.interceptKey(event, interactive)) { // handled by combo keys manager. mSingleKeyGestureDetector.reset(); @@ -4711,7 +4762,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } - mSingleKeyGestureDetector.interceptKey(event, interactive); + mSingleKeyGestureDetector.interceptKey(event, interactive, defaultDisplayOn); } // The camera gesture will be detected by GestureLauncherService. @@ -6167,6 +6218,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { pw.print("mTriplePressOnPowerBehavior="); pw.println(multiPressOnPowerBehaviorToString(mTriplePressOnPowerBehavior)); pw.print(prefix); + pw.print("mSupportShortPressPowerWhenDefaultDisplayOn="); + pw.println(mSupportShortPressPowerWhenDefaultDisplayOn); + pw.print(prefix); pw.print("mPowerVolUpBehavior="); pw.println(powerVolumeUpBehaviorToString(mPowerVolUpBehavior)); pw.print(prefix); diff --git a/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java b/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java index b999bbb3dce2..5fc0637debea 100644 --- a/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java +++ b/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java @@ -43,6 +43,7 @@ public final class SingleKeyGestureDetector { private int mKeyPressCounter; private boolean mBeganFromNonInteractive = false; + private boolean mBeganFromDefaultDisplayOn = false; private final ArrayList<SingleKeyRule> mRules = new ArrayList(); private SingleKeyRule mActiveRule = null; @@ -194,11 +195,12 @@ public final class SingleKeyGestureDetector { mRules.remove(rule); } - void interceptKey(KeyEvent event, boolean interactive) { + void interceptKey(KeyEvent event, boolean interactive, boolean defaultDisplayOn) { if (event.getAction() == KeyEvent.ACTION_DOWN) { - // Store the non interactive state when first down. + // Store the non interactive state and display on state when first down. if (mDownKeyCode == KeyEvent.KEYCODE_UNKNOWN || mDownKeyCode != event.getKeyCode()) { mBeganFromNonInteractive = !interactive; + mBeganFromDefaultDisplayOn = defaultDisplayOn; } interceptKeyDown(event); } else { @@ -388,6 +390,10 @@ public final class SingleKeyGestureDetector { return mBeganFromNonInteractive; } + boolean beganFromDefaultDisplayOn() { + return mBeganFromDefaultDisplayOn; + } + void dump(String prefix, PrintWriter pw) { pw.println(prefix + "SingleKey rules:"); for (SingleKeyRule rule : mRules) { diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 6432ff081c73..6ede3456d74c 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -1124,14 +1124,15 @@ class TransitionController { + "track #%d", transition.getSyncId(), track); } } - if (sync) { + transition.mAnimationTrack = track; + info.setTrack(track); + mTrackCount = Math.max(mTrackCount, track + 1); + if (sync && mTrackCount > 1) { + // If there are >1 tracks, mark as sync so that all tracks finish. info.setFlags(info.getFlags() | TransitionInfo.FLAG_SYNC); ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Marking #%d animation as SYNC.", transition.getSyncId()); } - transition.mAnimationTrack = track; - info.setTrack(track); - mTrackCount = Math.max(mTrackCount, track + 1); } void updateAnimatingState(SurfaceControl.Transaction t) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 5a45fe11797e..a172d9912cbd 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -30,7 +30,6 @@ import static android.os.PowerManager.DRAW_WAKE_LOCK; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.SurfaceControl.Transaction; import static android.view.SurfaceControl.getGlobalTransaction; -import static android.view.ViewRootImpl.LOCAL_LAYOUT; import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT; import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME; import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION; @@ -1446,9 +1445,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } final boolean dragResizingChanged = !mDragResizingChangeReported && isDragResizeChanged(); - - final boolean attachedFrameChanged = LOCAL_LAYOUT - && mLayoutAttached && getParentWindow().frameChanged(); + final boolean attachedFrameChanged = mLayoutAttached && getParentWindow().frameChanged(); if (DEBUG) { Slog.v(TAG_WM, "Resizing " + this + ": configChanged=" + configChanged diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java new file mode 100644 index 000000000000..4fd8f26d91a8 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display; + +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Point; +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; +import android.view.SurfaceControl; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for the {@link DisplayDevice} class. + * + * Build/Install/Run: + * atest DisplayServicesTests:DisplayDeviceTest + */ +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class DisplayDeviceTest { + private final DisplayDeviceInfo mDisplayDeviceInfo = new DisplayDeviceInfo(); + private static final int WIDTH = 500; + private static final int HEIGHT = 900; + private static final Point PORTRAIT_SIZE = new Point(WIDTH, HEIGHT); + private static final Point LANDSCAPE_SIZE = new Point(HEIGHT, WIDTH); + + @Mock + private SurfaceControl.Transaction mMockTransaction; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mDisplayDeviceInfo.width = WIDTH; + mDisplayDeviceInfo.height = HEIGHT; + mDisplayDeviceInfo.rotation = ROTATION_0; + } + + @Test + public void testGetDisplaySurfaceDefaultSizeLocked_notRotated() { + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo); + assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(PORTRAIT_SIZE); + } + + @Test + public void testGetDisplaySurfaceDefaultSizeLocked_rotation0() { + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo); + displayDevice.setProjectionLocked(mMockTransaction, ROTATION_0, new Rect(), new Rect()); + assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(PORTRAIT_SIZE); + } + + @Test + public void testGetDisplaySurfaceDefaultSizeLocked_rotation90() { + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo); + displayDevice.setProjectionLocked(mMockTransaction, ROTATION_90, new Rect(), new Rect()); + assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(LANDSCAPE_SIZE); + } + + @Test + public void testGetDisplaySurfaceDefaultSizeLocked_rotation180() { + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo); + displayDevice.setProjectionLocked(mMockTransaction, ROTATION_180, new Rect(), new Rect()); + assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(PORTRAIT_SIZE); + } + + @Test + public void testGetDisplaySurfaceDefaultSizeLocked_rotation270() { + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo); + displayDevice.setProjectionLocked(mMockTransaction, ROTATION_270, new Rect(), new Rect()); + assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(LANDSCAPE_SIZE); + } + + private static class FakeDisplayDevice extends DisplayDevice { + private final DisplayDeviceInfo mDisplayDeviceInfo; + + FakeDisplayDevice(DisplayDeviceInfo displayDeviceInfo) { + super(null, null, "", InstrumentationRegistry.getInstrumentation().getContext()); + mDisplayDeviceInfo = displayDeviceInfo; + } + + @Override + public boolean hasStableUniqueId() { + return false; + } + + @Override + public DisplayDeviceInfo getDisplayDeviceInfoLocked() { + return mDisplayDeviceInfo; + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java b/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java index de27d773504e..e672928c5403 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java @@ -102,7 +102,7 @@ public class SettingsToPropertiesMapperTest { ).when(() -> Settings.Global.getString(any(), anyString())); mTestMapper = new SettingsToPropertiesMapper( - mMockContentResolver, TEST_MAPPING, new String[] {}); + mMockContentResolver, TEST_MAPPING, new String[] {}, new String[] {}); } @After diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index bcbbcd4456ee..908afc861f8b 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -61,6 +61,7 @@ import android.companion.virtual.sensor.IVirtualSensorCallback; import android.companion.virtual.sensor.VirtualSensor; import android.companion.virtual.sensor.VirtualSensorCallback; import android.companion.virtual.sensor.VirtualSensorConfig; +import android.content.AttributionSource; import android.content.ComponentName; import android.content.Context; import android.content.ContextWrapper; @@ -1729,7 +1730,9 @@ public class VirtualDeviceManagerServiceTest { private VirtualDeviceImpl createVirtualDevice(int virtualDeviceId, int ownerUid, VirtualDeviceParams params) { VirtualDeviceImpl virtualDeviceImpl = new VirtualDeviceImpl(mContext, - mAssociationInfo, mVdms, new Binder(), ownerUid, virtualDeviceId, + mAssociationInfo, mVdms, new Binder(), + new AttributionSource(ownerUid, "com.android.virtualdevice.test", "virtualdevice"), + virtualDeviceId, mInputController, mCameraAccessController, mPendingTrampolineCallback, mActivityListener, mSoundEffectListener, mRunningAppsChangedCallback, params, new DisplayManagerGlobal(mIDisplayManager)); diff --git a/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java index 3bb86a7bfecb..b9492e9b1b77 100644 --- a/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java @@ -170,10 +170,15 @@ public class SingleKeyGestureTests { } private void pressKey(int keyCode, long pressTime, boolean interactive) { + pressKey(keyCode, pressTime, interactive, false /* defaultDisplayOn */); + } + + private void pressKey( + int keyCode, long pressTime, boolean interactive, boolean defaultDisplayOn) { long eventTime = SystemClock.uptimeMillis(); final KeyEvent keyDown = new KeyEvent(eventTime, eventTime, ACTION_DOWN, keyCode, 0 /* repeat */, 0 /* metaState */); - mDetector.interceptKey(keyDown, interactive); + mDetector.interceptKey(keyDown, interactive, defaultDisplayOn); // keep press down. try { @@ -186,7 +191,7 @@ public class SingleKeyGestureTests { final KeyEvent keyUp = new KeyEvent(eventTime, eventTime, ACTION_UP, keyCode, 0 /* repeat */, 0 /* metaState */); - mDetector.interceptKey(keyUp, interactive); + mDetector.interceptKey(keyUp, interactive, defaultDisplayOn); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 07cdfafdd341..9b6d4e216744 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -39,6 +39,7 @@ import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; +import static android.window.TransitionInfo.FLAG_SYNC; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static android.window.TransitionInfo.isIndependent; @@ -2390,6 +2391,37 @@ public class TransitionTests extends WindowTestsBase { assertFalse(controller.isCollecting()); } + @Test + public void testNoSyncFlagIfOneTrack() { + final TransitionController controller = mAtm.getTransitionController(); + final TestTransitionPlayer player = registerTestTransitionPlayer(); + + mSyncEngine = createTestBLASTSyncEngine(); + controller.setSyncEngine(mSyncEngine); + + final Transition transitA = createTestTransition(TRANSIT_OPEN, controller); + final Transition transitB = createTestTransition(TRANSIT_OPEN, controller); + final Transition transitC = createTestTransition(TRANSIT_OPEN, controller); + + controller.startCollectOrQueue(transitA, (deferred) -> {}); + controller.startCollectOrQueue(transitB, (deferred) -> {}); + controller.startCollectOrQueue(transitC, (deferred) -> {}); + + // Verify that, as-long as there is <= 1 track, we won't get a SYNC flag + transitA.start(); + transitA.setAllReady(); + mSyncEngine.tryFinishForTest(transitA.getSyncId()); + assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0); + transitB.start(); + transitB.setAllReady(); + mSyncEngine.tryFinishForTest(transitB.getSyncId()); + assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0); + transitC.start(); + transitC.setAllReady(); + mSyncEngine.tryFinishForTest(transitC.getSyncId()); + assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0); + } + private static void makeTaskOrganized(Task... tasks) { final ITaskOrganizer organizer = mock(ITaskOrganizer.class); for (Task t : tasks) { diff --git a/telecomm/OWNERS b/telecomm/OWNERS index dcaf858a0a0b..b57b7c79326e 100644 --- a/telecomm/OWNERS +++ b/telecomm/OWNERS @@ -4,7 +4,6 @@ breadley@google.com tgunn@google.com xiaotonj@google.com rgreenwalt@google.com -chinmayd@google.com grantmenke@google.com pmadapurmath@google.com tjstuart@google.com
\ No newline at end of file diff --git a/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java b/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java index d8113fc95591..1b98887199e3 100644 --- a/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java +++ b/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java @@ -19,6 +19,7 @@ package com.android.server.input; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; @@ -48,6 +49,7 @@ public class FocusEventDebugViewTest { private FocusEventDebugView mFocusEventDebugView; private FocusEventDebugView.RotaryInputValueView mRotaryInputValueView; + private FocusEventDebugView.RotaryInputGraphView mRotaryInputGraphView; private float mScaledVerticalScrollFactor; @Before @@ -60,8 +62,9 @@ public class FocusEventDebugViewTest { .thenReturn(InputChannel.openInputChannelPair("FocusEventDebugViewTest")[1]); mRotaryInputValueView = new FocusEventDebugView.RotaryInputValueView(context); + mRotaryInputGraphView = new FocusEventDebugView.RotaryInputGraphView(context); mFocusEventDebugView = new FocusEventDebugView(context, mockService, - () -> mRotaryInputValueView); + () -> mRotaryInputValueView, () -> mRotaryInputGraphView); } @Test @@ -70,6 +73,11 @@ public class FocusEventDebugViewTest { } @Test + public void startsRotaryInputGraphViewWithDefaultFrameCenter() { + assertEquals(0, mRotaryInputGraphView.getFrameCenterPosition(), 0.01); + } + + @Test public void handleRotaryInput_updatesRotaryInputValueViewWithScrollValue() { mFocusEventDebugView.handleUpdateShowRotaryInput(true); @@ -80,6 +88,15 @@ public class FocusEventDebugViewTest { } @Test + public void handleRotaryInput_translatesRotaryInputGraphViewWithHighScrollValue() { + mFocusEventDebugView.handleUpdateShowRotaryInput(true); + + mFocusEventDebugView.handleRotaryInput(createRotaryMotionEvent(1000f)); + + assertTrue(mRotaryInputGraphView.getFrameCenterPosition() > 0); + } + + @Test public void updateActivityStatus_setsAndRemovesColorFilter() { // It should not be active initially. assertNull(mRotaryInputValueView.getBackground().getColorFilter()); diff --git a/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ChatActivity.java b/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ChatActivity.java index ba12acb2c877..2b605c594ce8 100644 --- a/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ChatActivity.java +++ b/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ChatActivity.java @@ -18,6 +18,7 @@ package com.google.android.test.windowinsetstests; import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; + import static java.lang.Math.max; import static java.lang.Math.min; @@ -41,11 +42,11 @@ import android.view.WindowInsetsAnimationController; import android.view.animation.LinearInterpolator; import android.widget.LinearLayout; +import androidx.appcompat.app.AppCompatActivity; + import java.util.ArrayList; import java.util.List; -import androidx.appcompat.app.AppCompatActivity; - public class ChatActivity extends AppCompatActivity { private View mRoot; @@ -148,7 +149,7 @@ public class ChatActivity extends AppCompatActivity { inset = min(inset, shown); mAnimationController.setInsetsAndAlpha( Insets.of(0, 0, 0, inset), - 1f, (inset - start) / (float)(end - start)); + 1f, start == end ? 1f : (inset - start) / (float) (end - start)); } }); |