From 35b1f38403ba455c9980970fc2d91c269e8e72ea Mon Sep 17 00:00:00 2001 From: Liran Binyamin Date: Thu, 11 May 2023 12:13:49 -0400 Subject: [PATCH 1/9] Animate the bubble bar width and the bubbles within it as it expands and collapses. Fixes: 280604480 Test: Tested on a physical device Change-Id: I76587cf6ba97700b49c902d42cf4db5f7f3a7152 --- .../taskbar/bubbles/BubbleBarBackground.kt | 4 +- .../taskbar/bubbles/BubbleBarView.java | 141 ++++++++++++++---- 2 files changed, 117 insertions(+), 28 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt index 8a8e21f9fd..cd95ad38fc 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt @@ -48,6 +48,8 @@ class BubbleBarBackground(context: TaskbarActivityContext, private val backgroun private var showingArrow: Boolean = false private var arrowDrawable: ShapeDrawable + var width: Float = 0f + init { paint.color = context.getColor(R.color.taskbar_background) paint.flags = Paint.ANTI_ALIAS_FLAG @@ -102,7 +104,7 @@ class BubbleBarBackground(context: TaskbarActivityContext, private val backgroun // Draw background. val radius = backgroundHeight / 2f canvas.drawRoundRect( - 0f, + canvas.width.toFloat() - width, 0f, canvas.width.toFloat(), canvas.height.toFloat(), diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index 0e1e0e1a1b..8b3e2e42ef 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.taskbar.bubbles; +import android.animation.Animator; import android.animation.ValueAnimator; import android.annotation.Nullable; import android.content.Context; @@ -66,8 +67,8 @@ public class BubbleBarView extends FrameLayout { // if it's smaller than 5. private static final int MAX_BUBBLES = 5; private static final int ARROW_POSITION_ANIMATION_DURATION_MS = 200; + private static final int WIDTH_ANIMATION_DURATION_MS = 200; - private final TaskbarActivityContext mActivityContext; private final BubbleBarBackground mBubbleBarBackground; // The current bounds of all the bubble bar. @@ -90,6 +91,10 @@ public class BubbleBarView extends FrameLayout { private final Rect mTempRect = new Rect(); + // An animator that represents the expansion state of the bubble bar, where 0 corresponds to the + // collapsed state and 1 to the fully expanded state. + private final ValueAnimator mWidthAnimator = ValueAnimator.ofFloat(0, 1); + // We don't reorder the bubbles when they are expanded as it could be jarring for the user // this runnable will be populated with any reordering of the bubbles that should be applied // once they are collapsed. @@ -110,7 +115,7 @@ public class BubbleBarView extends FrameLayout { public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - mActivityContext = ActivityContext.lookupContext(context); + TaskbarActivityContext activityContext = ActivityContext.lookupContext(context); mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap); mIconSpacing = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_spacing); @@ -118,9 +123,39 @@ public class BubbleBarView extends FrameLayout { mBubbleElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_elevation); setClipToPadding(false); - mBubbleBarBackground = new BubbleBarBackground(mActivityContext, + mBubbleBarBackground = new BubbleBarBackground(activityContext, getResources().getDimensionPixelSize(R.dimen.bubblebar_size)); setBackgroundDrawable(mBubbleBarBackground); + + mWidthAnimator.setDuration(WIDTH_ANIMATION_DURATION_MS); + mWidthAnimator.addUpdateListener(animation -> { + updateChildrenRenderNodeProperties(); + invalidate(); + }); + mWidthAnimator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + mBubbleBarBackground.showArrow(mIsBarExpanded); + if (!mIsBarExpanded && mReorderRunnable != null) { + mReorderRunnable.run(); + mReorderRunnable = null; + } + updateWidth(); + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + + @Override + public void onAnimationStart(Animator animation) { + mBubbleBarBackground.showArrow(true); + } + }); } @Override @@ -146,34 +181,62 @@ public class BubbleBarView extends FrameLayout { return mBubbleBarBounds; } - // TODO: (b/273592694) animate it + // TODO: (b/280605790) animate it @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (getChildCount() + 1 > MAX_BUBBLES) { removeViewInLayout(getChildAt(getChildCount() - 1)); } super.addView(child, index, params); + updateWidth(); + } + + // TODO: (b/283309949) animate it + @Override + public void removeView(View view) { + super.removeView(view); + updateWidth(); + } + + private void updateWidth() { + LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); + lp.width = (int) (mIsBarExpanded ? expandedWidth() : collapsedWidth()); + setLayoutParams(lp); } /** * Updates the z order, positions, and badge visibility of the bubble views in the bar based * on the expanded state. */ - // TODO: (b/273592694) animate it private void updateChildrenRenderNodeProperties() { + final float widthState = (float) mWidthAnimator.getAnimatedValue(); + final float currentWidth = getWidth(); + final float expandedWidth = expandedWidth(); + final float collapsedWidth = collapsedWidth(); int bubbleCount = getChildCount(); final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f; for (int i = 0; i < bubbleCount; i++) { BubbleView bv = (BubbleView) getChildAt(i); bv.setTranslationY(ty); + + // the position of the bubble when the bar is fully expanded + final float expandedX = i * (mIconSize + mIconSpacing); + // the position of the bubble when the bar is fully collapsed + final float collapsedX = i * mIconOverlapAmount; + if (mIsBarExpanded) { - final float tx = i * (mIconSize + mIconSpacing); - bv.setTranslationX(tx); - bv.setZ(0); + // where the bubble will end up when the animation ends + final float targetX = currentWidth - expandedWidth + expandedX; + bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX); + // if we're fully expanded, set the z level to 0 + if (widthState == 1f) { + bv.setZ(0); + } bv.showBadge(); } else { + final float targetX = currentWidth - collapsedWidth + collapsedX; + bv.setTranslationX(widthState * (expandedX - targetX) + targetX); bv.setZ((MAX_BUBBLES * mBubbleElevation) - i); - bv.setTranslationX(i * mIconOverlapAmount); if (i > 0) { bv.hideBadge(); } else { @@ -181,13 +244,32 @@ public class BubbleBarView extends FrameLayout { } } } + + // update the arrow position + final float collapsedArrowPosition = arrowPositionForSelectedWhenCollapsed(); + final float expandedArrowPosition = arrowPositionForSelectedWhenExpanded(); + final float interpolatedWidth = + widthState * (expandedWidth - collapsedWidth) + collapsedWidth; + if (mIsBarExpanded) { + // when the bar is expanding, the selected bubble is always the first, so the arrow + // always shifts with the interpolated width. + final float arrowPosition = currentWidth - interpolatedWidth + collapsedArrowPosition; + mBubbleBarBackground.setArrowPosition(arrowPosition); + } else { + final float targetPosition = currentWidth - collapsedWidth + collapsedArrowPosition; + final float arrowPosition = + targetPosition + widthState * (expandedArrowPosition - targetPosition); + mBubbleBarBackground.setArrowPosition(arrowPosition); + } + + mBubbleBarBackground.setWidth(interpolatedWidth); } /** * Reorders the views to match the provided list. */ public void reorder(List viewOrder) { - if (isExpanded()) { + if (isExpanded() || mWidthAnimator.isRunning()) { mReorderRunnable = () -> doReorder(viewOrder); } else { doReorder(viewOrder); @@ -247,6 +329,16 @@ public class BubbleBarView extends FrameLayout { } } + private float arrowPositionForSelectedWhenExpanded() { + final int index = indexOfChild(mSelectedBubbleView); + return getPaddingStart() + index * (mIconSize + mIconSpacing) + mIconSize / 2f; + } + + private float arrowPositionForSelectedWhenCollapsed() { + final int index = indexOfChild(mSelectedBubbleView); + return getPaddingStart() + index * (mIconOverlapAmount) + mIconSize / 2f; + } + @Override public void setOnClickListener(View.OnClickListener listener) { mOnClickListener = listener; @@ -264,18 +356,16 @@ public class BubbleBarView extends FrameLayout { /** * Sets whether the bubble bar is expanded or collapsed. */ - // TODO: (b/273592694) animate it public void setExpanded(boolean isBarExpanded) { if (mIsBarExpanded != isBarExpanded) { mIsBarExpanded = isBarExpanded; updateArrowForSelected(/* shouldAnimate= */ false); setOrUnsetClickListener(); - if (!isBarExpanded && mReorderRunnable != null) { - mReorderRunnable.run(); - mReorderRunnable = null; + if (isBarExpanded) { + mWidthAnimator.start(); + } else { + mWidthAnimator.reverse(); } - mBubbleBarBackground.showArrow(mIsBarExpanded); - requestLayout(); // trigger layout to reposition views & update size for expansion } } @@ -286,19 +376,16 @@ public class BubbleBarView extends FrameLayout { return mIsBarExpanded; } - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + private float expandedWidth() { final int childCount = getChildCount(); - final float iconWidth = mIsBarExpanded - ? (childCount * (mIconSize + mIconSpacing)) - : mIconSize + ((childCount - 1) * mIconOverlapAmount); - final int totalWidth = (int) iconWidth + getPaddingStart() + getPaddingEnd(); - setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec)); + final int horizontalPadding = getPaddingStart() + getPaddingEnd(); + return childCount * (mIconSize + mIconSpacing) + horizontalPadding; + } - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - measureChild(child, (int) mIconSize, (int) mIconSize); - } + private float collapsedWidth() { + final int childCount = getChildCount(); + final int horizontalPadding = getPaddingStart() + getPaddingEnd(); + return mIconSize + ((childCount - 1) * mIconOverlapAmount) + horizontalPadding; } /** From 3e94d52732cdc1a10c117cf1bed9a8d6242db5c7 Mon Sep 17 00:00:00 2001 From: Liran Binyamin Date: Fri, 19 May 2023 12:36:54 -0400 Subject: [PATCH 2/9] Fade the bubble bar arrow in and out when the bubble bar expands and collapses. Demo: http://recall/-/bJtug1HhvXkkeA4MQvIaiP/hKhpM98JZe1gT8bwtYkNeW Bug: 280604480 Test: Tested on a physical device Change-Id: Iaee9aeb0a8c1e2bd61816964e4cae5f7bea926ff --- .../launcher3/taskbar/bubbles/BubbleBarBackground.kt | 11 +++++++++-- .../launcher3/taskbar/bubbles/BubbleBarView.java | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt index cd95ad38fc..1e3f4f13f2 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt @@ -61,8 +61,11 @@ class BubbleBarBackground(context: TaskbarActivityContext, private val backgroun pointerSize = res.getDimension(R.dimen.bubblebar_pointer_size) shadowAlpha = - if (Utilities.isDarkTheme(context)) DARK_THEME_SHADOW_ALPHA - else LIGHT_THEME_SHADOW_ALPHA + if (Utilities.isDarkTheme(context)) { + DARK_THEME_SHADOW_ALPHA + } else { + LIGHT_THEME_SHADOW_ALPHA + } arrowDrawable = ShapeDrawable(TriangleShape.create(pointerSize, pointerSize, /* pointUp= */ true)) @@ -134,4 +137,8 @@ class BubbleBarBackground(context: TaskbarActivityContext, private val backgroun override fun setColorFilter(colorFilter: ColorFilter?) { paint.colorFilter = colorFilter } + + fun setArrowAlpha(alpha: Int) { + arrowDrawable.paint.alpha = alpha + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index 8b3e2e42ef..ac5bfc686a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -262,6 +262,7 @@ public class BubbleBarView extends FrameLayout { mBubbleBarBackground.setArrowPosition(arrowPosition); } + mBubbleBarBackground.setArrowAlpha((int) (255 * widthState)); mBubbleBarBackground.setWidth(interpolatedWidth); } From ad6ebab8873430129d00161eff783f492bddab23 Mon Sep 17 00:00:00 2001 From: vadimt Date: Wed, 21 Jun 2023 16:11:37 -0700 Subject: [PATCH 3/9] View capture analyzer tree walker + Alpha jump detector V1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The analyzer is an utility that can be used from the test, and not only. If an anomaly (such as a sudden jump of view’s coordinates between frames) is detected, the utility will throw an error. The CL includes an early version of detectors for one of the anomalies we plan to detect: alpha jump (included), flash, position jump. The analysis is currently not invoked from tests, we simply are adding the code. Alpha jump detector: The included alpha jump detector contains a long (but still incomplete) list of views for which we ignore alpha jumps. This list should go away after view capture data begins supporting fields like “ignore alpha jumps for this view”. We currently detect only alpha jumps by 100%, i.e. when the view switches from completely opaque state to completely invisible or vice versa. ScrimView treatment: Since we don’t know at the moment whether ScrimView is opaque, we currently ignore all activity under it. Bug: 286251603 Flag: N/A Test: manually on Launcher hacked to invoke this analyzer Change-Id: Ic86aff561a0c273afd7714d8287cb724bb2aecaf --- .../AlphaJumpDetector.java | 91 +++++++ .../ViewCaptureAnalyzer.java | 238 ++++++++++++++++++ 2 files changed, 329 insertions(+) create mode 100644 tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java create mode 100644 tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java new file mode 100644 index 0000000000..e40fb79b1b --- /dev/null +++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java @@ -0,0 +1,91 @@ +/* + * 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.launcher3.util.viewcapture_analysis; + +import static com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.diagPathFromRoot; + +import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode; +import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnomalyDetector; + +import java.util.Collection; +import java.util.Set; + +/** + * Anomaly detector that triggers an error when alpha of a view changes too rapidly. + * Invisible views are treated as if they had zero alpha. + */ +final class AlphaJumpDetector extends AnomalyDetector { + // Paths of nodes that are excluded from analysis. + private static final Collection PATHS_TO_IGNORE = Set.of( + "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer" + + ":id/drag_layer|SearchContainerView:id/apps_view|SearchRecyclerView:id" + + "/search_results_list_view|SearchResultSmallIconRow", + "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer" + + ":id/drag_layer|SearchContainerView:id/apps_view|SearchRecyclerView:id" + + "/search_results_list_view|SearchResultIcon", + "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer" + + ":id/drag_layer|LauncherRecentsView:id/overview_panel|TaskView", + "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer" + + ":id/drag_layer|WidgetsFullSheet|SpringRelativeLayout:id/container" + + "|WidgetsRecyclerView:id/primary_widgets_list_view|WidgetsListHeader:id" + + "/widgets_list_header", + "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer" + + ":id/drag_layer|WidgetsFullSheet|SpringRelativeLayout:id/container" + + "|WidgetsRecyclerView:id/primary_widgets_list_view" + + "|StickyHeaderLayout$EmptySpaceView", + "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer" + + ":id/drag_layer|SearchContainerView:id/apps_view|AllAppsRecyclerView:id" + + "/apps_list_view|BubbleTextView:id/icon", + "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer" + + ":id/drag_layer|LauncherRecentsView:id/overview_panel|ClearAllButton:id" + + "/clear_all", + "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer" + + ":id/drag_layer|NexusOverviewActionsView:id/overview_actions_view" + + "|LinearLayout:id/action_buttons" + ); + // Minimal increase or decrease of view's alpha between frames that triggers the error. + private static final float ALPHA_JUMP_THRESHOLD = 1f; + + @Override + void initializeNode(AnalysisNode info) { + // If the parent view ignores alpha jumps, its descendants will too. + final boolean parentIgnoreAlphaJumps = info.parent != null && info.parent.ignoreAlphaJumps; + info.ignoreAlphaJumps = parentIgnoreAlphaJumps + || PATHS_TO_IGNORE.contains(diagPathFromRoot(info)); + } + + @Override + void detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN) { + // If the view was previously seen, proceed with analysis only if it was present in the + // view hierarchy in the previous frame. + if (oldInfo != null && oldInfo.frameN != frameN) return; + + final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo; + if (latestInfo.ignoreAlphaJumps) return; + + final float oldAlpha = oldInfo != null ? oldInfo.alpha : 0; + final float newAlpha = newInfo != null ? newInfo.alpha : 0; + final float alphaDeltaAbs = Math.abs(newAlpha - oldAlpha); + + if (alphaDeltaAbs >= ALPHA_JUMP_THRESHOLD) { + throw new AssertionError( + String.format( + "Alpha jump detected in ViewCapture data: alpha change: %s (%s -> %s)" + + ", threshold: %s, view: %s", + alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo)); + } + } +} diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java new file mode 100644 index 0000000000..5a2611c362 --- /dev/null +++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java @@ -0,0 +1,238 @@ +/* + * 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.launcher3.util.viewcapture_analysis; + +import static android.view.View.VISIBLE; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.app.viewcapture.data.ExportedData; +import com.android.app.viewcapture.data.FrameData; +import com.android.app.viewcapture.data.ViewNode; +import com.android.app.viewcapture.data.WindowData; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Utility that analyzes ViewCapture data and finds anomalies such as views appearing or + * disappearing without alpha-fading. + */ +public class ViewCaptureAnalyzer { + private static final String SCRIM_VIEW_CLASS = "com.android.launcher3.views.ScrimView"; + + /** + * Detector of one kind of anomaly. + */ + abstract static class AnomalyDetector { + /** + * Initializes fields of the node that are specific to the anomaly detected by this + * detector. + */ + abstract void initializeNode(@NonNull AnalysisNode info); + + /** + * Detects anomalies by looking at the last occurrence of a view, and the current one. + * null value means that the view. 'oldInfo' and 'newInfo' cannot be both null. + * If an anomaly is detected, an exception will be thrown. + * + * @param oldInfo the view, as seen in the last frame that contained it in the view + * hierarchy before 'currentFrame'. 'null' means that the view is first seen + * in the 'currentFrame'. + * @param newInfo the view in the view hierarchy of the 'currentFrame'. 'null' means that + * the view is not present in the 'currentFrame', but was present in earlier + * frames. + * @param frameN number of the current frame. + */ + abstract void detectAnomalies( + @Nullable AnalysisNode oldInfo, @Nullable AnalysisNode newInfo, int frameN); + } + + // All detectors. They will be invoked in the order listed here. + private static final Iterable ANOMALY_DETECTORS = Arrays.asList( + new AlphaJumpDetector() + ); + + // A view from view capture data converted to a form that's convenient for detecting anomalies. + static class AnalysisNode { + public String className; + public String resourceId; + public AnalysisNode parent; + + // Window coordinates of the view. + public float left; + public float top; + + // Visible scale and alpha, build recursively from the ancestor list. + public float scaleX; + public float scaleY; + public float alpha; + + public int frameN; + public ViewNode viewCaptureNode; + + public boolean ignoreAlphaJumps; + + @Override + public String toString() { + return String.format("window coordinates: (%s, %s), class path from the root: %s", + left, top, diagPathFromRoot(this)); + } + } + + /** + * Scans a view capture record and throws an error if an anomaly is found. + */ + public static void assertNoAnomalies(ExportedData viewCaptureData) { + final int scrimClassIndex = viewCaptureData.getClassnameList().indexOf(SCRIM_VIEW_CLASS); + + final int windowDataCount = viewCaptureData.getWindowDataCount(); + for (int i = 0; i < windowDataCount; ++i) { + analyzeWindowData(viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex); + } + } + + private static void analyzeWindowData(ExportedData viewCaptureData, WindowData windowData, + int scrimClassIndex) { + // View hash code => Last seen node with this hash code. + // The view is added when we analyze the first frame where it's visible. + // After that, it gets updated for every frame where it's visible. + // As we go though frames, if a view becomes invisible, it stays in the map. + final Map lastSeenNodes = new HashMap<>(); + + for (int frameN = 0; frameN < windowData.getFrameDataCount(); ++frameN) { + analyzeFrame(frameN, windowData.getFrameData(frameN), viewCaptureData, lastSeenNodes, + scrimClassIndex); + } + } + + private static void analyzeFrame(int frameN, FrameData frame, ExportedData viewCaptureData, + Map lastSeenNodes, int scrimClassIndex) { + // Analyze the node tree starting from the root. + analyzeView( + frame.getNode(), + /* parent = */ null, + frameN, + /* leftShift = */ 0, + /* topShift = */ 0, + viewCaptureData, + lastSeenNodes, + scrimClassIndex); + + // Analyze transitions when a view visible in the last frame become invisible in the + // current one. + for (AnalysisNode info : lastSeenNodes.values()) { + if (info.frameN == frameN - 1) { + if (!info.viewCaptureNode.getWillNotDraw()) { + ANOMALY_DETECTORS.forEach( + detector -> detector.detectAnomalies( + /* oldInfo = */ info, + /* newInfo = */ null, + frameN)); + } + } + } + } + + private static void analyzeView(ViewNode viewCaptureNode, AnalysisNode parent, int frameN, + float leftShift, float topShift, ExportedData viewCaptureData, + Map lastSeenNodes, int scrimClassIndex) { + // Skip analysis of invisible views + final float parentAlpha = parent != null ? parent.alpha : 1; + final float alpha = getVisibleAlpha(viewCaptureNode, parentAlpha); + if (alpha <= 0.0) return; + + // Calculate analysis node parameters + final int hashcode = viewCaptureNode.getHashcode(); + final int classIndex = viewCaptureNode.getClassnameIndex(); + + final float parentScaleX = parent != null ? parent.scaleX : 1; + final float parentScaleY = parent != null ? parent.scaleY : 1; + final float scaleX = parentScaleX * viewCaptureNode.getScaleX(); + final float scaleY = parentScaleY * viewCaptureNode.getScaleY(); + + final float left = leftShift + + (viewCaptureNode.getLeft() + viewCaptureNode.getTranslationX()) * parentScaleX + + viewCaptureNode.getWidth() * (parentScaleX - scaleX) / 2; + final float top = topShift + + (viewCaptureNode.getTop() + viewCaptureNode.getTranslationY()) * parentScaleY + + viewCaptureNode.getHeight() * (parentScaleY - scaleY) / 2; + + // Initialize new analysis node + final AnalysisNode newAnalysisNode = new AnalysisNode(); + newAnalysisNode.className = viewCaptureData.getClassname(classIndex); + newAnalysisNode.resourceId = viewCaptureNode.getId(); + newAnalysisNode.parent = parent; + newAnalysisNode.left = left; + newAnalysisNode.top = top; + newAnalysisNode.scaleX = scaleX; + newAnalysisNode.scaleY = scaleY; + newAnalysisNode.alpha = alpha; + newAnalysisNode.frameN = frameN; + newAnalysisNode.viewCaptureNode = viewCaptureNode; + ANOMALY_DETECTORS.forEach(detector -> detector.initializeNode(newAnalysisNode)); + + // Detect anomalies for the view + final AnalysisNode oldAnalysisNode = lastSeenNodes.get(hashcode); // may be null + if (frameN != 0 && !viewCaptureNode.getWillNotDraw()) { + ANOMALY_DETECTORS.forEach( + detector -> detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN)); + } + lastSeenNodes.put(hashcode, newAnalysisNode); + + // Enumerate children starting from the topmost one. Stop at ScrimView, if present. + final float leftShiftForChildren = left - viewCaptureNode.getScrollX(); + final float topShiftForChildren = top - viewCaptureNode.getScrollY(); + for (int i = viewCaptureNode.getChildrenCount() - 1; i >= 0; --i) { + final ViewNode child = viewCaptureNode.getChildren(i); + + // Don't analyze anything under scrim view because we don't know whether it's + // transparent. + if (child.getClassnameIndex() == scrimClassIndex) break; + + analyzeView(child, newAnalysisNode, frameN, leftShiftForChildren, topShiftForChildren, + viewCaptureData, lastSeenNodes, + scrimClassIndex); + } + } + + private static float getVisibleAlpha(ViewNode node, float parenVisibleAlpha) { + return node.getVisibility() == VISIBLE + ? parenVisibleAlpha * Math.max(0, Math.min(node.getAlpha(), 1)) + : 0f; + } + + private static String classNameToSimpleName(String className) { + return className.substring(className.lastIndexOf(".") + 1); + } + + static String diagPathFromRoot(AnalysisNode nodeBox) { + final StringBuilder path = new StringBuilder(diagPathElement(nodeBox)); + for (AnalysisNode ancestor = nodeBox.parent; ancestor != null; ancestor = ancestor.parent) { + path.insert(0, diagPathElement(ancestor) + "|"); + } + return path.toString(); + } + + private static String diagPathElement(AnalysisNode nodeBox) { + final StringBuilder sb = new StringBuilder(); + sb.append(classNameToSimpleName(nodeBox.className)); + if (!"NO_ID".equals(nodeBox.resourceId)) sb.append(":" + nodeBox.resourceId); + return sb.toString(); + } +} From 97eb471f2efda1eccce7eba8c68301b2fa34c5e7 Mon Sep 17 00:00:00 2001 From: Jerry Chang Date: Mon, 26 Jun 2023 12:23:08 +0000 Subject: [PATCH 4/9] Prevent exception when quick switching between two split pairs When switching in between two split pairs just quick enough, it is possible to send the second entering split transition while it is animating the first entering split transition which is merged to a recents-during-split transition. The split parents might not be collected into the second transition because the split parents are already visible at that time, and there is no recents transition to merge to. So updates to not throwing when there is no split parent when composing a recents-to-split animation. Bug: 236226779 Test: repro steps of the bug and the Launcher won't throw Change-Id: I3a595722721186e8de7d60c9fb8c099ec799804a --- quickstep/src/com/android/quickstep/TaskViewUtils.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java index 499a2601a9..1238819acd 100644 --- a/quickstep/src/com/android/quickstep/TaskViewUtils.java +++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java @@ -473,16 +473,14 @@ public final class TaskViewUtils { throw new IllegalStateException( "Expected task to be showing, but it is " + mode); } - if (change.getParent() == null) { - throw new IllegalStateException("Initiating multi-split launch but the split" - + "root of " + taskId + " is already visible or has broken hierarchy."); - } } if (taskId == initialTaskId) { - splitRoot1 = transitionInfo.getChange(change.getParent()); + splitRoot1 = change.getParent() == null ? change : + transitionInfo.getChange(change.getParent()); } if (taskId == secondTaskId) { - splitRoot2 = transitionInfo.getChange(change.getParent()); + splitRoot2 = change.getParent() == null ? change : + transitionInfo.getChange(change.getParent()); } } From b02dafc1d456ee3665c53f32a51f7f84b4008415 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Thu, 22 Jun 2023 11:17:29 -0700 Subject: [PATCH 5/9] Removing explicit target SDK for launcher > It will automatically target the latest SDK it was compiled with Bug: 284349887 Test: Presubmit and manual Flag: N/A Change-Id: I32629b0dd710c3c04d8f70f988b7279e7cae6731 --- quickstep/AndroidManifest-launcher.xml | 1 - .../android/launcher3/proxy/StartActivityParams.java | 6 +++++- .../launcher3/taskbar/TaskbarActivityContext.java | 2 ++ .../launcher3/uioverrides/QuickstepLauncher.java | 4 ++++ .../src/com/android/quickstep/RecentsActivity.java | 2 ++ src/com/android/launcher3/LauncherAppState.java | 8 +++++--- src/com/android/launcher3/Utilities.java | 12 ++++++++++++ .../launcher3/notification/NotificationInfo.java | 8 ++++---- .../launcher3/pm/ShortcutConfigActivityInfo.java | 7 ++++++- .../launcher3/popup/RemoteActionShortcut.java | 8 +++++++- src/com/android/launcher3/views/ActivityContext.java | 7 +++---- .../launcher3/widget/LauncherWidgetHolder.java | 10 ++++++++-- 12 files changed, 58 insertions(+), 17 deletions(-) diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml index 7d7054f5a5..c6e2d8cb74 100644 --- a/quickstep/AndroidManifest-launcher.xml +++ b/quickstep/AndroidManifest-launcher.xml @@ -20,7 +20,6 @@ - - + android:background="?androidprv:attr/materialColorSurfaceContainer" + android:fitsSystemWindows="true"> - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toTopOf="@id/guideline" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toStartOf="@id/gesture_tutorial_menu_back_button"> - - - - + + + + + + + + + + + + + + + + + + + + + + + + + +