diff options
17 files changed, 1081 insertions, 223 deletions
diff --git a/core/java/android/app/role/RoleControllerManager.java b/core/java/android/app/role/RoleControllerManager.java index 9186b3d5e15a..668dbd4537ff 100644 --- a/core/java/android/app/role/RoleControllerManager.java +++ b/core/java/android/app/role/RoleControllerManager.java @@ -22,6 +22,7 @@ import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.UserIdInt; +import android.app.ActivityThread; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -93,7 +94,7 @@ public class RoleControllerManager { int userId = context.getUserId(); RemoteService remoteService = sRemoteServices.get(userId); if (remoteService == null) { - remoteService = new RemoteService(context.getApplicationContext(), + remoteService = new RemoteService(ActivityThread.currentApplication(), remoteServiceComponentName, handler, userId); sRemoteServices.put(userId, remoteService); } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 00238bfe0ee3..3dd510c6a6e6 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -3015,7 +3015,8 @@ public abstract class Context { * specify an explicit component name. * @param flags Operation options for the binding as per {@link #bindService}. * @param instanceName Unique identifier for the service instance. Each unique - * name here will result in a different service instance being created. + * name here will result in a different service instance being created. Identifiers + * must only contain ASCII letters, digits, underscores, and periods. * @return Returns success of binding as per {@link #bindService}. * @param executor Callbacks on ServiceConnection will be called on executor. * Must use same instance for the same instance of ServiceConnection. @@ -3023,6 +3024,7 @@ public abstract class Context { * This must be a valid ServiceConnection object; it must not be null. * * @throws SecurityException If the caller does not have permission to access the service + * @throws IllegalArgumentException If the instanceName is invalid. * * @see #bindService */ diff --git a/media/java/android/media/AudioPlaybackConfiguration.java b/media/java/android/media/AudioPlaybackConfiguration.java index b2ebfa934178..ab80b3af9fc8 100644 --- a/media/java/android/media/AudioPlaybackConfiguration.java +++ b/media/java/android/media/AudioPlaybackConfiguration.java @@ -16,6 +16,9 @@ package android.media; +import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_ALL; +import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_NONE; + import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.SystemApi; @@ -235,6 +238,9 @@ public final class AudioPlaybackConfiguration implements Parcelable { .setUsage(in.mPlayerAttr.getUsage()) .setContentType(in.mPlayerAttr.getContentType()) .setFlags(in.mPlayerAttr.getFlags()) + .setAllowedCapturePolicy( + in.mPlayerAttr.getAllowedCapturePolicy() == ALLOW_CAPTURE_BY_ALL + ? ALLOW_CAPTURE_BY_ALL : ALLOW_CAPTURE_BY_NONE) .build(); // anonymized data anonymCopy.mPlayerType = PLAYER_TYPE_UNKNOWN; diff --git a/packages/PackageInstaller/TEST_MAPPING b/packages/PackageInstaller/TEST_MAPPING index 42aa47cab154..5d7b9bb36f75 100644 --- a/packages/PackageInstaller/TEST_MAPPING +++ b/packages/PackageInstaller/TEST_MAPPING @@ -1,5 +1,5 @@ { - "presubmit": [ + "postsubmit": [ { "name": "CtsPackageInstallTestCases", "options": [ diff --git a/packages/SystemUI/res-keyguard/values-sw320dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw320dp/dimens.xml deleted file mode 100644 index 91ca5c52c015..000000000000 --- a/packages/SystemUI/res-keyguard/values-sw320dp/dimens.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* //device/apps/common/assets/res/any/dimens.xml -** -** Copyright 2013, 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. -*/ ---> -<resources> - - <!-- Height of the sliding KeyguardSecurityContainer - (includes 2x keyguard_security_view_top_margin) --> - <dimen name="keyguard_security_height">395dp</dimen> - -</resources> diff --git a/packages/SystemUI/res-keyguard/values-sw360dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw360dp/dimens.xml deleted file mode 100644 index d7c9975ada41..000000000000 --- a/packages/SystemUI/res-keyguard/values-sw360dp/dimens.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* //device/apps/common/assets/res/any/dimens.xml -** -** Copyright 2013, 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. -*/ ---> -<resources> - - <!-- Height of the sliding KeyguardSecurityContainer (includes 2x - keyguard_security_view_top_margin) --> - <dimen name="keyguard_security_height">450dp</dimen> -</resources> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index d67c98a337e8..d8f543f1f710 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -27,11 +27,11 @@ <!-- Height of the sliding KeyguardSecurityContainer (includes 2x keyguard_security_view_top_margin) --> - <dimen name="keyguard_security_height">450dp</dimen> + <dimen name="keyguard_security_height">420dp</dimen> <!-- Max Height of the sliding KeyguardSecurityContainer (includes 2x keyguard_security_view_top_margin) --> - <dimen name="keyguard_security_max_height">505dp</dimen> + <dimen name="keyguard_security_max_height">450dp</dimen> <!-- Margin around the various security views --> <dimen name="keyguard_security_view_top_margin">8dp</dimen> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index df6bc20bbad8..a0c021a3f132 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -41,10 +41,16 @@ <!-- Size of the nav bar edge panels, should be greater to the edge sensitivity + the drag threshold --> - <dimen name="navigation_edge_panel_width">52dp</dimen> - <dimen name="navigation_edge_panel_height">52dp</dimen> + <dimen name="navigation_edge_panel_width">76dp</dimen> + <!-- Padding at the end of the navigation panel to allow the arrow not to be clipped off --> + <dimen name="navigation_edge_panel_padding">24dp</dimen> + <dimen name="navigation_edge_panel_height">84dp</dimen> <!-- The threshold to drag to trigger the edge action --> <dimen name="navigation_edge_action_drag_threshold">16dp</dimen> + <!-- The minimum display position of the arrow on the screen --> + <dimen name="navigation_edge_arrow_min_y">64dp</dimen> + <!-- The amount by which the arrow is shifted to avoid the finger--> + <dimen name="navigation_edge_finger_offset">48dp</dimen> <!-- Luminance threshold to determine black/white contrast for the navigation affordances --> <item name="navigation_luminance_threshold" type="dimen" format="float">0.5</item> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java index 814fec34b4dc..0a6885c0ae69 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java @@ -27,6 +27,7 @@ import android.os.Looper; import android.os.SystemClock; import android.text.TextUtils; import android.util.AttributeSet; +import android.util.TypedValue; import android.view.View; import android.widget.TextView; @@ -120,6 +121,15 @@ public class KeyguardMessageArea extends TextView implements SecurityMessageDisp } @Override + public void onDensityOrFontScaleChanged() { + TypedArray array = mContext.obtainStyledAttributes(R.style.Keyguard_TextView, new int[] { + android.R.attr.textSize + }); + setTextSize(TypedValue.COMPLEX_UNIT_PX, array.getDimensionPixelSize(0, 0)); + array.recycle(); + } + + @Override public void setMessage(CharSequence msg) { if (!TextUtils.isEmpty(msg)) { securityMessageChanged(msg); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java index 9b3f05e3e8d3..4638c40c415d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java @@ -48,6 +48,7 @@ import android.view.InputMonitor; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; import android.view.WindowManagerGlobal; @@ -126,6 +127,12 @@ public class EdgeBackGestureHandler implements DisplayListener { private final float mTouchSlop; // Minimum distance to move so that is can be considerd as a back swipe private final float mSwipeThreshold; + // The threshold where the touch needs to be at most, such that the arrow is displayed above the + // finger, otherwise it will be below + private final int mMinArrowPosition; + // The amount by which the arrow is shifted to avoid the finger + private final int mFingerOffset; + private final int mNavBarHeight; @@ -147,6 +154,8 @@ public class EdgeBackGestureHandler implements DisplayListener { private NavigationBarEdgePanel mEdgePanel; private WindowManager.LayoutParams mEdgePanelLp; + private final Rect mSamplingRect = new Rect(); + private RegionSamplingHelper mRegionSamplingHelper; public EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService) { final Resources res = context.getResources(); @@ -163,6 +172,9 @@ public class EdgeBackGestureHandler implements DisplayListener { mSwipeThreshold = res.getDimension(R.dimen.navigation_edge_action_drag_threshold); mNavBarHeight = res.getDimensionPixelSize(R.dimen.navigation_bar_frame_height); + mMinArrowPosition = res.getDimensionPixelSize( + R.dimen.navigation_edge_arrow_min_y); + mFingerOffset = res.getDimensionPixelSize(R.dimen.navigation_edge_finger_offset); } /** @@ -208,6 +220,8 @@ public class EdgeBackGestureHandler implements DisplayListener { if (mEdgePanel != null) { mWm.removeView(mEdgePanel); mEdgePanel = null; + mRegionSamplingHelper.stop(); + mRegionSamplingHelper = null; } if (!mIsEnabled) { @@ -261,6 +275,18 @@ public class EdgeBackGestureHandler implements DisplayListener { mEdgePanelLp.windowAnimations = 0; mEdgePanel.setLayoutParams(mEdgePanelLp); mWm.addView(mEdgePanel, mEdgePanelLp); + mRegionSamplingHelper = new RegionSamplingHelper(mEdgePanel, + new RegionSamplingHelper.SamplingCallback() { + @Override + public void onRegionDarknessChanged(boolean isRegionDark) { + mEdgePanel.setIsDark(!isRegionDark, true /* animate */); + } + + @Override + public Rect getSampledRegion(View sampledView) { + return mSamplingRect; + } + }); } } @@ -291,7 +317,7 @@ public class EdgeBackGestureHandler implements DisplayListener { // Verify if this is in within the touch region and we aren't in immersive mode, and // either the bouncer is showing or the notification panel is hidden int stateFlags = mOverviewProxyService.getSystemUiStateFlags(); - mIsOnLeftEdge = ev.getX() < mEdgeWidth; + mIsOnLeftEdge = ev.getX() <= mEdgeWidth; mAllowGesture = (stateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0 && ((stateFlags & SYSUI_STATE_BOUNCER_SHOWING) == SYSUI_STATE_BOUNCER_SHOWING || (stateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0) @@ -301,14 +327,13 @@ public class EdgeBackGestureHandler implements DisplayListener { ? (Gravity.LEFT | Gravity.TOP) : (Gravity.RIGHT | Gravity.TOP); mEdgePanel.setIsLeftPanel(mIsOnLeftEdge); - mEdgePanelLp.y = MathUtils.constrain( - (int) (ev.getY() - mEdgePanelLp.height / 2), - 0, mDisplaySize.y); + mEdgePanel.handleTouch(ev); + updateEdgePanelPosition(ev.getY()); mWm.updateViewLayout(mEdgePanel, mEdgePanelLp); + mRegionSamplingHelper.start(mSamplingRect); mDownPoint.set(ev.getX(), ev.getY()); mThresholdCrossed = false; - mEdgePanel.handleTouch(ev); } } else if (mAllowGesture) { if (!mThresholdCrossed && ev.getAction() == MotionEvent.ACTION_MOVE) { @@ -333,12 +358,9 @@ public class EdgeBackGestureHandler implements DisplayListener { // forward touch mEdgePanel.handleTouch(ev); - if (ev.getAction() == MotionEvent.ACTION_UP) { - float xDiff = ev.getX() - mDownPoint.x; - boolean exceedsThreshold = mIsOnLeftEdge - ? (xDiff > mSwipeThreshold) : (-xDiff > mSwipeThreshold); - boolean performAction = exceedsThreshold - && Math.abs(xDiff) > Math.abs(ev.getY() - mDownPoint.y); + boolean isUp = ev.getAction() == MotionEvent.ACTION_UP; + if (isUp) { + boolean performAction = mEdgePanel.shouldTriggerBack(); if (performAction) { // Perform back sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); @@ -347,9 +369,32 @@ public class EdgeBackGestureHandler implements DisplayListener { mOverviewProxyService.notifyBackAction(performAction, (int) mDownPoint.x, (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge); } + if (isUp || ev.getAction() == MotionEvent.ACTION_CANCEL) { + mRegionSamplingHelper.stop(); + } else { + updateSamplingRect(); + mRegionSamplingHelper.updateSamplingRect(); + } } } + private void updateEdgePanelPosition(float touchY) { + float position = touchY - mFingerOffset; + position = Math.max(position, mMinArrowPosition); + position = (position - mEdgePanelLp.height / 2.0f); + mEdgePanelLp.y = MathUtils.constrain((int) position, 0, mDisplaySize.y); + updateSamplingRect(); + } + + private void updateSamplingRect() { + int top = mEdgePanelLp.y; + int left = mIsOnLeftEdge ? 0 : mDisplaySize.x - mEdgePanelLp.width; + int right = left + mEdgePanelLp.width; + int bottom = top + mEdgePanelLp.height; + mSamplingRect.set(left, top, right, bottom); + mEdgePanel.adjustRectToBoundingBox(mSamplingRect); + } + @Override public void onDisplayAdded(int displayId) { } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java index 86b53445468c..79c7ab1d2b8c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java @@ -16,93 +16,221 @@ package com.android.systemui.statusbar.phone; -import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; import android.content.Context; -import android.graphics.Canvas; +import android.content.res.Configuration; +import android.graphics.Canvas;; import android.graphics.Paint; -import android.os.SystemClock; +import android.graphics.Path; +import android.graphics.Rect; import android.os.VibrationEffect; -import android.util.FloatProperty; import android.util.MathUtils; -import android.view.HapticFeedbackConstants; +import android.view.ContextThemeWrapper; import android.view.MotionEvent; +import android.view.VelocityTracker; import android.view.View; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; +import com.android.settingslib.Utils; import com.android.systemui.Dependency; +import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.statusbar.VibratorHelper; +import androidx.core.graphics.ColorUtils; +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.FloatPropertyCompat; +import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.dynamicanimation.animation.SpringForce; + public class NavigationBarEdgePanel extends View { - // TODO: read from resources once drawing is finalized. - private static final boolean SHOW_PROTECTION_STROKE = true; - private static final int PROTECTION_COLOR = 0xffc0c0c0; - private static final int STROKE_COLOR = 0xffe5e5e5; - private static final int PROTECTION_WIDTH_PX = 4; - private static final int BASE_EXTENT = 32; - private static final int ARROW_HEIGHT_DP = 32; - private static final int POINT_EXTENT_DP = 8; - private static final int ARROW_THICKNESS_DP = 4; - private static final float TRACK_LENGTH_MULTIPLIER = 1.5f; - private static final float START_POINTING_RATIO = 0.3f; - private static final float POINTEDNESS_BEFORE_SNAP_RATIO = 0.4f; - private static final int ANIM_DURATION_MS = 150; - private static final long HAPTIC_TIMEOUT_MS = 200; + private static final long COLOR_ANIMATION_DURATION_MS = 100; + private static final long DISAPPEAR_FADE_ANIMATION_DURATION_MS = 140; + private static final long DISAPPEAR_ARROW_ANIMATION_DURATION_MS = 100; + + /** + * The size of the protection of the arrow in px. Only used if this is not background protected + */ + private static final int PROTECTION_WIDTH_PX = 2; + + /** + * The basic translation in dp where the arrow resides + */ + private static final int BASE_TRANSLATION_DP = 32; + + /** + * The length of the arrow leg measured from the center to the end + */ + private static final int ARROW_LENGTH_DP = 18; + + /** + * The angle measured from the xAxis, where the leg is when the arrow rests + */ + private static final int ARROW_ANGLE_WHEN_EXTENDED_DEGREES = 56; + + /** + * The angle that is added per 1000 px speed to the angle of the leg + */ + private static final int ARROW_ANGLE_ADDED_PER_1000_SPEED = 8; + + /** + * The maximum angle offset allowed due to speed + */ + private static final int ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES = 4; + + /** + * The thickness of the arrow. Adjusted to match the home handle (approximately) + */ + private static final float ARROW_THICKNESS_DP = 2.5f; + + /** + * The amount of rubber banding we do for the horizontal translation beyond the base translation + */ + private static final int RUBBER_BAND_AMOUNT = 10; + + /** + * The interpolator used to rubberband + */ + private static final Interpolator RUBBER_BAND_INTERPOLATOR + = new PathInterpolator(1.0f / RUBBER_BAND_AMOUNT, 1.0f, 1.0f, 1.0f); + + /** + * The amount of rubber banding we do for the translation before base translation + */ + private static final int RUBBER_BAND_AMOUNT_APPEAR = 4; + + /** + * The interpolator used to rubberband the appearing of the arrow. + */ + private static final Interpolator RUBBER_BAND_INTERPOLATOR_APPEAR + = new PathInterpolator(1.0f / RUBBER_BAND_AMOUNT_APPEAR, 1.0f, 1.0f, 1.0f); private final VibratorHelper mVibratorHelper; + /** + * The paint the arrow is drawn with + */ private final Paint mPaint = new Paint(); - private final Paint mProtectionPaint = new Paint(); - - private final ObjectAnimator mEndAnimator; - private final ObjectAnimator mLegAnimator; + /** + * The paint the arrow protection is drawn with + */ + private final Paint mProtectionPaint; private final float mDensity; - private final float mBaseExtent; - private final float mPointExtent; - private final float mHeight; - private final float mStrokeThickness; + private final float mBaseTranslation; + private final float mArrowLength; + private final float mArrowThickness; + + /** + * The minimum delta needed in movement for the arrow to change direction / stop triggering back + */ + private final float mMinDeltaForSwitch; private final float mSwipeThreshold; + private final Path mArrowPath = new Path(); + + private final SpringAnimation mAngleAnimation; + private final SpringAnimation mTranslationAnimation; + private final SpringAnimation mVerticalTranslationAnimation; + private final SpringForce mAngleAppearForce; + private final SpringForce mAngleDisappearForce; + private final ValueAnimator mArrowColorAnimator; + private final ValueAnimator mArrowDisappearAnimation; + private final SpringForce mRegularTranslationSpring; + private final SpringForce mTriggerBackSpring; + + private VelocityTracker mVelocityTracker; + private boolean mIsDark = false; + private boolean mShowProtection = false; + private int mProtectionColorLight; + private int mArrowPaddingEnd; + private int mArrowColorLight; + private int mProtectionColorDark; + private int mArrowColorDark; + private int mProtectionColor; + private int mArrowColor; + /** + * True if the panel is currently on the left of the screen + */ private boolean mIsLeftPanel; private float mStartX; + private float mStartY; + private float mCurrentAngle; + /** + * The current translation of the arrow + */ + private float mCurrentTranslation; + /** + * Where the arrow will be in the resting position. + */ + private float mDesiredTranslation; private boolean mDragSlopPassed; - private long mLastSlopHapticTime; - private boolean mGestureDetected; private boolean mArrowsPointLeft; - private float mGestureLength; - private float mLegProgress; - private float mDragProgress; - - // How much the "legs" of the back arrow have proceeded from being a line to an arrow. - private static final FloatProperty<NavigationBarEdgePanel> LEG_PROGRESS = - new FloatProperty<NavigationBarEdgePanel>("legProgress") { + private float mMaxTranslation; + private boolean mTriggerBack; + private float mPreviousTouchTranslation; + private float mTotalTouchDelta; + private float mVerticalTranslation; + private float mDesiredVerticalTranslation; + private float mDesiredAngle; + private float mAngleOffset; + private int mArrowStartColor; + private int mCurrentArrowColor; + private float mDisappearAmount; + + private DynamicAnimation.OnAnimationEndListener mSetGoneEndListener + = new DynamicAnimation.OnAnimationEndListener() { + @Override + public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, + float velocity) { + animation.removeEndListener(this); + if (!canceled) { + setVisibility(GONE); + } + } + }; + private static final FloatPropertyCompat<NavigationBarEdgePanel> CURRENT_ANGLE = + new FloatPropertyCompat<NavigationBarEdgePanel>("currentAngle") { @Override public void setValue(NavigationBarEdgePanel object, float value) { - object.setLegProgress(value); + object.setCurrentAngle(value); } @Override - public Float get(NavigationBarEdgePanel object) { - return object.getLegProgress(); + public float getValue(NavigationBarEdgePanel object) { + return object.getCurrentAngle(); } }; - // How far across the view the arrow should be drawn. - private static final FloatProperty<NavigationBarEdgePanel> DRAG_PROGRESS = - new FloatProperty<NavigationBarEdgePanel>("dragProgress") { + private static final FloatPropertyCompat<NavigationBarEdgePanel> CURRENT_TRANSLATION = + new FloatPropertyCompat<NavigationBarEdgePanel>("currentTranslation") { + + @Override + public void setValue(NavigationBarEdgePanel object, float value) { + object.setCurrentTranslation(value); + } + + @Override + public float getValue(NavigationBarEdgePanel object) { + return object.getCurrentTranslation(); + } + }; + private static final FloatPropertyCompat<NavigationBarEdgePanel> CURRENT_VERTICAL_TRANSLATION = + new FloatPropertyCompat<NavigationBarEdgePanel>("verticalTranslation") { @Override public void setValue(NavigationBarEdgePanel object, float value) { - object.setDragProgress(value); + object.setVerticalTranslation(value); } @Override - public Float get(NavigationBarEdgePanel object) { - return object.getDragProgress(); + public float getValue(NavigationBarEdgePanel object) { + return object.getVerticalTranslation(); } }; @@ -111,171 +239,499 @@ public class NavigationBarEdgePanel extends View { mVibratorHelper = Dependency.get(VibratorHelper.class); - mEndAnimator = ObjectAnimator.ofFloat(this, DRAG_PROGRESS, 1f); - mEndAnimator.setAutoCancel(true); - mEndAnimator.setDuration(ANIM_DURATION_MS); - - mLegAnimator = ObjectAnimator.ofFloat(this, LEG_PROGRESS, 1f); - mLegAnimator.setAutoCancel(true); - mLegAnimator.setDuration(ANIM_DURATION_MS); - mDensity = context.getResources().getDisplayMetrics().density; - mBaseExtent = dp(BASE_EXTENT); - mHeight = dp(ARROW_HEIGHT_DP); - mPointExtent = dp(POINT_EXTENT_DP); - mStrokeThickness = dp(ARROW_THICKNESS_DP); + mBaseTranslation = dp(BASE_TRANSLATION_DP); + mArrowLength = dp(ARROW_LENGTH_DP); + mArrowThickness = dp(ARROW_THICKNESS_DP); + mMinDeltaForSwitch = dp(32); - mPaint.setStrokeWidth(mStrokeThickness); + mPaint.setStrokeWidth(mArrowThickness); mPaint.setStrokeCap(Paint.Cap.ROUND); - mPaint.setColor(STROKE_COLOR); mPaint.setAntiAlias(true); - - mProtectionPaint.setStrokeWidth(mStrokeThickness + PROTECTION_WIDTH_PX); - mProtectionPaint.setStrokeCap(Paint.Cap.ROUND); - mProtectionPaint.setColor(PROTECTION_COLOR); - mProtectionPaint.setAntiAlias(true); - - // Both panels arrow point the same way - mArrowsPointLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR; + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeJoin(Paint.Join.ROUND); + + mArrowColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); + mArrowColorAnimator.setDuration(COLOR_ANIMATION_DURATION_MS); + mArrowColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + int newColor = ColorUtils.blendARGB(mArrowStartColor, mArrowColor, + animation.getAnimatedFraction()); + setCurrentArrowColor(newColor); + } + }); + + mArrowDisappearAnimation = ValueAnimator.ofFloat(0.0f, 1.0f); + mArrowDisappearAnimation.setDuration(DISAPPEAR_ARROW_ANIMATION_DURATION_MS); + mArrowDisappearAnimation.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + mArrowDisappearAnimation.addUpdateListener(animation -> { + mDisappearAmount = (float) animation.getAnimatedValue(); + invalidate(); + }); + + mAngleAnimation = + new SpringAnimation(this, CURRENT_ANGLE); + mAngleAppearForce = new SpringForce() + .setStiffness(SpringForce.STIFFNESS_LOW) + .setDampingRatio(0.4f) + .setFinalPosition(ARROW_ANGLE_WHEN_EXTENDED_DEGREES); + mAngleDisappearForce = new SpringForce() + .setStiffness(SpringForce.STIFFNESS_MEDIUM) + .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) + .setFinalPosition(90); + mAngleAnimation.setSpring(mAngleAppearForce).setMaxValue(90); + + mTranslationAnimation = + new SpringAnimation(this, CURRENT_TRANSLATION); + mRegularTranslationSpring = new SpringForce() + .setStiffness(SpringForce.STIFFNESS_MEDIUM) + .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY); + mTriggerBackSpring = new SpringForce() + .setStiffness(450) + .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY); + mTranslationAnimation.setSpring(mRegularTranslationSpring); + mVerticalTranslationAnimation = + new SpringAnimation(this, CURRENT_VERTICAL_TRANSLATION); + mVerticalTranslationAnimation.setSpring( + new SpringForce() + .setStiffness(SpringForce.STIFFNESS_MEDIUM) + .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); + + mProtectionPaint = new Paint(mPaint); + mProtectionPaint.setStrokeWidth(mArrowThickness + PROTECTION_WIDTH_PX); + loadDimens(); + + loadColors(context); + updateArrowDirection(); mSwipeThreshold = context.getResources() .getDimension(R.dimen.navigation_edge_action_drag_threshold); setVisibility(GONE); } - public void setIsLeftPanel(boolean isLeftPanel) { - mIsLeftPanel = isLeftPanel; - } - @Override - protected void onDraw(Canvas canvas) { - float edgeOffset = mBaseExtent * mDragProgress - mStrokeThickness; - float animatedOffset = mPointExtent * mLegProgress; - canvas.save(); - canvas.translate( - mIsLeftPanel ? edgeOffset : getWidth() - edgeOffset, - (getHeight() - mHeight) * 0.5f); - - float outsideX = mArrowsPointLeft ? animatedOffset : 0; - float middleX = mArrowsPointLeft ? 0 : animatedOffset; - - if (SHOW_PROTECTION_STROKE) { - canvas.drawLine(outsideX, 0, middleX, mHeight * 0.5f, mProtectionPaint); - canvas.drawLine(middleX, mHeight * 0.5f, outsideX, mHeight, mProtectionPaint); - } - - canvas.drawLine(outsideX, 0, middleX, mHeight * 0.5f, mPaint); - canvas.drawLine(middleX, mHeight * 0.5f, outsideX, mHeight, mPaint); - canvas.restore(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - - // TODO: read the gesture length from the nav controller. - mGestureLength = getWidth(); + public boolean hasOverlappingRendering() { + return false; } - private void setLegProgress(float progress) { - mLegProgress = progress; - invalidate(); + public boolean shouldTriggerBack() { + return mTriggerBack; } - private float getLegProgress() { - return mLegProgress; + public void setIsDark(boolean isDark, boolean animate) { + mIsDark = isDark; + updateIsDark(animate); } - private void setDragProgress(float dragProgress) { - mDragProgress = dragProgress; + public void setShowProtection(boolean showProtection) { + mShowProtection = showProtection; invalidate(); } - private float getDragProgress() { - return mDragProgress; + public void setIsLeftPanel(boolean isLeftPanel) { + mIsLeftPanel = isLeftPanel; } - private void hide() { - animate().alpha(0f).setDuration(ANIM_DURATION_MS) - .withEndAction(() -> setVisibility(GONE)); + /** + * Adjust the rect to conform the the actual visible bounding box of the arrow. + * + * @param samplingRect the existing bounding box in screen coordinates, to be modified + */ + public void adjustRectToBoundingBox(Rect samplingRect) { + float translation = mDesiredTranslation; + if (!mTriggerBack) { + // Let's take the resting position and bounds as the sampling rect, since we are not + // visible right now + translation = mBaseTranslation; + if (mIsLeftPanel && mArrowsPointLeft + || (!mIsLeftPanel && !mArrowsPointLeft)) { + // If we're on the left we should move less, because the arrow is facing the other + // direction + translation -= getStaticArrowWidth(); + } + } + float left = translation - mArrowThickness / 2.0f; + left = mIsLeftPanel ? left : samplingRect.width() - left; + + // Let's calculate the position of the end based on the angle + float width = getStaticArrowWidth(); + float height = polarToCartY(ARROW_ANGLE_WHEN_EXTENDED_DEGREES) * mArrowLength * 2.0f; + if (!mArrowsPointLeft) { + left -= width; + } + + float top = (getHeight() * 0.5f) + mDesiredVerticalTranslation - height / 2.0f; + samplingRect.offset((int) left, (int) top); + samplingRect.set(samplingRect.left, samplingRect.top, + (int) (samplingRect.left + width), + (int) (samplingRect.top + height)); } /** * Updates the UI based on the motion events passed in device co-ordinates */ public void handleTouch(MotionEvent event) { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(event); switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN : { mDragSlopPassed = false; - mEndAnimator.cancel(); - mLegAnimator.cancel(); - animate().cancel(); - setLegProgress(0f); - setDragProgress(0f); + resetOnDown(); mStartX = event.getX(); + mStartY = event.getY(); setVisibility(VISIBLE); break; } case MotionEvent.ACTION_MOVE: { - handleNewSwipePoint(event.getX()); + handleMoveEvent(event); break; } // Fall through case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { - hide(); + if (mTriggerBack) { + triggerBackAnimation(); + } else { + if (mTranslationAnimation.isRunning()) { + mTranslationAnimation.addEndListener(mSetGoneEndListener); + } else { + setVisibility(GONE); + } + } + mVelocityTracker.recycle(); + mVelocityTracker = null; break; } } } - private void handleNewSwipePoint(float x) { - float dist = MathUtils.abs(x - mStartX); + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + updateArrowDirection(); + loadDimens(); + } + + @Override + protected void onDraw(Canvas canvas) { + float pointerPosition = mCurrentTranslation - mArrowThickness / 2.0f; + canvas.save(); + canvas.translate( + mIsLeftPanel ? pointerPosition : getWidth() - pointerPosition, + (getHeight() * 0.5f) + mVerticalTranslation); + + // Let's calculate the position of the end based on the angle + float x = (polarToCartX(mCurrentAngle) * mArrowLength); + float y = (polarToCartY(mCurrentAngle) * mArrowLength); + Path arrowPath = calculatePath(x,y); + if (mShowProtection) { + canvas.drawPath(arrowPath, mProtectionPaint); + } + + canvas.drawPath(arrowPath, mPaint); + canvas.restore(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + // TODO: read the gesture length from the nav controller. + mMaxTranslation = getWidth() - mArrowPaddingEnd; + } + + private void loadDimens() { + mArrowPaddingEnd = getContext().getResources().getDimensionPixelSize( + R.dimen.navigation_edge_panel_padding); + } + + private void updateArrowDirection() { + // Both panels arrow point the same way + mArrowsPointLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR; + invalidate(); + } + + private void loadColors(Context context) { + final int dualToneDarkTheme = Utils.getThemeAttr(context, R.attr.darkIconTheme); + final int dualToneLightTheme = Utils.getThemeAttr(context, R.attr.lightIconTheme); + Context lightContext = new ContextThemeWrapper(context, dualToneLightTheme); + Context darkContext = new ContextThemeWrapper(context, dualToneDarkTheme); + mArrowColorLight = Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor); + mArrowColorDark = Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor); + mProtectionColorDark = mArrowColorLight; + mProtectionColorLight = mArrowColorDark; + updateIsDark(false /* animate */); + } + + private void updateIsDark(boolean animate) { + // TODO: Maybe animate protection as well + mProtectionColor = mIsDark ? mProtectionColorDark : mProtectionColorLight; + mProtectionPaint.setColor(mProtectionColor); + mArrowColor = mIsDark ? mArrowColorDark : mArrowColorLight; + mArrowColorAnimator.cancel(); + if (!animate) { + setCurrentArrowColor(mArrowColor); + } else { + mArrowStartColor = mCurrentArrowColor; + mArrowColorAnimator.start(); + } + } + + private void setCurrentArrowColor(int color) { + mCurrentArrowColor = color; + mPaint.setColor(color); + invalidate(); + } + + private float getStaticArrowWidth() { + return polarToCartX(ARROW_ANGLE_WHEN_EXTENDED_DEGREES) * mArrowLength; + } + + private float polarToCartX(float angleInDegrees) { + return (float) Math.cos(Math.toRadians(angleInDegrees)); + } + + private float polarToCartY(float angleInDegrees) { + return (float) Math.sin(Math.toRadians(angleInDegrees)); + } + + private Path calculatePath(float x, float y) { + if (!mArrowsPointLeft) { + x = -x; + } + float extent = 1.0f - mDisappearAmount; + x = x * extent; + y = y * extent; + mArrowPath.reset(); + mArrowPath.moveTo(x, y); + mArrowPath.lineTo(0, 0); + mArrowPath.lineTo(x, -y); + return mArrowPath; + } + + private float getCurrentAngle() { + return mCurrentAngle; + } + + private float getCurrentTranslation() { + return mCurrentTranslation; + } + + private void triggerBackAnimation() { + + mVelocityTracker.computeCurrentVelocity(1000); + // Only do the extra translation if we're not already flinging + boolean doExtraTranslation = Math.abs(mVelocityTracker.getXVelocity()) < 1000; + if (doExtraTranslation) { + setDesiredTranslation(mDesiredTranslation + dp(16), true /* animate */); + } + + // Let's also snap the angle a bit + if (mAngleOffset < -4) { + mAngleOffset = Math.max(-16, mAngleOffset - 16); + updateAngle(true /* animated */); + } + + // Finally, after the translation, animate back and disappear the arrow + Runnable translationEnd = () -> { + setTriggerBack(false /* false */, true /* animate */); + mTranslationAnimation.setSpring(mTriggerBackSpring); + setDesiredTranslation(0, true /* animated */); + animate().alpha(0f).setDuration(DISAPPEAR_FADE_ANIMATION_DURATION_MS) + .withEndAction(() -> setVisibility(GONE)); + mArrowDisappearAnimation.start(); + }; + if (mTranslationAnimation.isRunning()) { + mTranslationAnimation.addEndListener(new DynamicAnimation.OnAnimationEndListener() { + @Override + public void onAnimationEnd(DynamicAnimation animation, boolean canceled, + float value, + float velocity) { + animation.removeEndListener(this); + if (!canceled) { + translationEnd.run(); + } + } + }); + } else { + translationEnd.run(); + } + + } + + private void resetOnDown() { + animate().cancel(); + mAngleAnimation.cancel(); + mTranslationAnimation.cancel(); + mVerticalTranslationAnimation.cancel(); + mArrowDisappearAnimation.cancel(); + mAngleOffset = 0; + mTranslationAnimation.setSpring(mRegularTranslationSpring); + // Reset the arrow to the side + setTriggerBack(false /* triggerBack */, false /* animated */); + setDesiredTranslation(0, false /* animated */); + setCurrentTranslation(0); + mPreviousTouchTranslation = 0; + mTotalTouchDelta = 0; + setDesiredVerticalTransition(0, false /* animated */); + } + + private void handleMoveEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + float touchTranslation = MathUtils.abs(x - mStartX); + float yOffset = y - mStartY; + float delta = touchTranslation - mPreviousTouchTranslation; + if (Math.abs(delta) > 0) { + if (Math.signum(delta) == Math.signum(mTotalTouchDelta)) { + mTotalTouchDelta += delta; + } else { + mTotalTouchDelta = delta; + } + } + mPreviousTouchTranslation = touchTranslation; // Apply a haptic on drag slop passed - if (!mDragSlopPassed && dist > mSwipeThreshold) { + if (!mDragSlopPassed && touchTranslation > mSwipeThreshold) { mDragSlopPassed = true; mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); - mLastSlopHapticTime = SystemClock.uptimeMillis(); + + // Let's show the arrow and animate it in! + mDisappearAmount = 0.0f; setAlpha(1f); + // And animate it go to back by default! + setTriggerBack(true /* triggerBack */, true /* animated */); + } + + // Let's make sure we only go to the baseextend and apply rubberbanding afterwards + if (touchTranslation > mBaseTranslation) { + float diff = touchTranslation - mBaseTranslation; + float progress = MathUtils.saturate(diff / (mBaseTranslation * RUBBER_BAND_AMOUNT)); + progress = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress) + * (mMaxTranslation - mBaseTranslation); + touchTranslation = mBaseTranslation + progress; + } else { + float diff = mBaseTranslation - touchTranslation; + float progress = MathUtils.saturate(diff / mBaseTranslation); + progress = RUBBER_BAND_INTERPOLATOR_APPEAR.getInterpolation(progress) + * (mBaseTranslation / RUBBER_BAND_AMOUNT_APPEAR); + touchTranslation = mBaseTranslation - progress; + } + // By default we just assume the current direction is kept + boolean triggerBack = mTriggerBack; + + // First lets see if we had continuous motion in one direction for a while + if (Math.abs(mTotalTouchDelta) > mMinDeltaForSwitch) { + triggerBack = mTotalTouchDelta > 0; } - setDragProgress(MathUtils.constrainedMap( - 0, 1.0f, - 0, mGestureLength * TRACK_LENGTH_MULTIPLIER, - dist)); + // Then, let's see if our velocity tells us to change direction + mVelocityTracker.computeCurrentVelocity(1000); + float xVelocity = mVelocityTracker.getXVelocity(); + float yVelocity = mVelocityTracker.getYVelocity(); + float velocity = MathUtils.mag(xVelocity, yVelocity); + mAngleOffset = Math.min(velocity / 1000 * ARROW_ANGLE_ADDED_PER_1000_SPEED, + ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES) * Math.signum(xVelocity); + if (mIsLeftPanel && mArrowsPointLeft || !mIsLeftPanel && !mArrowsPointLeft) { + mAngleOffset *= -1; + } + + // Last if the direction in Y is bigger than X * 2 we also abort + if (Math.abs(yOffset) > Math.abs(x - mStartX) * 2) { + triggerBack = false; + } + setTriggerBack(triggerBack, true /* animated */); + + if (!mTriggerBack) { + touchTranslation = 0; + } else if (mIsLeftPanel && mArrowsPointLeft + || (!mIsLeftPanel && !mArrowsPointLeft)) { + // If we're on the left we should move less, because the arrow is facing the other + // direction + touchTranslation -= getStaticArrowWidth(); + } + setDesiredTranslation(touchTranslation, true /* animated */); + updateAngle(true /* animated */); + + float maxYOffset = getHeight() / 2.0f - mArrowLength; + float progress = MathUtils.constrain( + Math.abs(yOffset) / (maxYOffset * RUBBER_BAND_AMOUNT), + 0, 1); + float verticalTranslation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress) + * maxYOffset * Math.signum(yOffset); + setDesiredVerticalTransition(verticalTranslation, true /* animated */); + } - if (dist < mGestureLength) { - float calculatedLegProgress = MathUtils.constrainedMap( - 0f, POINTEDNESS_BEFORE_SNAP_RATIO, - mGestureLength * START_POINTING_RATIO, mGestureLength, - dist); + private void setDesiredVerticalTransition(float verticalTranslation, boolean animated) { + if (mDesiredVerticalTranslation != verticalTranslation) { + mDesiredVerticalTranslation = verticalTranslation; + if (!animated) { + setVerticalTranslation(verticalTranslation); + } else { + mVerticalTranslationAnimation.animateToFinalPosition(verticalTranslation); + } + invalidate(); + } + } - // Blend animated value with drag calculated value, allow the gesture to continue - // while the animation is playing with jump cuts in the animation. - setLegProgress(MathUtils.lerp(calculatedLegProgress, mLegProgress, mDragProgress)); + private void setVerticalTranslation(float verticalTranslation) { + mVerticalTranslation = verticalTranslation; + invalidate(); + } - if (mGestureDetected) { - mGestureDetected = false; + private float getVerticalTranslation() { + return mVerticalTranslation; + } - mLegAnimator.setFloatValues(POINTEDNESS_BEFORE_SNAP_RATIO); - mLegAnimator.start(); + private void setDesiredTranslation(float desiredTranslation, boolean animated) { + if (mDesiredTranslation != desiredTranslation) { + mDesiredTranslation = desiredTranslation; + if (!animated) { + setCurrentTranslation(desiredTranslation); + } else { + mTranslationAnimation.animateToFinalPosition(desiredTranslation); } - } else { - if (!mGestureDetected) { - // Prevent another haptic if it was just used - if (SystemClock.uptimeMillis() - mLastSlopHapticTime > HAPTIC_TIMEOUT_MS) { - performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); - } - mGestureDetected = true; + } + } + + private void setCurrentTranslation(float currentTranslation) { + mCurrentTranslation = currentTranslation; + invalidate(); + } - mLegAnimator.setFloatValues(1f); - mLegAnimator.start(); + private void setTriggerBack(boolean triggerBack, boolean animated) { + if (mTriggerBack != triggerBack) { + mTriggerBack = triggerBack; + mAngleAnimation.cancel(); + updateAngle(animated); + // Whenever the trigger back state changes the existing translation animation should be + // cancelled + mTranslationAnimation.cancel(); + } + } + + private void updateAngle(boolean animated) { + float newAngle = mTriggerBack ? ARROW_ANGLE_WHEN_EXTENDED_DEGREES + mAngleOffset : 90; + if (newAngle != mDesiredAngle) { + if (!animated) { + setCurrentAngle(newAngle); + } else { + mAngleAnimation.setSpring(mTriggerBack ? mAngleAppearForce : mAngleDisappearForce); + mAngleAnimation.animateToFinalPosition(newAngle); } + mDesiredAngle = newAngle; } } + private void setCurrentAngle(float currentAngle) { + mCurrentAngle = currentAngle; + invalidate(); + } + private float dp(float dp) { return mDensity * dp; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java new file mode 100644 index 000000000000..d59319e110de --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.phone; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.annotation.Nullable; +import android.content.res.Resources; +import android.graphics.Rect; +import android.os.Handler; +import android.os.IBinder; +import android.provider.Settings; +import android.view.CompositionSamplingListener; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewTreeObserver; + +import com.android.systemui.R; + +/** + * A helper class to sample regions on the screen and inspect its luminosity. + */ +public class RegionSamplingHelper implements View.OnAttachStateChangeListener, + View.OnLayoutChangeListener { + + private final Handler mHandler = new Handler(); + private final View mSampledView; + + private final CompositionSamplingListener mSamplingListener; + private final Runnable mUpdateSamplingListener = this::updateSamplingListener; + + /** + * The requested sampling bounds that we want to sample from + */ + private final Rect mSamplingRequestBounds = new Rect(); + + /** + * The sampling bounds that are currently registered. + */ + private final Rect mRegisteredSamplingBounds = new Rect(); + private final SamplingCallback mCallback; + private boolean mSamplingEnabled = false; + private boolean mSamplingListenerRegistered = false; + + private float mLastMedianLuma; + private float mCurrentMedianLuma; + private boolean mWaitingOnDraw; + + // Passing the threshold of this luminance value will make the button black otherwise white + private final float mLuminanceThreshold; + private final float mLuminanceChangeThreshold; + private boolean mFirstSamplingAfterStart; + private SurfaceControl mRegisteredStopLayer = null; + private ViewTreeObserver.OnDrawListener mUpdateOnDraw = new ViewTreeObserver.OnDrawListener() { + @Override + public void onDraw() { + // We need to post the remove runnable, since it's not allowed to remove in onDraw + mHandler.post(mRemoveDrawRunnable); + RegionSamplingHelper.this.onDraw(); + } + }; + private Runnable mRemoveDrawRunnable = new Runnable() { + @Override + public void run() { + mSampledView.getViewTreeObserver().removeOnDrawListener(mUpdateOnDraw); + } + }; + + public RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback) { + mSamplingListener = new CompositionSamplingListener( + sampledView.getContext().getMainExecutor()) { + @Override + public void onSampleCollected(float medianLuma) { + if (mSamplingEnabled) { + updateMediaLuma(medianLuma); + } + } + }; + mSampledView = sampledView; + mSampledView.addOnAttachStateChangeListener(this); + mSampledView.addOnLayoutChangeListener(this); + + final Resources res = sampledView.getResources(); + mLuminanceThreshold = res.getFloat(R.dimen.navigation_luminance_threshold); + mLuminanceChangeThreshold = res.getFloat(R.dimen.navigation_luminance_change_threshold); + mCallback = samplingCallback; + } + + private void onDraw() { + if (mWaitingOnDraw) { + mWaitingOnDraw = false; + updateSamplingListener(); + } + } + + void start(Rect initialSamplingBounds) { + if (!mCallback.isSamplingEnabled()) { + return; + } + if (initialSamplingBounds != null) { + mSamplingRequestBounds.set(initialSamplingBounds); + } + mSamplingEnabled = true; + // make sure we notify once + mLastMedianLuma = -1; + mFirstSamplingAfterStart = true; + updateSamplingListener(); + } + + void stop() { + mSamplingEnabled = false; + updateSamplingListener(); + } + + @Override + public void onViewAttachedToWindow(View view) { + updateSamplingListener(); + } + + @Override + public void onViewDetachedFromWindow(View view) { + // isAttachedToWindow is only changed after this call to the listeners, so let's post it + // instead + postUpdateSamplingListener(); + } + + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + updateSamplingRect(); + } + + private void postUpdateSamplingListener() { + mHandler.removeCallbacks(mUpdateSamplingListener); + mHandler.post(mUpdateSamplingListener); + } + + private void updateSamplingListener() { + boolean isSamplingEnabled = mSamplingEnabled && !mSamplingRequestBounds.isEmpty() + && (mSampledView.isAttachedToWindow() || mFirstSamplingAfterStart); + if (isSamplingEnabled) { + SurfaceControl stopLayerControl = mSampledView.getViewRootImpl().getSurfaceControl(); + if (!stopLayerControl.isValid()) { + if (!mWaitingOnDraw) { + mWaitingOnDraw = true; + // The view might be attached but we haven't drawn yet, so wait until the + // next draw to update the listener again with the stop layer, such that our + // own drawing doesn't affect the sampling. + if (mHandler.hasCallbacks(mRemoveDrawRunnable)) { + mHandler.removeCallbacks(mRemoveDrawRunnable); + } else { + mSampledView.getViewTreeObserver().addOnDrawListener(mUpdateOnDraw); + } + } + // If there's no valid surface, let's just sample without a stop layer, so we + // don't have to delay + stopLayerControl = null; + } + if (!mSamplingRequestBounds.equals(mRegisteredSamplingBounds) + || mRegisteredStopLayer != stopLayerControl) { + // We only want to reregister if something actually changed + unregisterSamplingListener(); + mSamplingListenerRegistered = true; + CompositionSamplingListener.register(mSamplingListener, DEFAULT_DISPLAY, + stopLayerControl != null ? stopLayerControl.getHandle() : null, + mSamplingRequestBounds); + mRegisteredSamplingBounds.set(mSamplingRequestBounds); + mRegisteredStopLayer = stopLayerControl; + } + mFirstSamplingAfterStart = false; + } else { + unregisterSamplingListener(); + } + } + + private void unregisterSamplingListener() { + if (mSamplingListenerRegistered) { + mSamplingListenerRegistered = false; + mRegisteredStopLayer = null; + mRegisteredSamplingBounds.setEmpty(); + CompositionSamplingListener.unregister(mSamplingListener); + } + } + + private void updateMediaLuma(float medianLuma) { + mCurrentMedianLuma = medianLuma; + + // If the difference between the new luma and the current luma is larger than threshold + // then apply the current luma, this is to prevent small changes causing colors to flicker + if (Math.abs(mCurrentMedianLuma - mLastMedianLuma) > mLuminanceChangeThreshold) { + mCallback.onRegionDarknessChanged(medianLuma < mLuminanceThreshold /* isRegionDark */); + mLastMedianLuma = medianLuma; + } + } + + public void updateSamplingRect() { + Rect sampledRegion = mCallback.getSampledRegion(mSampledView); + if (!mSamplingRequestBounds.equals(sampledRegion)) { + mSamplingRequestBounds.set(sampledRegion); + updateSamplingListener(); + } + } + + public interface SamplingCallback { + /** + * Called when the darkness of the sampled region changes + * @param isRegionDark true if the sampled luminance is below the luminance threshold + */ + void onRegionDarknessChanged(boolean isRegionDark); + + /** + * Get the sampled region of interest from the sampled view + * @param sampledView The view that this helper is attached to for convenience + * @return the region to be sampled in sceen coordinates. Return {@code null} to avoid + * sampling in this frame + */ + Rect getSampledRegion(View sampledView); + + /** + * @return if sampling should be enabled in the current configuration + */ + default boolean isSamplingEnabled() { + return true; + } + } +} diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index dcb3a2298be1..fc5d3933f8f2 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -13853,6 +13853,18 @@ public class ActivityManagerService extends IActivityManager.Stub throw new IllegalArgumentException("callingPackage cannot be null"); } + // Ensure that instanceName, which is caller provided, does not contain + // unusual characters. + if (instanceName != null) { + for (int i = 0; i < instanceName.length(); ++i) { + char c = instanceName.charAt(i); + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') || c == '_' || c == '.')) { + throw new IllegalArgumentException("Illegal instanceName"); + } + } + } + synchronized(this) { return mServices.bindServiceLocked(caller, token, service, resolvedType, connection, flags, instanceName, callingPackage, userId); diff --git a/services/core/java/com/android/server/wm/TaskPositioner.java b/services/core/java/com/android/server/wm/TaskPositioner.java index 7f1b4c0d0dd3..efd3e1cbf770 100644 --- a/services/core/java/com/android/server/wm/TaskPositioner.java +++ b/services/core/java/com/android/server/wm/TaskPositioner.java @@ -450,6 +450,11 @@ class TaskPositioner implements IBinder.DeathRecipient { // This is a moving or scrolling operation. mTask.mStack.getDimBounds(mTmpRect); + // If a target window is covered by system bar, there is no way to move it again by touch. + // So we exclude them from stack bounds. and then it will be shown inside stable area. + Rect stableBounds = new Rect(); + mDisplayContent.getStableRect(stableBounds); + mTmpRect.intersect(stableBounds); int nX = (int) x; int nY = (int) y; diff --git a/services/core/java/com/android/server/wm/TaskRecord.java b/services/core/java/com/android/server/wm/TaskRecord.java index 10fb559de2e6..d4d157fbe75c 100644 --- a/services/core/java/com/android/server/wm/TaskRecord.java +++ b/services/core/java/com/android/server/wm/TaskRecord.java @@ -2209,9 +2209,27 @@ class TaskRecord extends ConfigurationContainer { // by policy, make sure the window remains within parent somewhere final float density = ((float) newParentConfig.densityDpi) / DisplayMetrics.DENSITY_DEFAULT; - fitWithinBounds(outOverrideBounds, newParentConfig.windowConfiguration.getBounds(), + final Rect parentBounds = + new Rect(newParentConfig.windowConfiguration.getBounds()); + final ActivityDisplay display = mStack.getDisplay(); + if (display != null && display.mDisplayContent != null) { + // If a freeform window moves below system bar, there is no way to move it again + // by touch. Because its caption is covered by system bar. So we exclude them + // from stack bounds. and then caption will be shown inside stable area. + final Rect stableBounds = new Rect(); + display.mDisplayContent.getStableRect(stableBounds); + parentBounds.intersect(stableBounds); + } + + fitWithinBounds(outOverrideBounds, parentBounds, (int) (density * WindowState.MINIMUM_VISIBLE_WIDTH_IN_DP), (int) (density * WindowState.MINIMUM_VISIBLE_HEIGHT_IN_DP)); + + // Prevent to overlap caption with stable insets. + final int offsetTop = parentBounds.top - outOverrideBounds.top; + if (offsetTop > 0) { + outOverrideBounds.offset(0, offsetTop); + } } computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig); } diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java index b76682269c2e..eb90295ed8c9 100644 --- a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java +++ b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java @@ -21,9 +21,14 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSess import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; import android.provider.DeviceConfig; +import android.provider.DeviceConfig.Properties; import android.util.Pair; import com.android.dx.mockito.inline.extended.StaticMockitoSession; @@ -32,9 +37,11 @@ import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.junit.runners.model.Statement; +import org.mockito.Mockito; import org.mockito.quality.Strictness; import org.mockito.stubbing.Answer; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -57,6 +64,8 @@ public final class TestableDeviceConfig implements TestRule { private StaticMockitoSession mMockitoSession; private Map<DeviceConfig.OnPropertyChangedListener, Pair<String, Executor>> mOnPropertyChangedListenerMap = new HashMap<>(); + private Map<DeviceConfig.OnPropertiesChangedListener, Pair<String, Executor>> + mOnPropertiesChangedListenerMap = new HashMap<>(); private Map<String, String> mKeyValueMap = new ConcurrentHashMap<>(); /** @@ -77,6 +86,18 @@ public final class TestableDeviceConfig implements TestRule { doAnswer((Answer<Void>) invocationOnMock -> { String namespace = invocationOnMock.getArgument(0); Executor executor = invocationOnMock.getArgument(1); + DeviceConfig.OnPropertiesChangedListener onPropertiesChangedListener = + invocationOnMock.getArgument(2); + mOnPropertiesChangedListenerMap.put( + onPropertiesChangedListener, new Pair<>(namespace, executor)); + return null; + }).when(() -> DeviceConfig.addOnPropertiesChangedListener( + anyString(), any(Executor.class), + any(DeviceConfig.OnPropertiesChangedListener.class))); + + doAnswer((Answer<Void>) invocationOnMock -> { + String namespace = invocationOnMock.getArgument(0); + Executor executor = invocationOnMock.getArgument(1); DeviceConfig.OnPropertyChangedListener onPropertyChangedListener = invocationOnMock.getArgument(2); mOnPropertyChangedListenerMap.put( @@ -91,6 +112,14 @@ public final class TestableDeviceConfig implements TestRule { String name = invocationOnMock.getArgument(1); String value = invocationOnMock.getArgument(2); mKeyValueMap.put(getKey(namespace, name), value); + for (DeviceConfig.OnPropertiesChangedListener listener : + mOnPropertiesChangedListenerMap.keySet()) { + if (namespace.equals(mOnPropertiesChangedListenerMap.get(listener).first)) { + mOnPropertiesChangedListenerMap.get(listener).second.execute( + () -> listener.onPropertiesChanged( + getProperties(namespace, name, value))); + } + } for (DeviceConfig.OnPropertyChangedListener listener : mOnPropertyChangedListenerMap.keySet()) { if (namespace.equals(mOnPropertyChangedListenerMap.get(listener).first)) { @@ -114,12 +143,14 @@ public final class TestableDeviceConfig implements TestRule { protected void succeeded(Description description) { mMockitoSession.finishMocking(); mOnPropertyChangedListenerMap.clear(); + mOnPropertiesChangedListenerMap.clear(); } @Override protected void failed(Throwable e, Description description) { mMockitoSession.finishMocking(e); mOnPropertyChangedListenerMap.clear(); + mOnPropertiesChangedListenerMap.clear(); } }.apply(base, description); } @@ -128,4 +159,79 @@ public final class TestableDeviceConfig implements TestRule { return namespace + "/" + name; } + private Properties getProperties(String namespace, String name, String value) { + Properties properties = Mockito.mock(Properties.class); + when(properties.getNamespace()).thenReturn(namespace); + when(properties.getKeyset()).thenReturn(Collections.singleton(name)); + when(properties.getBoolean(anyString(), anyBoolean())).thenAnswer( + invocation -> { + String key = invocation.getArgument(0); + boolean defaultValue = invocation.getArgument(1); + if (name.equalsIgnoreCase(key) && value != null) { + return Boolean.parseBoolean(value); + } else { + return defaultValue; + } + } + ); + when(properties.getFloat(anyString(), anyFloat())).thenAnswer( + invocation -> { + String key = invocation.getArgument(0); + float defaultValue = invocation.getArgument(1); + if (name.equalsIgnoreCase(key) && value != null) { + try { + return Float.parseFloat(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } else { + return defaultValue; + } + } + ); + when(properties.getInt(anyString(), anyInt())).thenAnswer( + invocation -> { + String key = invocation.getArgument(0); + int defaultValue = invocation.getArgument(1); + if (name.equalsIgnoreCase(key) && value != null) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } else { + return defaultValue; + } + } + ); + when(properties.getLong(anyString(), anyLong())).thenAnswer( + invocation -> { + String key = invocation.getArgument(0); + long defaultValue = invocation.getArgument(1); + if (name.equalsIgnoreCase(key) && value != null) { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } else { + return defaultValue; + } + } + ); + when(properties.getString(anyString(), anyString())).thenAnswer( + invocation -> { + String key = invocation.getArgument(0); + String defaultValue = invocation.getArgument(1); + if (name.equalsIgnoreCase(key) && value != null) { + return value; + } else { + return defaultValue; + } + } + ); + + return properties; + } + } diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java index 39b5840f12d8..3eb72097182d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java @@ -16,7 +16,7 @@ package com.android.server.testables; -import static android.provider.DeviceConfig.OnPropertyChangedListener; +import static android.provider.DeviceConfig.OnPropertiesChangedListener; import static com.google.common.truth.Truth.assertThat; @@ -93,20 +93,22 @@ public class TestableDeviceConfigTest { public void testListener() throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(1); - OnPropertyChangedListener changeListener = (namespace, name, value) -> { - assertThat(namespace).isEqualTo(sNamespace); - assertThat(name).isEqualTo(sKey); - assertThat(value).isEqualTo(sValue); + OnPropertiesChangedListener changeListener = (properties) -> { + assertThat(properties.getNamespace()).isEqualTo(sNamespace); + assertThat(properties.getKeyset().size()).isEqualTo(1); + assertThat(properties.getKeyset()).contains(sKey); + assertThat(properties.getString(sKey, "bogus_value")).isEqualTo(sValue); + assertThat(properties.getString("bogus_key", "bogus_value")).isEqualTo("bogus_value"); countDownLatch.countDown(); }; try { - DeviceConfig.addOnPropertyChangedListener(sNamespace, + DeviceConfig.addOnPropertiesChangedListener(sNamespace, ActivityThread.currentApplication().getMainExecutor(), changeListener); DeviceConfig.setProperty(sNamespace, sKey, sValue, false); assertThat(countDownLatch.await( WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue(); } finally { - DeviceConfig.removeOnPropertyChangedListener(changeListener); + DeviceConfig.removeOnPropertiesChangedListener(changeListener); } } |