diff options
Diffstat (limited to 'libs')
77 files changed, 5039 insertions, 486 deletions
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index 50cfd941adb3..4c2433fab2f8 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java @@ -443,7 +443,8 @@ public class OverlayPresentationTest { assertThat(taskContainer.getTaskFragmentContainers()).containsExactly(overlayContainer); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(Configuration.EMPTY, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */)); + DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */)); mSplitController.updateOverlayContainer(mTransaction, overlayContainer); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 02031a67e7e3..8c274a26177d 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -1139,7 +1139,8 @@ public class SplitControllerTest { public void testOnTransactionReady_taskFragmentParentInfoChanged() { final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(Configuration.EMPTY, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */); + DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */); transaction.addChange(new TaskFragmentTransaction.Change( TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED) .setTaskId(TASK_ID) diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java index e56c8ab686e7..7b77235f66f7 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -79,14 +79,16 @@ public class TaskContainerTest { configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */)); + DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */)); assertEquals(WINDOWING_MODE_MULTI_WINDOW, taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */)); + DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */)); assertEquals(WINDOWING_MODE_FREEFORM, taskContainer.getWindowingModeForSplitTaskFragment(splitBounds)); @@ -106,13 +108,15 @@ public class TaskContainerTest { configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */)); + DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */)); assertFalse(taskContainer.isInPictureInPicture()); configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_PINNED); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */)); + DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */)); assertTrue(taskContainer.isInPictureInPicture()); } diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index fd4522e02438..5ad144d50b87 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -160,6 +160,7 @@ android_library { "kotlinx-coroutines-core", "iconloader_base", "com_android_wm_shell_flags_lib", + "com.android.window.flags.window-aconfig-java", "WindowManager-Shell-proto", "dagger2", "jsr330", diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 4d2d960822d1..4511f3b91c5c 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -50,3 +50,10 @@ flag { bug: "290220798" is_fixed_read_only: true } + +flag { + name: "enable_left_right_split_in_portrait" + namespace: "multitasking" + description: "Enables left/right split in portrait" + bug: "291018646" +} diff --git a/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml b/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml deleted file mode 100644 index d732b01ce106..000000000000 --- a/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml +++ /dev/null @@ -1,38 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2015 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. ---> - -<com.android.wm.shell.legacysplitscreen.DividerView - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_height="match_parent" - android:layout_width="match_parent"> - - <View - style="@style/DockedDividerBackground" - android:id="@+id/docked_divider_background" - android:background="@color/split_divider_background"/> - - <com.android.wm.shell.legacysplitscreen.MinimizedDockShadow - style="@style/DockedDividerMinimizedShadow" - android:id="@+id/minimized_dock_shadow" - android:alpha="0"/> - - <com.android.wm.shell.common.split.DividerHandleView - style="@style/DockedDividerHandle" - android:id="@+id/docked_divider_handle" - android:contentDescription="@string/accessibility_divider" - android:background="@null"/> - -</com.android.wm.shell.legacysplitscreen.DividerView> diff --git a/libs/WindowManager/Shell/res/layout/split_divider.xml b/libs/WindowManager/Shell/res/layout/split_divider.xml index e3be700469a7..db35c8c57456 100644 --- a/libs/WindowManager/Shell/res/layout/split_divider.xml +++ b/libs/WindowManager/Shell/res/layout/split_divider.xml @@ -24,17 +24,16 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - <View - style="@style/DockedDividerBackground" - android:id="@+id/docked_divider_background"/> - <com.android.wm.shell.common.split.DividerHandleView - style="@style/DockedDividerHandle" android:id="@+id/docked_divider_handle" + android:layout_height="match_parent" + android:layout_width="match_parent" + android:layout_gravity="center" android:contentDescription="@string/accessibility_divider" android:background="@null"/> <com.android.wm.shell.common.split.DividerRoundedCorner + android:id="@+id/docked_divider_rounded_corner" android:layout_width="match_parent" android:layout_height="match_parent"/> diff --git a/libs/WindowManager/Shell/res/values-land/dimens.xml b/libs/WindowManager/Shell/res/values-land/dimens.xml index a95323fd4801..1b96fa227383 100644 --- a/libs/WindowManager/Shell/res/values-land/dimens.xml +++ b/libs/WindowManager/Shell/res/values-land/dimens.xml @@ -16,13 +16,6 @@ */ --> <resources> - <!-- Divider handle size for legacy split screen --> - <dimen name="docked_divider_handle_width">2dp</dimen> - <dimen name="docked_divider_handle_height">16dp</dimen> - <!-- Divider handle size for split screen --> - <dimen name="split_divider_handle_width">3dp</dimen> - <dimen name="split_divider_handle_height">72dp</dimen> - <!-- Padding between status bar and bubbles when displayed in expanded state, smaller value in landscape since we have limited vertical space--> <dimen name="bubble_padding_top">4dp</dimen> diff --git a/libs/WindowManager/Shell/res/values-land/styles.xml b/libs/WindowManager/Shell/res/values-land/styles.xml deleted file mode 100644 index e89f65bef792..000000000000 --- a/libs/WindowManager/Shell/res/values-land/styles.xml +++ /dev/null @@ -1,36 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2020 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 xmlns:android="http://schemas.android.com/apk/res/android"> - <style name="DockedDividerBackground"> - <item name="android:layout_width">@dimen/split_divider_bar_width</item> - <item name="android:layout_height">match_parent</item> - <item name="android:layout_gravity">center_horizontal</item> - <item name="android:background">@color/split_divider_background</item> - </style> - - <style name="DockedDividerHandle"> - <item name="android:layout_gravity">center</item> - <item name="android:layout_width">48dp</item> - <item name="android:layout_height">96dp</item> - </style> - - <style name="DockedDividerMinimizedShadow"> - <item name="android:layout_width">8dp</item> - <item name="android:layout_height">match_parent</item> - </style> -</resources> - diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index f20d44df21b1..8f9de6168bc7 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -96,6 +96,9 @@ <dimen name="docked_divider_handle_width">16dp</dimen> <dimen name="docked_divider_handle_height">2dp</dimen> <!-- Divider handle size for split screen --> + <dimen name="split_divider_handle_region_width">96dp</dimen> + <dimen name="split_divider_handle_region_height">48dp</dimen> + <dimen name="split_divider_handle_width">72dp</dimen> <dimen name="split_divider_handle_height">3dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 468cfd5260cc..08c2a02acf55 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -60,20 +60,9 @@ <style name="DockedDividerBackground"> <item name="android:layout_width">match_parent</item> - <item name="android:layout_height">@dimen/split_divider_bar_width</item> - <item name="android:layout_gravity">center_vertical</item> - <item name="android:background">@color/split_divider_background</item> - </style> - - <style name="DockedDividerMinimizedShadow"> - <item name="android:layout_width">match_parent</item> - <item name="android:layout_height">8dp</item> - </style> - - <style name="DockedDividerHandle"> + <item name="android:layout_height">match_parent</item> <item name="android:layout_gravity">center</item> - <item name="android:layout_width">96dp</item> - <item name="android:layout_height">48dp</item> + <item name="android:background">@color/split_divider_background</item> </style> <style name="TvPipEduText"> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index f5b877a70b84..a3eb429b1d7e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -412,6 +412,23 @@ public class BubbleExpandedView extends LinearLayout { setLayoutDirection(LAYOUT_DIRECTION_LOCALE); } + + /** Updates the width of the task view if it changed. */ + void updateTaskViewContentWidth() { + if (mTaskView != null) { + int width = getContentWidth(); + if (mTaskView.getWidth() != width) { + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(width, MATCH_PARENT); + mTaskView.setLayoutParams(lp); + } + } + } + + private int getContentWidth() { + boolean isStackOnLeft = mPositioner.isStackOnLeft(mStackView.getStackPosition()); + return mPositioner.getTaskViewContentWidth(isStackOnLeft); + } + /** * Initialize {@link BubbleController} and {@link BubbleStackView} here, this method must need * to be called after view inflate. @@ -438,7 +455,12 @@ public class BubbleExpandedView extends LinearLayout { mController.getTaskViewTransitions(), mController.getSyncTransactionQueue()); mTaskView = new TaskView(mContext, mTaskViewTaskController); mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener); - mExpandedViewContainer.addView(mTaskView); + + // set a fixed width so it is not recalculated as part of a rotation. the width will be + // updated manually after the rotation. + FrameLayout.LayoutParams lp = + new FrameLayout.LayoutParams(getContentWidth(), MATCH_PARENT); + mExpandedViewContainer.addView(mTaskView, lp); bringChildToFront(mTaskView); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 1efd9df3a1d9..5d161962be4a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -375,6 +375,13 @@ public class BubblePositioner { } } + /** Returns the width of the task view content. */ + public int getTaskViewContentWidth(boolean onLeft) { + int[] paddings = getExpandedViewContainerPadding(onLeft, /* isOverflow = */ false); + int pointerOffset = showBubblesVertically() ? getPointerSize() : 0; + return mPositionRect.width() - paddings[0] - paddings[2] - pointerOffset; + } + /** Gets the y position of the expanded view if it was top-aligned. */ public float getExpandedViewYTopAligned() { final int top = getAvailableRect().top; @@ -390,6 +397,9 @@ public class BubblePositioner { * the screen and the size of the elements around it (e.g. padding, pointer, manage button). */ public int getMaxExpandedViewHeight(boolean isOverflow) { + if (mDeviceConfig.isLargeScreen() && !mDeviceConfig.isSmallTablet() && !isOverflow) { + return getExpandedViewHeightForLargeScreen(); + } // Subtract top insets because availableRect.height would account for that int expandedContainerY = (int) getExpandedViewYTopAligned() - getInsets().top; int paddingTop = showBubblesVertically() @@ -407,6 +417,16 @@ public class BubblePositioner { - bottomPadding; } + private int getExpandedViewHeightForLargeScreen() { + // the expanded view height on large tablets is calculated based on the shortest screen + // size and is the same in both portrait and landscape + int maxVerticalInset = Math.max(mInsets.top, mInsets.bottom); + int shortestScreenSide = Math.min(getScreenRect().height(), getScreenRect().width()); + // Subtract pointer size because it's laid out in LinearLayout with the expanded view. + return shortestScreenSide - maxVerticalInset * 2 + - mManageButtonHeight - mPointerWidth - mExpandedViewPadding * 2; + } + /** * Determines the height for the bubble, ensuring a minimum height. If the height should be as * big as available, returns {@link #MAX_HEIGHT}. @@ -417,15 +437,6 @@ public class BubblePositioner { // overflow in landscape on phone is max return MAX_HEIGHT; } - - if (mDeviceConfig.isLargeScreen() && !mDeviceConfig.isSmallTablet() && !isOverflow) { - // the expanded view height on large tablets is calculated based on the shortest screen - // size and is the same in both portrait and landscape - int maxVerticalInset = Math.max(mInsets.top, mInsets.bottom); - int shortestScreenSide = Math.min(mScreenRect.height(), mScreenRect.width()); - return shortestScreenSide - 2 * maxVerticalInset - mManageButtonHeight; - } - float desiredHeight = isOverflow ? mOverflowHeight : ((Bubble) bubble).getDesiredHeight(mContext); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index 8f904c42d247..ff4da853654d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -537,8 +537,8 @@ public class BubbleStackView extends FrameLayout return; } - final boolean clickedBubbleIsCurrentlyExpandedBubble = - clickedBubble.getKey().equals(mExpandedBubble.getKey()); + final boolean clickedBubbleIsCurrentlyExpandedBubble = mExpandedBubble != null + && clickedBubble.getKey().equals(mExpandedBubble.getKey()); if (isExpanded()) { mExpandedAnimationController.onGestureFinished(); @@ -1510,6 +1510,11 @@ public class BubbleStackView extends FrameLayout updateExpandedView(); } setUpManageMenu(); + if (mShowingManage) { + // the manage menu location depends on the manage button location which may need a + // layout pass, so post this to the looper + post(() -> showManageMenu(true)); + } } @Override @@ -3288,6 +3293,7 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY(mExpandedBubble, mPositioner.showBubblesVertically() ? p.y : p.x)); mExpandedViewContainer.setTranslationX(0f); + mExpandedBubble.getExpandedView().updateTaskViewContentWidth(); mExpandedBubble.getExpandedView().updateView( mExpandedViewContainer.getLocationOnScreen()); updatePointerPosition(false /* forIme */); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 79f188ab2611..50e1f7311ce0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -67,6 +67,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView private boolean mIsOverflow; private BubbleTaskViewHelper mBubbleTaskViewHelper; private BubbleBarMenuViewController mMenuViewController; + private BubbleBarExpandedViewDragController mDragController; private @Nullable Supplier<Rect> mLayerBoundsSupplier; private @Nullable Listener mListener; @@ -180,6 +181,8 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView mHandleView.setOnClickListener(view -> { mMenuViewController.showMenu(true /* animated */); }); + + mDragController = new BubbleBarExpandedViewDragController(this); } public BubbleBarHandleView getHandleView() { @@ -386,4 +389,11 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView setContentVisibility(mIsContentVisible); } } + + /** + * Check whether the view is animating + */ + public boolean isAnimating() { + return mIsAnimating; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt new file mode 100644 index 000000000000..933794be071e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt @@ -0,0 +1,100 @@ +/* + * 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.wm.shell.bubbles.bar + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.graphics.PointF +import android.view.MotionEvent +import android.view.View +import com.android.wm.shell.animation.Interpolators +import com.android.wm.shell.common.bubbles.RelativeTouchListener + +/** Controller for handling drag interactions with [BubbleBarExpandedView] */ +class BubbleBarExpandedViewDragController(private val expandedView: BubbleBarExpandedView) { + + init { + expandedView.handleView.setOnTouchListener(HandleDragListener()) + } + + private fun resetExpandedViewPosition(initialX: Float, initialY: Float) { + val listener = object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + expandedView.isAnimating = true + } + + override fun onAnimationEnd(animation: Animator) { + expandedView.isAnimating = false + } + } + expandedView.animate() + .translationX(initialX) + .translationY(initialY) + .setDuration(RESET_POSITION_ANIM_DURATION) + .setInterpolator(Interpolators.EMPHASIZED_DECELERATE) + .setListener(listener) + .start() + } + + private inner class HandleDragListener : RelativeTouchListener() { + + private val expandedViewRestPosition = PointF() + + override fun onDown(v: View, ev: MotionEvent): Boolean { + // While animating, don't allow new touch events + if (expandedView.isAnimating) { + return false + } + expandedViewRestPosition.x = expandedView.translationX + expandedViewRestPosition.y = expandedView.translationY + return true + } + + override fun onMove( + v: View, + ev: MotionEvent, + viewInitialX: Float, + viewInitialY: Float, + dx: Float, + dy: Float + ) { + expandedView.translationX = expandedViewRestPosition.x + dx + expandedView.translationY = expandedViewRestPosition.y + dy + } + + override fun onUp( + v: View, + ev: MotionEvent, + viewInitialX: Float, + viewInitialY: Float, + dx: Float, + dy: Float, + velX: Float, + velY: Float + ) { + resetExpandedViewPosition(expandedViewRestPosition.x, expandedViewRestPosition.y) + } + + override fun onCancel(v: View, ev: MotionEvent, viewInitialX: Float, viewInitialY: Float) { + resetExpandedViewPosition(expandedViewRestPosition.x, expandedViewRestPosition.y) + } + } + + companion object { + const val RESET_POSITION_ANIM_DURATION = 300L + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt index d45e1265daac..4e55ba23407b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt @@ -78,8 +78,15 @@ abstract class RelativeTouchListener : View.OnTouchListener { velY: Float ) + open fun onCancel( + v: View, + ev: MotionEvent, + viewInitialX: Float, + viewInitialY: Float + ) {} + /** The raw coordinates of the last ACTION_DOWN event. */ - private val touchDown = PointF() + private var touchDown: PointF? = null /** The coordinates of the view, at the time of the last ACTION_DOWN event. */ private val viewPositionOnTouchDown = PointF() @@ -91,12 +98,11 @@ abstract class RelativeTouchListener : View.OnTouchListener { private var performedLongClick = false - @Suppress("UNCHECKED_CAST") override fun onTouch(v: View, ev: MotionEvent): Boolean { addMovement(ev) - val dx = ev.rawX - touchDown.x - val dy = ev.rawY - touchDown.y + val dx = touchDown?.let { ev.rawX - it.x } ?: 0f + val dy = touchDown?.let { ev.rawY - it.y } ?: 0f when (ev.action) { MotionEvent.ACTION_DOWN -> { @@ -108,7 +114,7 @@ abstract class RelativeTouchListener : View.OnTouchListener { // last gesture. touchSlop = ViewConfiguration.get(v.context).scaledTouchSlop - touchDown.set(ev.rawX, ev.rawY) + touchDown = PointF(ev.rawX, ev.rawY) viewPositionOnTouchDown.set(v.translationX, v.translationY) performedLongClick = false @@ -120,6 +126,7 @@ abstract class RelativeTouchListener : View.OnTouchListener { } MotionEvent.ACTION_MOVE -> { + if (touchDown == null) return false if (!movedEnough && hypot(dx, dy) > touchSlop && !performedLongClick) { movedEnough = true v.handler?.removeCallbacksAndMessages(null) @@ -131,6 +138,7 @@ abstract class RelativeTouchListener : View.OnTouchListener { } MotionEvent.ACTION_UP -> { + if (touchDown == null) return false if (movedEnough) { velocityTracker.computeCurrentVelocity(1000 /* units */) onUp(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy, @@ -143,12 +151,16 @@ abstract class RelativeTouchListener : View.OnTouchListener { velocityTracker.clear() movedEnough = false + touchDown = null } MotionEvent.ACTION_CANCEL -> { + if (touchDown == null) return false v.handler?.removeCallbacksAndMessages(null) velocityTracker.clear() movedEnough = false + touchDown = null + onCancel(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java index ec2680085fb5..999da2443248 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java @@ -68,24 +68,33 @@ public class DividerHandleView extends View { }; private final Paint mPaint = new Paint(); - private final int mWidth; - private final int mHeight; - private final int mTouchingWidth; - private final int mTouchingHeight; + private int mWidth; + private int mHeight; + private int mTouchingWidth; + private int mTouchingHeight; private int mCurrentWidth; private int mCurrentHeight; private AnimatorSet mAnimator; private boolean mTouching; private boolean mHovering; - private final int mHoveringWidth; - private final int mHoveringHeight; + private int mHoveringWidth; + private int mHoveringHeight; + private boolean mIsLeftRightSplit; public DividerHandleView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mPaint.setColor(getResources().getColor(R.color.docked_divider_handle, null)); mPaint.setAntiAlias(true); - mWidth = getResources().getDimensionPixelSize(R.dimen.split_divider_handle_width); - mHeight = getResources().getDimensionPixelSize(R.dimen.split_divider_handle_height); + updateDimens(); + } + + private void updateDimens() { + mWidth = getResources().getDimensionPixelSize(mIsLeftRightSplit + ? R.dimen.split_divider_handle_height + : R.dimen.split_divider_handle_width); + mHeight = getResources().getDimensionPixelSize(mIsLeftRightSplit + ? R.dimen.split_divider_handle_width + : R.dimen.split_divider_handle_height); mCurrentWidth = mWidth; mCurrentHeight = mHeight; mTouchingWidth = mWidth > mHeight ? mWidth / 2 : mWidth; @@ -94,6 +103,11 @@ public class DividerHandleView extends View { mHoveringHeight = mHeight > mWidth ? ((int) (mHeight * 1.5f)) : mHeight; } + void setIsLeftRightSplit(boolean isLeftRightSplit) { + mIsLeftRightSplit = isLeftRightSplit; + updateDimens(); + } + /** Sets touching state for this handle view. */ public void setTouching(boolean touching, boolean animate) { if (touching == mTouching) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java index 364bb651d55d..834c15d6b8d6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java @@ -16,7 +16,6 @@ package com.android.wm.shell.common.split; -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT; import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT; import static android.view.RoundedCorner.POSITION_TOP_LEFT; @@ -47,6 +46,7 @@ public class DividerRoundedCorner extends View { private InvertedRoundedCornerDrawInfo mTopRightCorner; private InvertedRoundedCornerDrawInfo mBottomLeftCorner; private InvertedRoundedCornerDrawInfo mBottomRightCorner; + private boolean mIsLeftRightSplit; public DividerRoundedCorner(Context context, @Nullable AttributeSet attrs) { super(context, attrs); @@ -98,8 +98,8 @@ public class DividerRoundedCorner extends View { return false; } - private boolean isLandscape() { - return getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; + void setIsLeftRightSplit(boolean isLeftRightSplit) { + mIsLeftRightSplit = isLeftRightSplit; } /** @@ -134,7 +134,7 @@ public class DividerRoundedCorner extends View { } private void calculateStartPos(Point outPos) { - if (isLandscape()) { + if (mIsLeftRightSplit) { // Place left corner at the right side of the divider bar. outPos.x = isLeftCorner() ? getWidth() / 2 + mDividerWidth / 2 diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java index 0b0c6937553b..0f0fbd9cc12f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java @@ -16,7 +16,6 @@ package com.android.wm.shell.common.split; -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; @@ -27,6 +26,8 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; import android.graphics.Rect; import android.os.Bundle; import android.provider.DeviceConfig; @@ -65,12 +66,15 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { public static final long TOUCH_ANIMATION_DURATION = 150; public static final long TOUCH_RELEASE_ANIMATION_DURATION = 200; + private final Paint mPaint = new Paint(); + private final Rect mBackgroundRect = new Rect(); private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); private SplitLayout mSplitLayout; private SplitWindowManager mSplitWindowManager; private SurfaceControlViewHost mViewHost; private DividerHandleView mHandle; + private DividerRoundedCorner mCorners; private View mBackground; private int mTouchElevation; @@ -81,6 +85,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private boolean mInteractive; private boolean mSetTouchRegion = true; private int mLastDraggingPosition; + private int mHandleRegionWidth; + private int mHandleRegionHeight; /** * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with @@ -123,7 +129,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); final DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; - if (isLandscape()) { + if (mSplitLayout.isLeftRightSplit()) { info.addAction(new AccessibilityAction(R.id.action_move_tl_full, mContext.getString(R.string.accessibility_action_divider_left_full))); if (snapAlgorithm.isFirstSplitTargetAvailable()) { @@ -215,6 +221,17 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mViewHost = viewHost; layout.getDividerBounds(mDividerBounds); onInsetsChanged(insetsState, false /* animate */); + + final boolean isLeftRightSplit = mSplitLayout.isLeftRightSplit(); + mHandle.setIsLeftRightSplit(isLeftRightSplit); + mCorners.setIsLeftRightSplit(isLeftRightSplit); + + mHandleRegionWidth = getResources().getDimensionPixelSize(isLeftRightSplit + ? R.dimen.split_divider_handle_region_height + : R.dimen.split_divider_handle_region_width); + mHandleRegionHeight = getResources().getDimensionPixelSize(isLeftRightSplit + ? R.dimen.split_divider_handle_region_width + : R.dimen.split_divider_handle_region_height); } void onInsetsChanged(InsetsState insetsState, boolean animate) { @@ -255,30 +272,47 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { super.onFinishInflate(); mDividerBar = findViewById(R.id.divider_bar); mHandle = findViewById(R.id.docked_divider_handle); - mBackground = findViewById(R.id.docked_divider_background); + mCorners = findViewById(R.id.docked_divider_rounded_corner); mTouchElevation = getResources().getDimensionPixelSize( R.dimen.docked_stack_divider_lift_elevation); mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener()); mInteractive = true; setOnTouchListener(this); mHandle.setAccessibilityDelegate(mHandleDelegate); + setWillNotDraw(false); + mPaint.setColor(getResources().getColor(R.color.split_divider_background, null)); + mPaint.setAntiAlias(true); + mPaint.setStyle(Paint.Style.FILL); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (mSetTouchRegion) { - mTempRect.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(), - mHandle.getBottom()); + int startX = (mDividerBounds.width() - mHandleRegionWidth) / 2; + int startY = (mDividerBounds.height() - mHandleRegionHeight) / 2; + mTempRect.set(startX, startY, startX + mHandleRegionWidth, + startY + mHandleRegionHeight); mSplitWindowManager.setTouchRegion(mTempRect); mSetTouchRegion = false; } + + if (changed) { + boolean isHorizontalSplit = mSplitLayout.isLeftRightSplit(); + int dividerSize = getResources().getDimensionPixelSize(R.dimen.split_divider_bar_width); + left = isHorizontalSplit ? (getWidth() - dividerSize) / 2 : 0; + top = isHorizontalSplit ? 0 : (getHeight() - dividerSize) / 2; + right = isHorizontalSplit ? left + dividerSize : getWidth(); + bottom = isHorizontalSplit ? getHeight() : top + dividerSize; + mBackgroundRect.set(left, top, right, bottom); + } } @Override public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { return PointerIcon.getSystemIcon(getContext(), - isLandscape() ? TYPE_HORIZONTAL_DOUBLE_ARROW : TYPE_VERTICAL_DOUBLE_ARROW); + mSplitLayout.isLeftRightSplit() ? TYPE_HORIZONTAL_DOUBLE_ARROW + : TYPE_VERTICAL_DOUBLE_ARROW); } @Override @@ -295,8 +329,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { // moving divider bar and calculating dragging velocity. event.setLocation(event.getRawX(), event.getRawY()); final int action = event.getAction() & MotionEvent.ACTION_MASK; - final boolean isLandscape = isLandscape(); - final int touchPos = (int) (isLandscape ? event.getX() : event.getY()); + final boolean isLeftRightSplit = mSplitLayout.isLeftRightSplit(); + final int touchPos = (int) (isLeftRightSplit ? event.getX() : event.getY()); switch (action) { case MotionEvent.ACTION_DOWN: mVelocityTracker = VelocityTracker.obtain(); @@ -328,7 +362,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mVelocityTracker.addMovement(event); mVelocityTracker.computeCurrentVelocity(1000 /* units */); - final float velocity = isLandscape + final float velocity = isLeftRightSplit ? mVelocityTracker.getXVelocity() : mVelocityTracker.getYVelocity(); final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos; @@ -410,6 +444,11 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { .start(); } + @Override + protected void onDraw(@NonNull Canvas canvas) { + canvas.drawRect(mBackgroundRect, mPaint); + } + @VisibleForTesting void releaseHovering() { mHandle.setHovering(false, true); @@ -446,10 +485,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mHandle.setVisibility(!mInteractive && hideHandle ? View.INVISIBLE : View.VISIBLE); } - private boolean isLandscape() { - return getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; - } - private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDoubleTap(MotionEvent e) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 63cdb4f151ff..b699533374df 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -79,7 +79,7 @@ import java.util.function.Consumer; * divide position changes. */ public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener { - + private static final String TAG = "SplitLayout"; public static final int PARALLAX_NONE = 0; public static final int PARALLAX_DISMISSING = 1; public static final int PARALLAX_ALIGN_CENTER = 2; @@ -121,12 +121,15 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private int mDividerPosition; private boolean mInitialized = false; private boolean mFreezeDividerWindow = false; + private boolean mIsLargeScreen = false; private int mOrientation; private int mRotation; private int mDensity; private int mUiMode; private final boolean mDimNonImeSide; + private final boolean mAllowLeftRightSplitInPortrait; + private boolean mIsLeftRightSplit; private ValueAnimator mDividerFlingAnimator; public SplitLayout(String windowName, Context context, Configuration configuration, @@ -138,6 +141,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mOrientation = configuration.orientation; mRotation = configuration.windowConfiguration.getRotation(); mDensity = configuration.densityDpi; + mIsLargeScreen = configuration.smallestScreenWidthDp >= 600; mSplitLayoutHandler = splitLayoutHandler; mDisplayController = displayController; mDisplayImeController = displayImeController; @@ -147,14 +151,17 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId()); mSurfaceEffectPolicy = new ResizingEffectPolicy(parallaxType); + final Resources res = mContext.getResources(); + mDimNonImeSide = res.getBoolean(R.bool.config_dimNonImeAttachedSide); + mAllowLeftRightSplitInPortrait = SplitScreenUtils.allowLeftRightSplitInPortrait(res); + mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, + configuration); + updateDividerConfig(mContext); mRootBounds.set(configuration.windowConfiguration.getBounds()); mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); resetDividerPosition(); - - mDimNonImeSide = mContext.getResources().getBoolean(R.bool.config_dimNonImeAttachedSide); - updateInvisibleRect(); } @@ -284,17 +291,17 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * Returns the divider position as a fraction from 0 to 1. */ public float getDividerPositionAsFraction() { - return Math.min(1f, Math.max(0f, isLandscape() + return Math.min(1f, Math.max(0f, mIsLeftRightSplit ? (float) ((mBounds1.right + mBounds2.left) / 2f) / mBounds2.right : (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom)); } private void updateInvisibleRect() { mInvisibleBounds.set(mRootBounds.left, mRootBounds.top, - isLandscape() ? mRootBounds.right / 2 : mRootBounds.right, - isLandscape() ? mRootBounds.bottom : mRootBounds.bottom / 2); - mInvisibleBounds.offset(isLandscape() ? mRootBounds.right : 0, - isLandscape() ? 0 : mRootBounds.bottom); + mIsLeftRightSplit ? mRootBounds.right / 2 : mRootBounds.right, + mIsLeftRightSplit ? mRootBounds.bottom : mRootBounds.bottom / 2); + mInvisibleBounds.offset(mIsLeftRightSplit ? mRootBounds.right : 0, + mIsLeftRightSplit ? 0 : mRootBounds.bottom); } /** Applies new configuration, returns {@code false} if there's no effect to the layout. */ @@ -309,6 +316,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange final int orientation = configuration.orientation; final int density = configuration.densityDpi; final int uiMode = configuration.uiMode; + final boolean wasLeftRightSplit = mIsLeftRightSplit; if (mOrientation == orientation && mRotation == rotation @@ -326,9 +334,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mRotation = rotation; mDensity = density; mUiMode = uiMode; + mIsLargeScreen = configuration.smallestScreenWidthDp >= 600; + mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, + configuration); mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); updateDividerConfig(mContext); - initDividerPosition(mTempRect); + initDividerPosition(mTempRect, wasLeftRightSplit); updateInvisibleRect(); return true; @@ -347,18 +358,27 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } // We only need new bounds here, other configuration should be update later. + final boolean wasLeftRightSplit = SplitScreenUtils.isLeftRightSplit( + mAllowLeftRightSplitInPortrait, mIsLargeScreen, + mRootBounds.width() >= mRootBounds.height()); mTempRect.set(mRootBounds); mRootBounds.set(tmpRect); + mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, + mIsLargeScreen, mRootBounds.width() >= mRootBounds.height()); mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); - initDividerPosition(mTempRect); + initDividerPosition(mTempRect, wasLeftRightSplit); } - private void initDividerPosition(Rect oldBounds) { + /** + * Updates the divider position to the position in the current orientation and bounds using the + * snap fraction calculated based on the previous orientation and bounds. + */ + private void initDividerPosition(Rect oldBounds, boolean wasLeftRightSplit) { final float snapRatio = (float) mDividerPosition - / (float) (isLandscape(oldBounds) ? oldBounds.width() : oldBounds.height()); + / (float) (wasLeftRightSplit ? oldBounds.width() : oldBounds.height()); // Estimate position by previous ratio. final float length = - (float) (isLandscape() ? mRootBounds.width() : mRootBounds.height()); + (float) (mIsLeftRightSplit ? mRootBounds.width() : mRootBounds.height()); final int estimatePosition = (int) (length * snapRatio); // Init divider position by estimated position using current bounds snap algorithm. mDividerPosition = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( @@ -376,8 +396,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange dividerBounds.set(mRootBounds); bounds1.set(mRootBounds); bounds2.set(mRootBounds); - final boolean isLandscape = isLandscape(mRootBounds); - if (isLandscape) { + if (mIsLeftRightSplit) { position += mRootBounds.left; dividerBounds.left = position - mDividerInsets; dividerBounds.right = dividerBounds.left + mDividerWindowWidth; @@ -393,7 +412,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange DockedDividerUtils.sanitizeStackBounds(bounds1, true /** topLeft */); DockedDividerUtils.sanitizeStackBounds(bounds2, false /** topLeft */); if (setEffectBounds) { - mSurfaceEffectPolicy.applyDividerPosition(position, isLandscape); + mSurfaceEffectPolicy.applyDividerPosition(position, mIsLeftRightSplit); } } @@ -563,13 +582,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds) { - final boolean isLandscape = isLandscape(rootBounds); final Rect insets = getDisplayStableInsets(context); // Make split axis insets value same as the larger one to avoid bounds1 and bounds2 // have difference for avoiding size-compat mode when switching unresizable apps in // landscape while they are letterboxed. - if (!isLandscape) { + if (!mIsLeftRightSplit) { final int largerInsets = Math.max(insets.top, insets.bottom); insets.set(insets.left, largerInsets, insets.right, largerInsets); } @@ -579,9 +597,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange rootBounds.width(), rootBounds.height(), mDividerSize, - !isLandscape, + !mIsLeftRightSplit, insets, - isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); + mIsLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); } /** Fling divider from current position to end or start position then exit */ @@ -643,13 +661,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange /** Switch both surface position with animation. */ public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, Consumer<Rect> finishCallback) { - final boolean isLandscape = isLandscape(); final Rect insets = getDisplayStableInsets(mContext); - insets.set(isLandscape ? insets.left : 0, isLandscape ? 0 : insets.top, - isLandscape ? insets.right : 0, isLandscape ? 0 : insets.bottom); + insets.set(mIsLeftRightSplit ? insets.left : 0, mIsLeftRightSplit ? 0 : insets.top, + mIsLeftRightSplit ? insets.right : 0, mIsLeftRightSplit ? 0 : insets.bottom); final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( - isLandscape ? mBounds2.width() : mBounds2.height()).position; + mIsLeftRightSplit ? mBounds2.width() : mBounds2.height()).position; final Rect distBounds1 = new Rect(); final Rect distBounds2 = new Rect(); final Rect distDividerBounds = new Rect(); @@ -740,15 +757,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange .toRect(); } - private static boolean isLandscape(Rect bounds) { - return bounds.width() > bounds.height(); - } - /** - * Return if this layout is landscape. + * @return {@code true} if we should create a left-right split, {@code false} if we should + * create a top-bottom split. */ - public boolean isLandscape() { - return isLandscape(mRootBounds); + public boolean isLeftRightSplit() { + return mIsLeftRightSplit; } /** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */ @@ -850,9 +864,13 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange /** Dumps the current split bounds recorded in this layout. */ public void dump(@NonNull PrintWriter pw, String prefix) { - pw.println(prefix + "bounds1=" + mBounds1.toShortString()); - pw.println(prefix + "dividerBounds=" + mDividerBounds.toShortString()); - pw.println(prefix + "bounds2=" + mBounds2.toShortString()); + final String innerPrefix = prefix + "\t"; + pw.println(prefix + TAG + ":"); + pw.println(innerPrefix + "mAllowLeftRightSplitInPortrait=" + mAllowLeftRightSplitInPortrait); + pw.println(innerPrefix + "mIsLeftRightSplit=" + mIsLeftRightSplit); + pw.println(innerPrefix + "bounds1=" + mBounds1.toShortString()); + pw.println(innerPrefix + "dividerBounds=" + mDividerBounds.toShortString()); + pw.println(innerPrefix + "bounds2=" + mBounds2.toShortString()); } /** Handles layout change event. */ @@ -937,32 +955,32 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * Applies a parallax to the task to hint dismissing progress. * * @param position the split position to apply dismissing parallax effect - * @param isLandscape indicates whether it's splitting horizontally or vertically + * @param isLeftRightSplit indicates whether it's splitting horizontally or vertically */ - void applyDividerPosition(int position, boolean isLandscape) { + void applyDividerPosition(int position, boolean isLeftRightSplit) { mDismissingSide = DOCKED_INVALID; mParallaxOffset.set(0, 0); mDismissingDimValue = 0; int totalDismissingDistance = 0; if (position < mDividerSnapAlgorithm.getFirstSplitTarget().position) { - mDismissingSide = isLandscape ? DOCKED_LEFT : DOCKED_TOP; + mDismissingSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; totalDismissingDistance = mDividerSnapAlgorithm.getDismissStartTarget().position - mDividerSnapAlgorithm.getFirstSplitTarget().position; } else if (position > mDividerSnapAlgorithm.getLastSplitTarget().position) { - mDismissingSide = isLandscape ? DOCKED_RIGHT : DOCKED_BOTTOM; + mDismissingSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; totalDismissingDistance = mDividerSnapAlgorithm.getLastSplitTarget().position - mDividerSnapAlgorithm.getDismissEndTarget().position; } - final boolean topLeftShrink = isLandscape + final boolean topLeftShrink = isLeftRightSplit ? position < mWinBounds1.right : position < mWinBounds1.bottom; if (topLeftShrink) { - mShrinkSide = isLandscape ? DOCKED_LEFT : DOCKED_TOP; + mShrinkSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; mContentBounds.set(mWinBounds1); mSurfaceBounds.set(mBounds1); } else { - mShrinkSide = isLandscape ? DOCKED_RIGHT : DOCKED_BOTTOM; + mShrinkSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; mContentBounds.set(mWinBounds2); mSurfaceBounds.set(mBounds2); } @@ -973,7 +991,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction); if (mParallaxType == PARALLAX_DISMISSING) { fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide); - if (isLandscape) { + if (isLeftRightSplit) { mParallaxOffset.x = (int) (fraction * totalDismissingDistance); } else { mParallaxOffset.y = (int) (fraction * totalDismissingDistance); @@ -982,7 +1000,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } if (mParallaxType == PARALLAX_ALIGN_CENTER) { - if (isLandscape) { + if (isLeftRightSplit) { mParallaxOffset.x = (mSurfaceBounds.width() - mContentBounds.width()) / 2; } else { @@ -1129,7 +1147,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // Calculate target bounds offset for IME mLastYOffset = mYOffsetForIme; final boolean needOffset = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT - && !isFloating && !isLandscape(mRootBounds) && mImeShown; + && !isFloating && !mIsLeftRightSplit && mImeShown; mTargetYOffset = needOffset ? getTargetYOffset() : 0; if (mTargetYOffset != mLastYOffset) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java index d7ea1c0c620d..0693543515b4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java @@ -16,6 +16,8 @@ package com.android.wm.shell.common.split; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; + import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES; import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -25,9 +27,14 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT import android.annotation.Nullable; import android.app.ActivityManager; import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Rect; import com.android.internal.util.ArrayUtils; +import com.android.wm.shell.Flags; import com.android.wm.shell.ShellTaskOrganizer; /** Helper utility class for split screen components to use. */ @@ -94,4 +101,38 @@ public class SplitScreenUtils { public static String splitFailureMessage(String caller, String reason) { return "(" + caller + ") Splitscreen aborted: " + reason; } + + /** + * Returns whether left/right split is allowed in portrait. + */ + public static boolean allowLeftRightSplitInPortrait(Resources res) { + return Flags.enableLeftRightSplitInPortrait() && res.getBoolean( + com.android.internal.R.bool.config_leftRightSplitInPortrait); + } + + /** + * Returns whether left/right split is supported in the given configuration. + */ + public static boolean isLeftRightSplit(boolean allowLeftRightSplitInPortrait, + Configuration config) { + // Compare the max bounds sizes as on near-square devices, the insets may result in a + // configuration in the other orientation + final boolean isLargeScreen = config.smallestScreenWidthDp >= 600; + final Rect maxBounds = config.windowConfiguration.getMaxBounds(); + final boolean isLandscape = maxBounds.width() >= maxBounds.height(); + return isLeftRightSplit(allowLeftRightSplitInPortrait, isLargeScreen, isLandscape); + } + + /** + * Returns whether left/right split is supported in the given configuration state. This method + * is useful for cases where we need to calculate this given last saved state. + */ + public static boolean isLeftRightSplit(boolean allowLeftRightSplitInPortrait, + boolean isLargeScreen, boolean isLandscape) { + if (allowLeftRightSplitInPortrait && isLargeScreen) { + return !isLandscape; + } else { + return isLandscape; + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index b158f88a68c3..3c6bc1754c5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -324,13 +324,16 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static SystemPerformanceHinter provideSystemPerformanceHinter(Context context, + static Optional<SystemPerformanceHinter> provideSystemPerformanceHinter(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, RootTaskDisplayAreaOrganizer rootTdaOrganizer) { + if (!com.android.window.flags.Flags.explicitRefreshRateHints()) { + return Optional.empty(); + } final PerfHintController perfHintController = new PerfHintController(context, shellInit, shellCommandHandler, rootTdaOrganizer); - return perfHintController.getHinter(); + return Optional.of(perfHintController.getHinter()); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index 0bf8ec32c6c0..fdfb6f3680b2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -94,6 +94,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll private ShellExecutor mMainExecutor; private ArrayList<DragAndDropListener> mListeners = new ArrayList<>(); + // Map of displayId -> per-display info private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>(); /** @@ -362,7 +363,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll */ private boolean isReadyToHandleDrag() { for (int i = 0; i < mDisplayDropTargets.size(); i++) { - if (mDisplayDropTargets.valueAt(i).mHasDrawn) { + if (mDisplayDropTargets.valueAt(i).hasDrawn) { return true; } } @@ -398,8 +399,13 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll * Dumps information about this controller. */ public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; pw.println(prefix + TAG); - pw.println(prefix + " listeners=" + mListeners.size()); + pw.println(innerPrefix + "listeners=" + mListeners.size()); + pw.println(innerPrefix + "Per display:"); + for (int i = 0; i < mDisplayDropTargets.size(); i++) { + mDisplayDropTargets.valueAt(i).dump(pw, innerPrefix); + } } /** @@ -440,7 +446,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll final FrameLayout rootView; final DragLayout dragLayout; // Tracks whether the window has fully drawn since it was last made visible - boolean mHasDrawn; + boolean hasDrawn; boolean isHandlingDrag; // A count of the number of active drags in progress to ensure that we only hide the window @@ -464,17 +470,29 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll rootView.setVisibility(visibility); if (visibility == View.VISIBLE) { rootView.requestApplyInsets(); - if (!mHasDrawn && rootView.getViewRootImpl() != null) { + if (!hasDrawn && rootView.getViewRootImpl() != null) { rootView.getViewRootImpl().registerRtFrameCallback(this); } } else { - mHasDrawn = false; + hasDrawn = false; } } @Override public void onFrameDraw(long frame) { - mHasDrawn = true; + hasDrawn = true; + } + + /** + * Dumps information about this display's shell drop target. + */ + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(innerPrefix + "displayId=" + displayId); + pw.println(innerPrefix + "hasDrawn=" + hasDrawn); + pw.println(innerPrefix + "isHandlingDrag=" + isHandlingDrag); + pw.println(innerPrefix + "activeDragCount=" + activeDragCount); + dragLayout.dump(pw, innerPrefix); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index e70768b6b752..162ce19703ea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -138,7 +138,7 @@ public class DragAndDropPolicy { final Rect displayRegion = new Rect(l, t, l + iw, t + ih); final Rect fullscreenDrawRegion = new Rect(displayRegion); final Rect fullscreenHitRegion = new Rect(displayRegion); - final boolean inLandscape = mSession.displayLayout.isLandscape(); + final boolean isLeftRightSplit = mSplitScreen != null && mSplitScreen.isLeftRightSplit(); final boolean inSplitScreen = mSplitScreen != null && mSplitScreen.isSplitScreenVisible(); final float dividerWidth = mContext.getResources().getDimensionPixelSize( R.dimen.split_divider_bar_width); @@ -155,7 +155,7 @@ public class DragAndDropPolicy { topOrLeftBounds.intersect(displayRegion); bottomOrRightBounds.intersect(displayRegion); - if (inLandscape) { + if (isLeftRightSplit) { final Rect leftHitRegion = new Rect(); final Rect rightHitRegion = new Rect(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java index 205a455200bd..445ba897c173 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java @@ -20,6 +20,7 @@ import static android.app.StatusBarManager.DISABLE_NONE; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS; import static android.content.pm.ActivityInfo.CONFIG_UI_MODE; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -47,14 +48,18 @@ import android.view.WindowInsets; import android.view.WindowInsets.Type; import android.widget.LinearLayout; +import androidx.annotation.NonNull; + import com.android.internal.logging.InstanceId; import com.android.internal.protolog.common.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; +import java.io.PrintWriter; import java.util.ArrayList; /** @@ -74,6 +79,11 @@ public class DragLayout extends LinearLayout { private final StatusBarManager mStatusBarManager; private final Configuration mLastConfiguration = new Configuration(); + // Whether this device supports left/right split in portrait + private final boolean mAllowLeftRightSplitInPortrait; + // Whether the device is currently in left/right split mode + private boolean mIsLeftRightSplit; + private DragAndDropPolicy.Target mCurrentTarget = null; private DropZoneView mDropZoneView1; private DropZoneView mDropZoneView2; @@ -106,17 +116,18 @@ public class DragLayout extends LinearLayout { setLayoutDirection(LAYOUT_DIRECTION_LTR); mDropZoneView1 = new DropZoneView(context); mDropZoneView2 = new DropZoneView(context); - addView(mDropZoneView1, new LinearLayout.LayoutParams(MATCH_PARENT, - MATCH_PARENT)); - addView(mDropZoneView2, new LinearLayout.LayoutParams(MATCH_PARENT, - MATCH_PARENT)); + addView(mDropZoneView1, new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + addView(mDropZoneView2, new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); ((LayoutParams) mDropZoneView1.getLayoutParams()).weight = 1; ((LayoutParams) mDropZoneView2.getLayoutParams()).weight = 1; - int orientation = getResources().getConfiguration().orientation; - setOrientation(orientation == Configuration.ORIENTATION_LANDSCAPE - ? LinearLayout.HORIZONTAL - : LinearLayout.VERTICAL); - updateContainerMargins(getResources().getConfiguration().orientation); + // We don't use the configuration orientation here to determine landscape because + // near-square devices may report the same orietation with insets taken into account + mAllowLeftRightSplitInPortrait = SplitScreenUtils.allowLeftRightSplitInPortrait( + context.getResources()); + mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, + getResources().getConfiguration()); + setOrientation(mIsLeftRightSplit ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); + updateContainerMargins(mIsLeftRightSplit); } @Override @@ -124,11 +135,12 @@ public class DragLayout extends LinearLayout { mInsets = insets.getInsets(Type.tappableElement() | Type.displayCutout()); recomputeDropTargets(); - final int orientation = getResources().getConfiguration().orientation; - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + boolean isLeftRightSplit = mSplitScreenController != null + && mSplitScreenController.isLeftRightSplit(); + if (isLeftRightSplit) { mDropZoneView1.setBottomInset(mInsets.bottom); mDropZoneView2.setBottomInset(mInsets.bottom); - } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { + } else { mDropZoneView1.setBottomInset(0); mDropZoneView2.setBottomInset(mInsets.bottom); } @@ -136,14 +148,12 @@ public class DragLayout extends LinearLayout { } public void onConfigChanged(Configuration newConfig) { - if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE - && getOrientation() != HORIZONTAL) { - setOrientation(LinearLayout.HORIZONTAL); - updateContainerMargins(newConfig.orientation); - } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT - && getOrientation() != VERTICAL) { - setOrientation(LinearLayout.VERTICAL); - updateContainerMargins(newConfig.orientation); + boolean isLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait, + newConfig); + if (isLeftRightSplit != mIsLeftRightSplit) { + mIsLeftRightSplit = isLeftRightSplit; + setOrientation(mIsLeftRightSplit ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); + updateContainerMargins(mIsLeftRightSplit); } final int diff = newConfig.diff(mLastConfiguration); @@ -162,14 +172,14 @@ public class DragLayout extends LinearLayout { mDropZoneView2.setContainerMargin(0, 0, 0, 0); } - private void updateContainerMargins(int orientation) { + private void updateContainerMargins(boolean isLeftRightSplit) { final float halfMargin = mDisplayMargin / 2f; - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + if (isLeftRightSplit) { mDropZoneView1.setContainerMargin( mDisplayMargin, mDisplayMargin, halfMargin, mDisplayMargin); mDropZoneView2.setContainerMargin( halfMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin); - } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { + } else { mDropZoneView1.setContainerMargin( mDisplayMargin, mDisplayMargin, mDisplayMargin, halfMargin); mDropZoneView2.setContainerMargin( @@ -257,23 +267,21 @@ public class DragLayout extends LinearLayout { * @param bounds2 bounds to apply to the second dropzone view, null if split in half. */ private void updateDropZoneSizes(Rect bounds1, Rect bounds2) { - final int orientation = getResources().getConfiguration().orientation; - final boolean isPortrait = orientation == Configuration.ORIENTATION_PORTRAIT; final int halfDivider = mDividerSize / 2; final LinearLayout.LayoutParams dropZoneView1 = (LayoutParams) mDropZoneView1.getLayoutParams(); final LinearLayout.LayoutParams dropZoneView2 = (LayoutParams) mDropZoneView2.getLayoutParams(); - if (isPortrait) { - dropZoneView1.width = MATCH_PARENT; - dropZoneView2.width = MATCH_PARENT; - dropZoneView1.height = bounds1 != null ? bounds1.height() + halfDivider : MATCH_PARENT; - dropZoneView2.height = bounds2 != null ? bounds2.height() + halfDivider : MATCH_PARENT; - } else { + if (mIsLeftRightSplit) { dropZoneView1.width = bounds1 != null ? bounds1.width() + halfDivider : MATCH_PARENT; dropZoneView2.width = bounds2 != null ? bounds2.width() + halfDivider : MATCH_PARENT; dropZoneView1.height = MATCH_PARENT; dropZoneView2.height = MATCH_PARENT; + } else { + dropZoneView1.width = MATCH_PARENT; + dropZoneView2.width = MATCH_PARENT; + dropZoneView1.height = bounds1 != null ? bounds1.height() + halfDivider : MATCH_PARENT; + dropZoneView2.height = bounds2 != null ? bounds2.height() + halfDivider : MATCH_PARENT; } dropZoneView1.weight = bounds1 != null ? 0 : 1; dropZoneView2.weight = bounds2 != null ? 0 : 1; @@ -371,7 +379,7 @@ public class DragLayout extends LinearLayout { // Reset the state if we previously force-ignore the bottom margin mDropZoneView1.setForceIgnoreBottomMargin(false); mDropZoneView2.setForceIgnoreBottomMargin(false); - updateContainerMargins(getResources().getConfiguration().orientation); + updateContainerMargins(mIsLeftRightSplit); mCurrentTarget = null; } @@ -481,4 +489,19 @@ public class DragLayout extends LinearLayout { final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb(); } + + /** + * Dumps information about this drag layout. + */ + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + "DragLayout:"); + pw.println(innerPrefix + "mIsLeftRightSplitInPortrait=" + mAllowLeftRightSplitInPortrait); + pw.println(innerPrefix + "mIsLeftRightSplit=" + mIsLeftRightSplit); + pw.println(innerPrefix + "mDisplayMargin=" + mDisplayMargin); + pw.println(innerPrefix + "mDividerSize=" + mDividerSize); + pw.println(innerPrefix + "mIsShowing=" + mIsShowing); + pw.println(innerPrefix + "mHasDropped=" + mHasDropped); + pw.println(innerPrefix + "mCurrentTarget=" + mCurrentTarget); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java index 478b6a9d95f6..353d702e5bc4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java @@ -18,31 +18,17 @@ package com.android.wm.shell.draganddrop; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static android.content.ClipDescription.EXTRA_PENDING_INTENT; -import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY; -import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; -import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; -import static android.content.Intent.EXTRA_USER; import android.app.ActivityManager; import android.app.ActivityTaskManager; -import android.app.PendingIntent; import android.app.WindowConfiguration; import android.content.ClipData; -import android.content.ClipDescription; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; -import android.net.Uri; -import android.os.UserHandle; - -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; import com.android.wm.shell.common.DisplayLayout; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.List; /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index b4067d0db112..c1164fca22f2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -123,7 +123,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private static final int EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS = SystemProperties.getInt( - "persist.wm.debug.extra_content_overlay_fade_out_delay_ms", 0); + "persist.wm.debug.extra_content_overlay_fade_out_delay_ms", 400); private final Context mContext; private final SyncTransactionQueue mSyncTransactionQueue; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 271a3b26305d..79c20761abed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -590,7 +590,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { cancel("transit_sleep"); return; } - if (mKeyguardLocked) { + if (mKeyguardLocked || (info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: keyguard is locked", mInstanceId); // We will not accept new changes if we are swiping over the keyguard. @@ -627,7 +627,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { && mRecentsTask.equals(change.getContainer()); hasTaskChange = hasTaskChange || isRootTask; final boolean isLeafTask = leafTaskFilter.test(change); - if (TransitionUtil.isOpeningType(change.getMode())) { + if (TransitionUtil.isOpeningType(change.getMode()) + || TransitionUtil.isOrderOnly(change)) { if (isRecentsTask) { recentsOpening = change; } else if (isRootTask || isLeafTask) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 664d44910e72..37b24e505ade 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -366,6 +366,14 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return mStageCoordinator.getStageOfTask(taskId); } + /** + * @return {@code true} if we should create a left-right split, {@code false} if we should + * create a top-bottom split. + */ + public boolean isLeftRightSplit() { + return mStageCoordinator.isLeftRightSplit(); + } + /** Check split is foreground and task is under split or not by taskId. */ public boolean isTaskInSplitScreenForeground(int taskId) { return isTaskInSplitScreen(taskId) && isSplitScreenVisible(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 7a4834cb5adb..be685b57f779 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -1301,7 +1301,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Switch split position: %s", reason); mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); + mSplitLayout.isLeftRightSplit()); } void setSideStagePosition(@SplitPosition int sideStagePosition, @@ -1659,7 +1659,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), getMainStagePosition(), mMainStage.getTopChildTaskUid(), getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); + mSplitLayout.isLeftRightSplit()); } } @@ -1749,10 +1749,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } if (stage == STAGE_TYPE_MAIN) { mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); + mSplitLayout.isLeftRightSplit()); } else { mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); + mSplitLayout.isLeftRightSplit()); } if (present) { updateRecentTasksSplitPair(); @@ -2113,7 +2113,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), getMainStagePosition(), mMainStage.getTopChildTaskUid(), getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); + mSplitLayout.isLeftRightSplit()); } } } @@ -2205,8 +2205,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); } - private boolean isLandscape() { - return mSplitLayout.isLandscape(); + /** + * @return {@code true} if we should create a left-right split, {@code false} if we should + * create a top-bottom split. + */ + boolean isLeftRightSplit() { + return mSplitLayout != null && mSplitLayout.isLeftRightSplit(); } /** @@ -3177,6 +3181,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible); pw.println(innerPrefix + "isSplitActive=" + isSplitActive()); pw.println(innerPrefix + "isSplitVisible=" + isSplitScreenVisible()); + pw.println(innerPrefix + "isLeftRightSplit=" + mSplitLayout.isLeftRightSplit()); pw.println(innerPrefix + "MainStage"); pw.println(childPrefix + "stagePosition=" + splitPositionToString(getMainStagePosition())); pw.println(childPrefix + "isActive=" + mMainStage.isActive()); @@ -3188,10 +3193,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.dump(pw, childPrefix); pw.println(innerPrefix + "SideStageListener"); mSideStageListener.dump(pw, childPrefix); - if (mMainStage.isActive()) { - pw.println(innerPrefix + "SplitLayout"); - mSplitLayout.dump(pw, childPrefix); - } + mSplitLayout.dump(pw, childPrefix); if (!mPausingTasks.isEmpty()) { pw.println(childPrefix + "mPausingTasks=" + mPausingTasks); } @@ -3243,7 +3245,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLogger.logExit(exitReason, SPLIT_POSITION_UNDEFINED, 0 /* mainStageUid */, SPLIT_POSITION_UNDEFINED, 0 /* sideStageUid */, - mSplitLayout.isLandscape()); + mSplitLayout.isLeftRightSplit()); } /** @@ -3256,7 +3258,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */, !toMainStage ? getSideStagePosition() : SPLIT_POSITION_UNDEFINED, !toMainStage ? mSideStage.getTopChildTaskUid() : 0 /* sideStageUid */, - mSplitLayout.isLandscape()); + mSplitLayout.isLeftRightSplit()); } class StageListenerImpl implements StageTaskListener.StageListenerCallbacks { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index 8cbcde320795..53ec20192f2b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -49,7 +49,6 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManagerGlobal; -import com.android.internal.view.BaseIWindow; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; @@ -70,7 +69,9 @@ class DragResizeInputListener implements AutoCloseable { private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; private final int mDisplayId; - private final BaseIWindow mFakeWindow; + + private final IBinder mClientToken; + private final IBinder mFocusGrantToken; private final SurfaceControl mDecorationSurface; private final InputChannel mInputChannel; @@ -78,7 +79,7 @@ class DragResizeInputListener implements AutoCloseable { private final DragPositioningCallback mCallback; private final SurfaceControl mInputSinkSurface; - private final BaseIWindow mFakeSinkWindow; + private final IBinder mSinkClientToken; private final InputChannel mSinkInputChannel; private final DisplayController mDisplayController; @@ -116,17 +117,14 @@ class DragResizeInputListener implements AutoCloseable { mTaskCornerRadius = taskCornerRadius; mDecorationSurface = decorationSurface; mDisplayController = displayController; - // Use a fake window as the backing surface is a container layer, and we don't want to - // create a buffer layer for it, so we can't use ViewRootImpl. - mFakeWindow = new BaseIWindow(); - mFakeWindow.setSession(mWindowSession); + mClientToken = new Binder(); mFocusGrantToken = new Binder(); mInputChannel = new InputChannel(); try { mWindowSession.grantInputChannel( mDisplayId, mDecorationSurface, - mFakeWindow.asBinder(), + mClientToken, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, PRIVATE_FLAG_TRUSTED_OVERLAY, @@ -155,13 +153,13 @@ class DragResizeInputListener implements AutoCloseable { .setLayer(mInputSinkSurface, WindowDecoration.INPUT_SINK_Z_ORDER) .show(mInputSinkSurface) .apply(); - mFakeSinkWindow = new BaseIWindow(); + mSinkClientToken = new Binder(); mSinkInputChannel = new InputChannel(); try { mWindowSession.grantInputChannel( mDisplayId, mInputSinkSurface, - mFakeSinkWindow.asBinder(), + mSinkClientToken, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, 0 /* privateFlags */, @@ -324,14 +322,14 @@ class DragResizeInputListener implements AutoCloseable { mInputEventReceiver.dispose(); mInputChannel.dispose(); try { - mWindowSession.remove(mFakeWindow.asBinder()); + mWindowSession.remove(mClientToken); } catch (RemoteException e) { e.rethrowFromSystemServer(); } mSinkInputChannel.dispose(); try { - mWindowSession.remove(mFakeSinkWindow.asBinder()); + mWindowSession.remove(mSinkClientToken); } catch (RemoteException e) { e.rethrowFromSystemServer(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java index bf11c8bc4f79..3a1ea0e201b2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java @@ -87,7 +87,7 @@ class FluidResizeTaskPositioner implements DragPositioningCallback { mDisplayController.getDisplayLayout(mWindowDecoration.mDisplay.getDisplayId()) .getStableBounds(mStableBounds); } - return mRepositionTaskBounds; + return new Rect(mRepositionTaskBounds); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index 79fec0978a12..4b55a0caaca5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -107,7 +107,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, mDisplayController.getDisplayLayout(mDesktopWindowDecoration.mDisplay.getDisplayId()) .getStableBounds(mStableBounds); } - return mRepositionTaskBounds; + return new Rect(mRepositionTaskBounds); } @Override diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp index 366f7b1e678f..4abaf5bd4a38 100644 --- a/libs/WindowManager/Shell/tests/flicker/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/Android.bp @@ -52,7 +52,7 @@ java_library { } java_defaults { - name: "WMShellFlickerTestsDefaultWithoutTemplate", + name: "WMShellFlickerTestsDefault", platform_apis: true, certificate: "platform", optimize: { @@ -75,16 +75,9 @@ java_defaults { ], data: [ ":FlickerTestApp", - "trace_config/*", ], } -java_defaults { - name: "WMShellFlickerTestsDefault", - defaults: ["WMShellFlickerTestsDefaultWithoutTemplate"], - test_config_template: "AndroidTestTemplate.xml", -} - java_library { name: "WMShellFlickerTestsBase", defaults: ["WMShellFlickerTestsDefault"], diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml deleted file mode 100644 index b00d88e61e13..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml +++ /dev/null @@ -1,112 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2023 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<configuration description="Runs WindowManager Shell Flicker Tests {MODULE}"> - <option name="test-tag" value="FlickerTests"/> - <!-- Needed for storing the perfetto trace files in the sdcard/test_results--> - <option name="isolated-storage" value="false"/> - - <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> - <!-- keeps the screen on during tests --> - <option name="screen-always-on" value="on"/> - <!-- prevents the phone from restarting --> - <option name="force-skip-system-props" value="true"/> - <!-- set WM tracing verbose level to all --> - <option name="run-command" value="cmd window tracing level all"/> - <!-- set WM tracing to frame (avoid incomplete states) --> - <option name="run-command" value="cmd window tracing frame"/> - <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests --> - <option name="run-command" value="pm disable com.google.android.internal.betterbug"/> - <!-- ensure lock screen mode is swipe --> - <option name="run-command" value="locksettings set-disabled false"/> - <!-- restart launcher to activate TAPL --> - <option name="run-command" - value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher"/> - <!-- Increase trace size: 20mb for WM and 80mb for SF --> - <option name="run-command" value="cmd window tracing size 20480"/> - <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920"/> - <!-- b/307664397 - Ensure camera has the correct permissions and doesn't show a dialog --> - <option name="run-command" - value="pm grant com.google.android.GoogleCamera android.permission.ACCESS_FINE_LOCATION"/> - </target_preparer> - <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> - <option name="test-user-token" value="%TEST_USER%"/> - <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> - <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> - <option name="run-command" value="settings put system show_touches 1"/> - <option name="run-command" value="settings put system pointer_location 1"/> - <option name="teardown-command" - value="settings delete secure show_ime_with_hard_keyboard"/> - <option name="teardown-command" value="settings delete system show_touches"/> - <option name="teardown-command" value="settings delete system pointer_location"/> - <option name="teardown-command" - value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> - </target_preparer> - <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> - <option name="cleanup-apks" value="true"/> - <option name="test-file-name" value="{MODULE}.apk"/> - <option name="test-file-name" value="FlickerTestApp.apk"/> - </target_preparer> - <!-- Enable mocking GPS location by the test app --> - <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> - <option name="run-command" - value="appops set com.android.wm.shell.flicker.pip.apps android:mock_location allow"/> - <option name="teardown-command" - value="appops set com.android.wm.shell.flicker.pip.apps android:mock_location deny"/> - </target_preparer> - - <!-- Needed for pushing the trace config file --> - <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> - <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> - <option name="push-file" - key="trace_config.textproto" - value="/data/misc/perfetto-traces/trace_config.textproto" - /> - <!--Install the content provider automatically when we push some file in sdcard folder.--> - <!--Needed to avoid the installation during the test suite.--> - <option name="push-file" key="trace_config.textproto" value="/sdcard/sample.textproto"/> - </target_preparer> - <test class="com.android.tradefed.testtype.AndroidJUnitTest"> - <option name="package" value="{PACKAGE}"/> - <option name="shell-timeout" value="6600s"/> - <option name="test-timeout" value="6000s"/> - <option name="hidden-api-checks" value="false"/> - <option name="device-listeners" value="android.device.collectors.PerfettoListener"/> - <!-- PerfettoListener related arguments --> - <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true"/> - <option name="instrumentation-arg" - key="perfetto_config_file" - value="trace_config.textproto" - /> - <option name="instrumentation-arg" key="per_run" value="true"/> - </test> - <!-- Needed for pulling the collected trace config on to the host --> - <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> - <option name="pull-pattern-keys" value="perfetto_file_path"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.bubbles/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.pip/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.splitscreen/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.service/files"/> - <option name="collect-on-run-ended-only" value="true"/> - <option name="clean-up" value="true"/> - </metrics_collector> -</configuration> diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp b/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp index bae701f2cbeb..e151ab2c5878 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp @@ -36,6 +36,8 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker", instrumentation_target_package: "com.android.wm.shell.flicker", + test_config_template: "AndroidTestTemplate.xml", srcs: [":WMShellFlickerTestsAppCompat-src"], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/Android.bp b/libs/WindowManager/Shell/tests/flicker/bubble/Android.bp index c4e9a8479563..f0b4f1faad46 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/bubble/Android.bp @@ -29,6 +29,8 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.bubbles", instrumentation_target_package: "com.android.wm.shell.flicker.bubbles", + test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*.kt"], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp index 386983ce6aae..e61f7629f4fd 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp @@ -62,11 +62,13 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.pip", instrumentation_target_package: "com.android.wm.shell.flicker.pip", + test_config_template: "AndroidTestTemplate.xml", srcs: [ ":WMShellFlickerTestsPip1-src", ":WMShellFlickerTestsPipCommon-src", ], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } android_test { @@ -75,11 +77,13 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.pip", instrumentation_target_package: "com.android.wm.shell.flicker.pip", + test_config_template: "AndroidTestTemplate.xml", srcs: [ ":WMShellFlickerTestsPip2-src", ":WMShellFlickerTestsPipCommon-src", ], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } android_test { @@ -88,6 +92,7 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.pip", instrumentation_target_package: "com.android.wm.shell.flicker.pip", + test_config_template: "AndroidTestTemplate.xml", srcs: [ ":WMShellFlickerTestsPip3-src", ":WMShellFlickerTestsPipCommon-src", @@ -98,6 +103,7 @@ android_test { ":WMShellFlickerTestsPipApps-src", ], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } android_test { @@ -106,19 +112,22 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.pip.apps", instrumentation_target_package: "com.android.wm.shell.flicker.pip.apps", + test_config_template: "AndroidTestTemplate.xml", srcs: [ ":WMShellFlickerTestsPipApps-src", ":WMShellFlickerTestsPipCommon-src", ], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } android_test { name: "WMShellFlickerTestsPipAppsCSuite", - defaults: ["WMShellFlickerTestsDefaultWithoutTemplate"], + defaults: ["WMShellFlickerTestsDefault"], additional_manifests: ["AndroidManifest.xml"], package_name: "com.android.wm.shell.flicker.pip.apps", instrumentation_target_package: "com.android.wm.shell.flicker.pip.apps", + test_config_template: "AndroidTestTemplate.xml", srcs: [ ":WMShellFlickerTestsPipApps-src", ":WMShellFlickerTestsPipCommon-src", @@ -128,9 +137,11 @@ android_test { "device-tests", "csuite", ], + data: ["trace_config/*"], } csuite_test { name: "csuite-1p3p-pip-flickers", + test_plan_include: "csuitePlan.xml", test_config_template: "csuiteDefaultTemplate.xml", } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml index 6429b00a2a58..f5a8655b81f0 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml @@ -45,12 +45,15 @@ <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> + <option name="run-command" value="settings put global package_verifier_user_consent -1"/> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard"/> <option name="teardown-command" value="settings delete system show_touches"/> <option name="teardown-command" value="settings delete system pointer_location"/> <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> + <option name="teardown-command" + value="settings put global package_verifier_user_consent 1"/> </target_preparer> <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="cleanup-apks" value="true"/> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuitePlan.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuitePlan.xml new file mode 100644 index 000000000000..a2fc6b45c2ad --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/pip/csuitePlan.xml @@ -0,0 +1,3 @@ +<configuration description="Flicker tests C-Suite Crawler Test Plan"> + <target_preparer class="com.android.csuite.core.AppCrawlTesterHostPreparer"/> +</configuration>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/service/Android.bp b/libs/WindowManager/Shell/tests/flicker/service/Android.bp index 9b8cd94d56b2..4f1a68a1a74e 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/service/Android.bp @@ -52,8 +52,10 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.service", instrumentation_target_package: "com.android.wm.shell.flicker.service", + test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*.kt"], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } android_test { @@ -62,6 +64,8 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.service", instrumentation_target_package: "com.android.wm.shell.flicker.service", + test_config_template: "AndroidTestTemplate.xml", srcs: [":WMShellFlickerServicePlatinumTests-src"], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt index 80ab24ddf9ef..824e45403d2a 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -27,6 +28,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -64,4 +66,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt index cc982d1ba860..c52ada3b312f 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.flicker.rules.ChangeDisplayOrientationRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry @@ -29,6 +30,7 @@ import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -75,4 +77,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { secondaryApp.exit(wmHelper) sendNotificationApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt index fa12bb869467..8134fddd40e5 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.flicker.rules.ChangeDisplayOrientationRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry @@ -29,6 +30,7 @@ import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -82,4 +84,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt index 2592fd40d902..3417744f13a5 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -28,6 +29,7 @@ import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -70,4 +72,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt index 983653b9b5ca..f1a011c0d191 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -27,6 +28,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -70,4 +72,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt index 068171d2e129..c9b1c916ff4b 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt @@ -20,6 +20,7 @@ import android.app.Instrumentation import android.graphics.Point import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.helpers.WindowUtils import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry @@ -29,6 +30,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -149,4 +151,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { val LARGE_SCREEN_DP_THRESHOLD = 600 return sizeDp.x >= LARGE_SCREEN_DP_THRESHOLD && sizeDp.y >= LARGE_SCREEN_DP_THRESHOLD } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt index 64b75c5fd967..72f2db3380dd 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -27,6 +28,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -67,4 +69,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt index 179501089168..511de4fd8b90 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -27,6 +28,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -66,4 +68,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt index 7065846dc653..558d2bf1f349 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -27,6 +28,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -68,4 +70,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt index 251cb50de017..ecd68295df9a 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -27,6 +28,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -69,4 +71,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { thirdApp.exit(wmHelper) fourthApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt index a9933bbe09fc..f50d5c7df8d7 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios import android.app.Instrumentation import android.tools.common.NavBar import android.tools.common.Rotation +import android.tools.device.AndroidLoggerSetupRule import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -27,6 +28,7 @@ import com.android.wm.shell.flicker.service.common.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before +import org.junit.ClassRule import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -66,4 +68,8 @@ abstract class UnlockKeyguardToSplitScreen { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) } + + companion object { + @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule() + } } diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp index 4629c5318366..f813b0d3b0b7 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp @@ -54,11 +54,13 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.wm.shell.flicker.splitscreen", instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", + test_config_template: "AndroidTestTemplate.xml", srcs: [ ":WMShellFlickerTestsSplitScreenBase-src", ":WMShellFlickerTestsSplitScreenGroup1-src", ], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } android_test { @@ -74,4 +76,5 @@ android_test { ":WMShellFlickerTestsSplitScreenGroup1-src", ], static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], } diff --git a/libs/WindowManager/Shell/tests/flicker/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/trace_config/trace_config.textproto deleted file mode 100644 index 406ada97a07d..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/trace_config/trace_config.textproto +++ /dev/null @@ -1,75 +0,0 @@ -# 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. - -# proto-message: TraceConfig - -# Enable periodic flushing of the trace buffer into the output file. -write_into_file: true - -# Writes the userspace buffer into the file every 1s. -file_write_period_ms: 2500 - -# See b/126487238 - we need to guarantee ordering of events. -flush_period_ms: 30000 - -# The trace buffers needs to be big enough to hold |file_write_period_ms| of -# trace data. The trace buffer sizing depends on the number of trace categories -# enabled and the device activity. - -# RSS events -buffers: { - size_kb: 63488 - fill_policy: RING_BUFFER -} - -data_sources { - config { - name: "linux.process_stats" - target_buffer: 0 - # polled per-process memory counters and process/thread names. - # If you don't want the polled counters, remove the "process_stats_config" - # section, but keep the data source itself as it still provides on-demand - # thread/process naming for ftrace data below. - process_stats_config { - scan_all_processes_on_start: true - } - } -} - -data_sources: { - config { - name: "linux.ftrace" - ftrace_config { - ftrace_events: "ftrace/print" - ftrace_events: "task/task_newtask" - ftrace_events: "task/task_rename" - atrace_categories: "ss" - atrace_categories: "wm" - atrace_categories: "am" - atrace_categories: "aidl" - atrace_categories: "input" - atrace_categories: "binder_driver" - atrace_categories: "sched_process_exit" - atrace_apps: "com.android.server.wm.flicker.testapp" - atrace_apps: "com.android.systemui" - atrace_apps: "com.android.wm.shell.flicker" - atrace_apps: "com.android.wm.shell.flicker.other" - atrace_apps: "com.android.wm.shell.flicker.bubbles" - atrace_apps: "com.android.wm.shell.flicker.pip" - atrace_apps: "com.android.wm.shell.flicker.splitscreen" - atrace_apps: "com.google.android.apps.nexuslauncher" - } - } -} - diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java index 835ebe2206ad..e5ae6e515566 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java @@ -16,10 +16,15 @@ package com.android.wm.shell.bubbles; +import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT; + import static com.google.common.truth.Truth.assertThat; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.mockito.Mockito.mock; + import android.content.Intent; +import android.content.pm.ShortcutInfo; import android.graphics.Insets; import android.graphics.PointF; import android.graphics.Rect; @@ -226,7 +231,7 @@ public class BubblePositionerTest extends ShellTestCase { } @Test - public void testExpandedViewHeight_onLargeTablet() { + public void testGetExpandedViewHeight_max() { Insets insets = Insets.of(10, 20, 5, 15); Rect screenBounds = new Rect(0, 0, 1800, 2600); @@ -240,10 +245,91 @@ public class BubblePositionerTest extends ShellTestCase { Intent intent = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName()); Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1), null, directExecutor()); + assertThat(mPositioner.getExpandedViewHeight(bubble)).isEqualTo(MAX_HEIGHT); + } + + @Test + public void testGetExpandedViewHeight_customHeight_valid() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setLargeScreen() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + + final int minHeight = mContext.getResources().getDimensionPixelSize( + R.dimen.bubble_expanded_default_height); + Bubble bubble = new Bubble("key", + mock(ShortcutInfo.class), + minHeight + 100 /* desiredHeight */, + 0 /* desiredHeightResId */, + "title", + 0 /* taskId */, + null /* locus */, + true /* isDismissable */, + directExecutor(), + mock(Bubbles.BubbleMetadataFlagListener.class)); + + // Ensure the height is the same as the desired value + assertThat(mPositioner.getExpandedViewHeight(bubble)).isEqualTo( + bubble.getDesiredHeight(mContext)); + } + + + @Test + public void testGetExpandedViewHeight_customHeight_tooSmall() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setLargeScreen() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + + Bubble bubble = new Bubble("key", + mock(ShortcutInfo.class), + 10 /* desiredHeight */, + 0 /* desiredHeightResId */, + "title", + 0 /* taskId */, + null /* locus */, + true /* isDismissable */, + directExecutor(), + mock(Bubbles.BubbleMetadataFlagListener.class)); + + // Ensure the height is the same as the minimum value + final int minHeight = mContext.getResources().getDimensionPixelSize( + R.dimen.bubble_expanded_default_height); + assertThat(mPositioner.getExpandedViewHeight(bubble)).isEqualTo(minHeight); + } + + @Test + public void testGetMaxExpandedViewHeight_onLargeTablet() { + Insets insets = Insets.of(10, 20, 5, 15); + Rect screenBounds = new Rect(0, 0, 1800, 2600); + + DeviceConfig deviceConfig = new ConfigBuilder() + .setLargeScreen() + .setInsets(insets) + .setScreenBounds(screenBounds) + .build(); + mPositioner.update(deviceConfig); + int manageButtonHeight = mContext.getResources().getDimensionPixelSize(R.dimen.bubble_manage_button_height); - float expectedHeight = 1800 - 2 * 20 - manageButtonHeight; - assertThat(mPositioner.getExpandedViewHeight(bubble)).isWithin(0.1f).of(expectedHeight); + int pointerWidth = mContext.getResources().getDimensionPixelSize( + R.dimen.bubble_pointer_width); + int expandedViewPadding = mContext.getResources().getDimensionPixelSize(R + .dimen.bubble_expanded_view_padding); + float expectedHeight = 1800 - 2 * 20 - manageButtonHeight - pointerWidth + - expandedViewPadding * 2; + assertThat(((float) mPositioner.getMaxExpandedViewHeight(false /* isOverflow */))) + .isWithin(0.1f).of(expectedHeight); } /** diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java index 527dc0149716..1b347e01888e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java @@ -204,6 +204,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { @Test public void testDragAppOverFullscreenHome_expectOnlyFullscreenTarget() { + doReturn(true).when(mSplitScreenStarter).isLeftRightSplit(); setRunningTask(mHomeTask); DragSession dragSession = new DragSession(mContext, mActivityTaskManager, mLandscapeDisplayLayout, mActivityClipData); @@ -219,6 +220,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { @Test public void testDragAppOverFullscreenApp_expectSplitScreenTargets() { + doReturn(true).when(mSplitScreenStarter).isLeftRightSplit(); setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mContext, mActivityTaskManager, mLandscapeDisplayLayout, mActivityClipData); @@ -239,6 +241,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { @Test public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenTargets() { + doReturn(false).when(mSplitScreenStarter).isLeftRightSplit(); setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mContext, mActivityTaskManager, mPortraitDisplayLayout, mActivityClipData); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenUtilsTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenUtilsTests.java new file mode 100644 index 000000000000..30847d3ac192 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenUtilsTests.java @@ -0,0 +1,75 @@ +/* + * 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.wm.shell.splitscreen; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.res.Configuration; +import android.graphics.Rect; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.split.SplitScreenUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; + + +/** Tests for {@link com.android.wm.shell.common.split.SplitScreenUtils} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SplitScreenUtilsTests extends ShellTestCase { + + @Test + public void testIsLeftRightSplit() { + Configuration portraitTablet = new Configuration(); + portraitTablet.smallestScreenWidthDp = 720; + portraitTablet.windowConfiguration.setMaxBounds(new Rect(0, 0, 500, 1000)); + Configuration landscapeTablet = new Configuration(); + landscapeTablet.smallestScreenWidthDp = 720; + landscapeTablet.windowConfiguration.setMaxBounds(new Rect(0, 0, 1000, 500)); + Configuration portraitPhone = new Configuration(); + portraitPhone.smallestScreenWidthDp = 420; + portraitPhone.windowConfiguration.setMaxBounds(new Rect(0, 0, 500, 1000)); + Configuration landscapePhone = new Configuration(); + landscapePhone.smallestScreenWidthDp = 420; + landscapePhone.windowConfiguration.setMaxBounds(new Rect(0, 0, 1000, 500)); + + // Allow L/R split in portrait = false + assertTrue(SplitScreenUtils.isLeftRightSplit(false /* allowLeftRightSplitInPortrait */, + landscapeTablet)); + assertTrue(SplitScreenUtils.isLeftRightSplit(false /* allowLeftRightSplitInPortrait */, + landscapePhone)); + assertFalse(SplitScreenUtils.isLeftRightSplit(false /* allowLeftRightSplitInPortrait */, + portraitTablet)); + assertFalse(SplitScreenUtils.isLeftRightSplit(false /* allowLeftRightSplitInPortrait */, + portraitPhone)); + + // Allow L/R split in portrait = true, only affects large screens + assertFalse(SplitScreenUtils.isLeftRightSplit(true /* allowLeftRightSplitInPortrait */, + landscapeTablet)); + assertTrue(SplitScreenUtils.isLeftRightSplit(true /* allowLeftRightSplitInPortrait */, + landscapePhone)); + assertTrue(SplitScreenUtils.isLeftRightSplit(true /* allowLeftRightSplitInPortrait */, + portraitTablet)); + assertFalse(SplitScreenUtils.isLeftRightSplit(true /* allowLeftRightSplitInPortrait */, + portraitPhone)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index fff65f364121..d819261ecba2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -140,7 +140,7 @@ public class StageCoordinatorTests extends ShellTestCase { when(mSplitLayout.getBounds1()).thenReturn(mBounds1); when(mSplitLayout.getBounds2()).thenReturn(mBounds2); when(mSplitLayout.getRootBounds()).thenReturn(mRootBounds); - when(mSplitLayout.isLandscape()).thenReturn(false); + when(mSplitLayout.isLeftRightSplit()).thenReturn(false); when(mSplitLayout.applyTaskChanges(any(), any(), any())).thenReturn(true); when(mSplitLayout.getDividerLeash()).thenReturn(mDividerLeash); diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index 47a7f3579764..2f28363aedc7 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -63,15 +63,21 @@ cc_library { "AssetsProvider.cpp", "AttributeResolution.cpp", "BigBuffer.cpp", + "BigBufferStream.cpp", "ChunkIterator.cpp", "ConfigDescription.cpp", + "FileStream.cpp", "Idmap.cpp", "LoadedArsc.cpp", "Locale.cpp", "LocaleData.cpp", "misc.cpp", + "NinePatch.cpp", "ObbFile.cpp", "PosixUtils.cpp", + "Png.cpp", + "PngChunkFilter.cpp", + "PngCrunch.cpp", "ResourceTimer.cpp", "ResourceTypes.cpp", "ResourceUtils.cpp", @@ -84,7 +90,10 @@ cc_library { ], export_include_dirs: ["include"], export_shared_lib_headers: ["libz"], - static_libs: ["libincfs-utils"], + static_libs: [ + "libincfs-utils", + "libpng", + ], whole_static_libs: [ "libandroidfw_pathutils", "libincfs-utils", @@ -198,9 +207,11 @@ cc_test { "tests/ConfigDescription_test.cpp", "tests/ConfigLocale_test.cpp", "tests/DynamicRefTable_test.cpp", + "tests/FileStream_test.cpp", "tests/Idmap_test.cpp", "tests/LoadedArsc_test.cpp", "tests/Locale_test.cpp", + "tests/NinePatch_test.cpp", "tests/ResourceTimer_test.cpp", "tests/ResourceUtils_test.cpp", "tests/ResTable_test.cpp", diff --git a/libs/androidfw/BigBufferStream.cpp b/libs/androidfw/BigBufferStream.cpp new file mode 100644 index 000000000000..f18199cfa52b --- /dev/null +++ b/libs/androidfw/BigBufferStream.cpp @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2017 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. + */ + +#include "androidfw/BigBufferStream.h" + +#include <algorithm> + +namespace android { + +// +// BigBufferInputStream +// + +bool BigBufferInputStream::Next(const void** data, size_t* size) { + if (iter_ == buffer_->end()) { + return false; + } + + if (offset_ == iter_->size) { + ++iter_; + if (iter_ == buffer_->end()) { + return false; + } + offset_ = 0; + } + + *data = iter_->buffer.get() + offset_; + *size = iter_->size - offset_; + bytes_read_ += iter_->size - offset_; + offset_ = iter_->size; + return true; +} + +void BigBufferInputStream::BackUp(size_t count) { + if (count > offset_) { + bytes_read_ -= offset_; + offset_ = 0; + } else { + offset_ -= count; + bytes_read_ -= count; + } +} + +bool BigBufferInputStream::CanRewind() const { + return true; +} + +bool BigBufferInputStream::Rewind() { + iter_ = buffer_->begin(); + offset_ = 0; + bytes_read_ = 0; + return true; +} + +size_t BigBufferInputStream::ByteCount() const { + return bytes_read_; +} + +bool BigBufferInputStream::HadError() const { + return false; +} + +size_t BigBufferInputStream::TotalSize() const { + return buffer_->size(); +} + +bool BigBufferInputStream::ReadFullyAtOffset(void* data, size_t byte_count, off64_t offset) { + if (byte_count == 0) { + return true; + } + if (offset < 0) { + return false; + } + if (offset > std::numeric_limits<off64_t>::max() - byte_count) { + return false; + } + if (offset + byte_count > buffer_->size()) { + return false; + } + auto p = reinterpret_cast<uint8_t*>(data); + for (auto iter = buffer_->begin(); iter != buffer_->end() && byte_count > 0; ++iter) { + if (offset < iter->size) { + size_t to_read = std::min(byte_count, (size_t)(iter->size - offset)); + memcpy(p, iter->buffer.get() + offset, to_read); + byte_count -= to_read; + p += to_read; + offset = 0; + } else { + offset -= iter->size; + } + } + return byte_count == 0; +} + +// +// BigBufferOutputStream +// + +bool BigBufferOutputStream::Next(void** data, size_t* size) { + *data = buffer_->NextBlock(size); + return true; +} + +void BigBufferOutputStream::BackUp(size_t count) { + buffer_->BackUp(count); +} + +size_t BigBufferOutputStream::ByteCount() const { + return buffer_->size(); +} + +bool BigBufferOutputStream::HadError() const { + return false; +} + +} // namespace android diff --git a/libs/androidfw/FileStream.cpp b/libs/androidfw/FileStream.cpp new file mode 100644 index 000000000000..b86c9cb729d4 --- /dev/null +++ b/libs/androidfw/FileStream.cpp @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2017 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. + */ + +#include "androidfw/FileStream.h" + +#include <errno.h> // for errno +#include <fcntl.h> // for O_RDONLY +#include <unistd.h> // for read + +#include "android-base/errors.h" +#include "android-base/file.h" // for O_BINARY +#include "android-base/macros.h" +#include "android-base/utf8.h" + +#if defined(_WIN32) +// This is only needed for O_CLOEXEC. +#include <windows.h> +#define O_CLOEXEC O_NOINHERIT +#endif + +using ::android::base::SystemErrorCodeToString; +using ::android::base::unique_fd; + +namespace android { + +FileInputStream::FileInputStream(const std::string& path, size_t buffer_capacity) + : buffer_capacity_(buffer_capacity) { + int mode = O_RDONLY | O_CLOEXEC | O_BINARY; + fd_.reset(TEMP_FAILURE_RETRY(::android::base::utf8::open(path.c_str(), mode))); + if (fd_ == -1) { + error_ = SystemErrorCodeToString(errno); + } else { + buffer_.reset(new uint8_t[buffer_capacity_]); + } +} + +FileInputStream::FileInputStream(int fd, size_t buffer_capacity) + : fd_(fd), buffer_capacity_(buffer_capacity) { + if (fd_ < 0) { + error_ = "Bad File Descriptor"; + } else { + buffer_.reset(new uint8_t[buffer_capacity_]); + } +} + +bool FileInputStream::Next(const void** data, size_t* size) { + if (HadError()) { + return false; + } + + // Deal with any remaining bytes after BackUp was called. + if (buffer_offset_ != buffer_size_) { + *data = buffer_.get() + buffer_offset_; + *size = buffer_size_ - buffer_offset_; + total_byte_count_ += buffer_size_ - buffer_offset_; + buffer_offset_ = buffer_size_; + return true; + } + + ssize_t n = TEMP_FAILURE_RETRY(read(fd_, buffer_.get(), buffer_capacity_)); + if (n < 0) { + error_ = SystemErrorCodeToString(errno); + fd_.reset(); + buffer_.reset(); + return false; + } + + buffer_size_ = static_cast<size_t>(n); + buffer_offset_ = buffer_size_; + total_byte_count_ += buffer_size_; + + *data = buffer_.get(); + *size = buffer_size_; + return buffer_size_ != 0u; +} + +void FileInputStream::BackUp(size_t count) { + if (count > buffer_offset_) { + count = buffer_offset_; + } + buffer_offset_ -= count; + total_byte_count_ -= count; +} + +size_t FileInputStream::ByteCount() const { + return total_byte_count_; +} + +bool FileInputStream::HadError() const { + return fd_ == -1; +} + +std::string FileInputStream::GetError() const { + return error_; +} + +bool FileInputStream::ReadFullyAtOffset(void* data, size_t byte_count, off64_t offset) { + return base::ReadFullyAtOffset(fd_, data, byte_count, offset); +} + +FileOutputStream::FileOutputStream(const std::string& path, size_t buffer_capacity) + : buffer_capacity_(buffer_capacity) { + int mode = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_BINARY; + owned_fd_.reset(TEMP_FAILURE_RETRY(::android::base::utf8::open(path.c_str(), mode, 0666))); + fd_ = owned_fd_.get(); + if (fd_ < 0) { + error_ = SystemErrorCodeToString(errno); + } else { + buffer_.reset(new uint8_t[buffer_capacity_]); + } +} + +FileOutputStream::FileOutputStream(unique_fd fd, size_t buffer_capacity) + : FileOutputStream(fd.get(), buffer_capacity) { + owned_fd_ = std::move(fd); +} + +FileOutputStream::FileOutputStream(int fd, size_t buffer_capacity) + : fd_(fd), buffer_capacity_(buffer_capacity) { + if (fd_ < 0) { + error_ = "Bad File Descriptor"; + } else { + buffer_.reset(new uint8_t[buffer_capacity_]); + } +} + +FileOutputStream::~FileOutputStream() { + // Flush the buffer. + Flush(); +} + +bool FileOutputStream::Next(void** data, size_t* size) { + if (HadError()) { + return false; + } + + if (buffer_offset_ == buffer_capacity_) { + if (!FlushImpl()) { + return false; + } + } + + const size_t buffer_size = buffer_capacity_ - buffer_offset_; + *data = buffer_.get() + buffer_offset_; + *size = buffer_size; + total_byte_count_ += buffer_size; + buffer_offset_ = buffer_capacity_; + return true; +} + +void FileOutputStream::BackUp(size_t count) { + if (count > buffer_offset_) { + count = buffer_offset_; + } + buffer_offset_ -= count; + total_byte_count_ -= count; +} + +size_t FileOutputStream::ByteCount() const { + return total_byte_count_; +} + +bool FileOutputStream::Flush() { + if (!HadError()) { + return FlushImpl(); + } + return false; +} + +bool FileOutputStream::FlushImpl() { + ssize_t n = TEMP_FAILURE_RETRY(write(fd_, buffer_.get(), buffer_offset_)); + if (n < 0) { + error_ = SystemErrorCodeToString(errno); + owned_fd_.reset(); + fd_ = -1; + buffer_.reset(); + return false; + } + + buffer_offset_ = 0u; + return true; +} + +bool FileOutputStream::HadError() const { + return fd_ == -1; +} + +std::string FileOutputStream::GetError() const { + return error_; +} + +} // namespace android diff --git a/libs/androidfw/NinePatch.cpp b/libs/androidfw/NinePatch.cpp new file mode 100644 index 000000000000..1fdbebfb6daa --- /dev/null +++ b/libs/androidfw/NinePatch.cpp @@ -0,0 +1,682 @@ +/* + * Copyright (C) 2016 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. + */ + +#include <sstream> +#include <string> +#include <vector> + +#include "androidfw/Image.h" +#include "androidfw/ResourceTypes.h" +#include "androidfw/StringPiece.h" + +using android::StringPiece; + +namespace android { + +// Colors in the format 0xAARRGGBB (the way 9-patch expects it). +constexpr static const uint32_t kColorOpaqueWhite = 0xffffffffu; +constexpr static const uint32_t kColorOpaqueBlack = 0xff000000u; +constexpr static const uint32_t kColorOpaqueRed = 0xffff0000u; + +constexpr static const uint32_t kPrimaryColor = kColorOpaqueBlack; +constexpr static const uint32_t kSecondaryColor = kColorOpaqueRed; + +/** + * Returns the alpha value encoded in the 0xAARRGBB encoded pixel. + */ +static uint32_t get_alpha(uint32_t color); + +/** + * Determines whether a color on an ImageLine is valid. + * A 9patch image may use a transparent color as neutral, + * or a fully opaque white color as neutral, based on the + * pixel color at (0,0) of the image. One or the other is fine, + * but we need to ensure consistency throughout the image. + */ +class ColorValidator { + public: + virtual ~ColorValidator() = default; + + /** + * Returns true if the color specified is a neutral color + * (no padding, stretching, or optical bounds). + */ + virtual bool IsNeutralColor(uint32_t color) const = 0; + + /** + * Returns true if the color is either a neutral color + * or one denoting padding, stretching, or optical bounds. + */ + bool IsValidColor(uint32_t color) const { + switch (color) { + case kPrimaryColor: + case kSecondaryColor: + return true; + } + return IsNeutralColor(color); + } +}; + +// Walks an ImageLine and records Ranges of primary and secondary colors. +// The primary color is black and is used to denote a padding or stretching +// range, +// depending on which border we're iterating over. +// The secondary color is red and is used to denote optical bounds. +// +// An ImageLine is a templated-interface that would look something like this if +// it +// were polymorphic: +// +// class ImageLine { +// public: +// virtual int32_t GetLength() const = 0; +// virtual uint32_t GetColor(int32_t idx) const = 0; +// }; +// +template <typename ImageLine> +static bool FillRanges(const ImageLine* image_line, const ColorValidator* color_validator, + std::vector<Range>* primary_ranges, std::vector<Range>* secondary_ranges, + std::string* out_err) { + const int32_t length = image_line->GetLength(); + + uint32_t last_color = 0xffffffffu; + for (int32_t idx = 1; idx < length - 1; idx++) { + const uint32_t color = image_line->GetColor(idx); + if (!color_validator->IsValidColor(color)) { + *out_err = "found an invalid color"; + return false; + } + + if (color != last_color) { + // We are ending a range. Which range? + // note: encode the x offset without the final 1 pixel border. + if (last_color == kPrimaryColor) { + primary_ranges->back().end = idx - 1; + } else if (last_color == kSecondaryColor) { + secondary_ranges->back().end = idx - 1; + } + + // We are starting a range. Which range? + // note: encode the x offset without the final 1 pixel border. + if (color == kPrimaryColor) { + primary_ranges->push_back(Range(idx - 1, length - 2)); + } else if (color == kSecondaryColor) { + secondary_ranges->push_back(Range(idx - 1, length - 2)); + } + last_color = color; + } + } + return true; +} + +/** + * Iterates over a row in an image. Implements the templated ImageLine + * interface. + */ +class HorizontalImageLine { + public: + explicit HorizontalImageLine(uint8_t** rows, int32_t xoffset, int32_t yoffset, int32_t length) + : rows_(rows), xoffset_(xoffset), yoffset_(yoffset), length_(length) { + } + + inline int32_t GetLength() const { + return length_; + } + + inline uint32_t GetColor(int32_t idx) const { + return NinePatch::PackRGBA(rows_[yoffset_] + (idx + xoffset_) * 4); + } + + private: + uint8_t** rows_; + int32_t xoffset_, yoffset_, length_; + + DISALLOW_COPY_AND_ASSIGN(HorizontalImageLine); +}; + +/** + * Iterates over a column in an image. Implements the templated ImageLine + * interface. + */ +class VerticalImageLine { + public: + explicit VerticalImageLine(uint8_t** rows, int32_t xoffset, int32_t yoffset, int32_t length) + : rows_(rows), xoffset_(xoffset), yoffset_(yoffset), length_(length) { + } + + inline int32_t GetLength() const { + return length_; + } + + inline uint32_t GetColor(int32_t idx) const { + return NinePatch::PackRGBA(rows_[yoffset_ + idx] + (xoffset_ * 4)); + } + + private: + uint8_t** rows_; + int32_t xoffset_, yoffset_, length_; + + DISALLOW_COPY_AND_ASSIGN(VerticalImageLine); +}; + +class DiagonalImageLine { + public: + explicit DiagonalImageLine(uint8_t** rows, int32_t xoffset, int32_t yoffset, int32_t xstep, + int32_t ystep, int32_t length) + : rows_(rows), + xoffset_(xoffset), + yoffset_(yoffset), + xstep_(xstep), + ystep_(ystep), + length_(length) { + } + + inline int32_t GetLength() const { + return length_; + } + + inline uint32_t GetColor(int32_t idx) const { + return NinePatch::PackRGBA(rows_[yoffset_ + (idx * ystep_)] + ((idx + xoffset_) * xstep_) * 4); + } + + private: + uint8_t** rows_; + int32_t xoffset_, yoffset_, xstep_, ystep_, length_; + + DISALLOW_COPY_AND_ASSIGN(DiagonalImageLine); +}; + +class TransparentNeutralColorValidator : public ColorValidator { + public: + bool IsNeutralColor(uint32_t color) const override { + return get_alpha(color) == 0; + } +}; + +class WhiteNeutralColorValidator : public ColorValidator { + public: + bool IsNeutralColor(uint32_t color) const override { + return color == kColorOpaqueWhite; + } +}; + +inline static uint32_t get_alpha(uint32_t color) { + return (color & 0xff000000u) >> 24; +} + +static bool PopulateBounds(const std::vector<Range>& padding, + const std::vector<Range>& layout_bounds, + const std::vector<Range>& stretch_regions, const int32_t length, + int32_t* padding_start, int32_t* padding_end, int32_t* layout_start, + int32_t* layout_end, StringPiece edge_name, std::string* out_err) { + if (padding.size() > 1) { + std::stringstream err_stream; + err_stream << "too many padding sections on " << edge_name << " border"; + *out_err = err_stream.str(); + return false; + } + + *padding_start = 0; + *padding_end = 0; + if (!padding.empty()) { + const Range& range = padding.front(); + *padding_start = range.start; + *padding_end = length - range.end; + } else if (!stretch_regions.empty()) { + // No padding was defined. Compute the padding from the first and last + // stretch regions. + *padding_start = stretch_regions.front().start; + *padding_end = length - stretch_regions.back().end; + } + + if (layout_bounds.size() > 2) { + std::stringstream err_stream; + err_stream << "too many layout bounds sections on " << edge_name << " border"; + *out_err = err_stream.str(); + return false; + } + + *layout_start = 0; + *layout_end = 0; + if (layout_bounds.size() >= 1) { + const Range& range = layout_bounds.front(); + // If there is only one layout bound segment, it might not start at 0, but + // then it should + // end at length. + if (range.start != 0 && range.end != length) { + std::stringstream err_stream; + err_stream << "layout bounds on " << edge_name << " border must start at edge"; + *out_err = err_stream.str(); + return false; + } + *layout_start = range.end; + + if (layout_bounds.size() >= 2) { + const Range& range = layout_bounds.back(); + if (range.end != length) { + std::stringstream err_stream; + err_stream << "layout bounds on " << edge_name << " border must start at edge"; + *out_err = err_stream.str(); + return false; + } + *layout_end = length - range.start; + } + } + return true; +} + +static int32_t CalculateSegmentCount(const std::vector<Range>& stretch_regions, int32_t length) { + if (stretch_regions.size() == 0) { + return 0; + } + + const bool start_is_fixed = stretch_regions.front().start != 0; + const bool end_is_fixed = stretch_regions.back().end != length; + int32_t modifier = 0; + if (start_is_fixed && end_is_fixed) { + modifier = 1; + } else if (!start_is_fixed && !end_is_fixed) { + modifier = -1; + } + return static_cast<int32_t>(stretch_regions.size()) * 2 + modifier; +} + +static uint32_t GetRegionColor(uint8_t** rows, const Bounds& region) { + // Sample the first pixel to compare against. + const uint32_t expected_color = NinePatch::PackRGBA(rows[region.top] + region.left * 4); + for (int32_t y = region.top; y < region.bottom; y++) { + const uint8_t* row = rows[y]; + for (int32_t x = region.left; x < region.right; x++) { + const uint32_t color = NinePatch::PackRGBA(row + x * 4); + if (get_alpha(color) == 0) { + // The color is transparent. + // If the expectedColor is not transparent, NO_COLOR. + if (get_alpha(expected_color) != 0) { + return android::Res_png_9patch::NO_COLOR; + } + } else if (color != expected_color) { + return android::Res_png_9patch::NO_COLOR; + } + } + } + + if (get_alpha(expected_color) == 0) { + return android::Res_png_9patch::TRANSPARENT_COLOR; + } + return expected_color; +} + +// Fills out_colors with each 9-patch section's color. If the whole section is +// transparent, +// it gets the special TRANSPARENT color. If the whole section is the same +// color, it is assigned +// that color. Otherwise it gets the special NO_COLOR color. +// +// Note that the rows contain the 9-patch 1px border, and the indices in the +// stretch regions are +// already offset to exclude the border. This means that each time the rows are +// accessed, +// the indices must be offset by 1. +// +// width and height also include the 9-patch 1px border. +static void CalculateRegionColors(uint8_t** rows, + const std::vector<Range>& horizontal_stretch_regions, + const std::vector<Range>& vertical_stretch_regions, + const int32_t width, const int32_t height, + std::vector<uint32_t>* out_colors) { + int32_t next_top = 0; + Bounds bounds; + auto row_iter = vertical_stretch_regions.begin(); + while (next_top != height) { + if (row_iter != vertical_stretch_regions.end()) { + if (next_top != row_iter->start) { + // This is a fixed segment. + // Offset the bounds by 1 to accommodate the border. + bounds.top = next_top + 1; + bounds.bottom = row_iter->start + 1; + next_top = row_iter->start; + } else { + // This is a stretchy segment. + // Offset the bounds by 1 to accommodate the border. + bounds.top = row_iter->start + 1; + bounds.bottom = row_iter->end + 1; + next_top = row_iter->end; + ++row_iter; + } + } else { + // This is the end, fixed section. + // Offset the bounds by 1 to accommodate the border. + bounds.top = next_top + 1; + bounds.bottom = height + 1; + next_top = height; + } + + int32_t next_left = 0; + auto col_iter = horizontal_stretch_regions.begin(); + while (next_left != width) { + if (col_iter != horizontal_stretch_regions.end()) { + if (next_left != col_iter->start) { + // This is a fixed segment. + // Offset the bounds by 1 to accommodate the border. + bounds.left = next_left + 1; + bounds.right = col_iter->start + 1; + next_left = col_iter->start; + } else { + // This is a stretchy segment. + // Offset the bounds by 1 to accommodate the border. + bounds.left = col_iter->start + 1; + bounds.right = col_iter->end + 1; + next_left = col_iter->end; + ++col_iter; + } + } else { + // This is the end, fixed section. + // Offset the bounds by 1 to accommodate the border. + bounds.left = next_left + 1; + bounds.right = width + 1; + next_left = width; + } + out_colors->push_back(GetRegionColor(rows, bounds)); + } + } +} + +// Calculates the insets of a row/column of pixels based on where the largest +// alpha value begins +// (on both sides). +template <typename ImageLine> +static void FindOutlineInsets(const ImageLine* image_line, int32_t* out_start, int32_t* out_end) { + *out_start = 0; + *out_end = 0; + + const int32_t length = image_line->GetLength(); + if (length < 3) { + return; + } + + // If the length is odd, we want both sides to process the center pixel, + // so we use two different midpoints (to account for < and <= in the different + // loops). + const int32_t mid2 = length / 2; + const int32_t mid1 = mid2 + (length % 2); + + uint32_t max_alpha = 0; + for (int32_t i = 0; i < mid1 && max_alpha != 0xff; i++) { + uint32_t alpha = get_alpha(image_line->GetColor(i)); + if (alpha > max_alpha) { + max_alpha = alpha; + *out_start = i; + } + } + + max_alpha = 0; + for (int32_t i = length - 1; i >= mid2 && max_alpha != 0xff; i--) { + uint32_t alpha = get_alpha(image_line->GetColor(i)); + if (alpha > max_alpha) { + max_alpha = alpha; + *out_end = length - (i + 1); + } + } + return; +} + +template <typename ImageLine> +static uint32_t FindMaxAlpha(const ImageLine* image_line) { + const int32_t length = image_line->GetLength(); + uint32_t max_alpha = 0; + for (int32_t idx = 0; idx < length && max_alpha != 0xff; idx++) { + uint32_t alpha = get_alpha(image_line->GetColor(idx)); + if (alpha > max_alpha) { + max_alpha = alpha; + } + } + return max_alpha; +} + +// Pack the pixels in as 0xAARRGGBB (as 9-patch expects it). +uint32_t NinePatch::PackRGBA(const uint8_t* pixel) { + return (pixel[3] << 24) | (pixel[0] << 16) | (pixel[1] << 8) | pixel[2]; +} + +std::unique_ptr<NinePatch> NinePatch::Create(uint8_t** rows, const int32_t width, + const int32_t height, std::string* out_err) { + if (width < 3 || height < 3) { + *out_err = "image must be at least 3x3 (1x1 image with 1 pixel border)"; + return {}; + } + + std::vector<Range> horizontal_padding; + std::vector<Range> horizontal_layout_bounds; + std::vector<Range> vertical_padding; + std::vector<Range> vertical_layout_bounds; + std::vector<Range> unexpected_ranges; + std::unique_ptr<ColorValidator> color_validator; + + if (rows[0][3] == 0) { + color_validator = std::make_unique<TransparentNeutralColorValidator>(); + } else if (PackRGBA(rows[0]) == kColorOpaqueWhite) { + color_validator = std::make_unique<WhiteNeutralColorValidator>(); + } else { + *out_err = "top-left corner pixel must be either opaque white or transparent"; + return {}; + } + + // Private constructor, can't use make_unique. + auto nine_patch = std::unique_ptr<NinePatch>(new NinePatch()); + + HorizontalImageLine top_row(rows, 0, 0, width); + if (!FillRanges(&top_row, color_validator.get(), &nine_patch->horizontal_stretch_regions, + &unexpected_ranges, out_err)) { + return {}; + } + + if (!unexpected_ranges.empty()) { + const Range& range = unexpected_ranges[0]; + std::stringstream err_stream; + err_stream << "found unexpected optical bounds (red pixel) on top border " + << "at x=" << range.start + 1; + *out_err = err_stream.str(); + return {}; + } + + VerticalImageLine left_col(rows, 0, 0, height); + if (!FillRanges(&left_col, color_validator.get(), &nine_patch->vertical_stretch_regions, + &unexpected_ranges, out_err)) { + return {}; + } + + if (!unexpected_ranges.empty()) { + const Range& range = unexpected_ranges[0]; + std::stringstream err_stream; + err_stream << "found unexpected optical bounds (red pixel) on left border " + << "at y=" << range.start + 1; + return {}; + } + + HorizontalImageLine bottom_row(rows, 0, height - 1, width); + if (!FillRanges(&bottom_row, color_validator.get(), &horizontal_padding, + &horizontal_layout_bounds, out_err)) { + return {}; + } + + if (!PopulateBounds(horizontal_padding, horizontal_layout_bounds, + nine_patch->horizontal_stretch_regions, width - 2, &nine_patch->padding.left, + &nine_patch->padding.right, &nine_patch->layout_bounds.left, + &nine_patch->layout_bounds.right, "bottom", out_err)) { + return {}; + } + + VerticalImageLine right_col(rows, width - 1, 0, height); + if (!FillRanges(&right_col, color_validator.get(), &vertical_padding, &vertical_layout_bounds, + out_err)) { + return {}; + } + + if (!PopulateBounds(vertical_padding, vertical_layout_bounds, + nine_patch->vertical_stretch_regions, height - 2, &nine_patch->padding.top, + &nine_patch->padding.bottom, &nine_patch->layout_bounds.top, + &nine_patch->layout_bounds.bottom, "right", out_err)) { + return {}; + } + + // Fill the region colors of the 9-patch. + const int32_t num_rows = CalculateSegmentCount(nine_patch->horizontal_stretch_regions, width - 2); + const int32_t num_cols = CalculateSegmentCount(nine_patch->vertical_stretch_regions, height - 2); + if ((int64_t)num_rows * (int64_t)num_cols > 0x7f) { + *out_err = "too many regions in 9-patch"; + return {}; + } + + nine_patch->region_colors.reserve(num_rows * num_cols); + CalculateRegionColors(rows, nine_patch->horizontal_stretch_regions, + nine_patch->vertical_stretch_regions, width - 2, height - 2, + &nine_patch->region_colors); + + // Compute the outline based on opacity. + + // Find left and right extent of 9-patch content on center row. + HorizontalImageLine mid_row(rows, 1, height / 2, width - 2); + FindOutlineInsets(&mid_row, &nine_patch->outline.left, &nine_patch->outline.right); + + // Find top and bottom extent of 9-patch content on center column. + VerticalImageLine mid_col(rows, width / 2, 1, height - 2); + FindOutlineInsets(&mid_col, &nine_patch->outline.top, &nine_patch->outline.bottom); + + const int32_t outline_width = (width - 2) - nine_patch->outline.left - nine_patch->outline.right; + const int32_t outline_height = + (height - 2) - nine_patch->outline.top - nine_patch->outline.bottom; + + // Find the largest alpha value within the outline area. + HorizontalImageLine outline_mid_row(rows, 1 + nine_patch->outline.left, + 1 + nine_patch->outline.top + (outline_height / 2), + outline_width); + VerticalImageLine outline_mid_col(rows, 1 + nine_patch->outline.left + (outline_width / 2), + 1 + nine_patch->outline.top, outline_height); + nine_patch->outline_alpha = + std::max(FindMaxAlpha(&outline_mid_row), FindMaxAlpha(&outline_mid_col)); + + // Assuming the image is a round rect, compute the radius by marching + // diagonally from the top left corner towards the center. + DiagonalImageLine diagonal(rows, 1 + nine_patch->outline.left, 1 + nine_patch->outline.top, 1, 1, + std::min(outline_width, outline_height)); + int32_t top_left, bottom_right; + FindOutlineInsets(&diagonal, &top_left, &bottom_right); + + /* Determine source radius based upon inset: + * sqrt(r^2 + r^2) = sqrt(i^2 + i^2) + r + * sqrt(2) * r = sqrt(2) * i + r + * (sqrt(2) - 1) * r = sqrt(2) * i + * r = sqrt(2) / (sqrt(2) - 1) * i + */ + nine_patch->outline_radius = 3.4142f * top_left; + return nine_patch; +} + +std::unique_ptr<uint8_t[]> NinePatch::SerializeBase(size_t* outLen) const { + android::Res_png_9patch data; + data.numXDivs = static_cast<uint8_t>(horizontal_stretch_regions.size()) * 2; + data.numYDivs = static_cast<uint8_t>(vertical_stretch_regions.size()) * 2; + data.numColors = static_cast<uint8_t>(region_colors.size()); + data.paddingLeft = padding.left; + data.paddingRight = padding.right; + data.paddingTop = padding.top; + data.paddingBottom = padding.bottom; + + auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[data.serializedSize()]); + android::Res_png_9patch::serialize(data, (const int32_t*)horizontal_stretch_regions.data(), + (const int32_t*)vertical_stretch_regions.data(), + region_colors.data(), buffer.get()); + // Convert to file endianness. + reinterpret_cast<android::Res_png_9patch*>(buffer.get())->deviceToFile(); + + *outLen = data.serializedSize(); + return buffer; +} + +std::unique_ptr<uint8_t[]> NinePatch::SerializeLayoutBounds(size_t* out_len) const { + size_t chunk_len = sizeof(uint32_t) * 4; + auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[chunk_len]); + uint8_t* cursor = buffer.get(); + + memcpy(cursor, &layout_bounds.left, sizeof(layout_bounds.left)); + cursor += sizeof(layout_bounds.left); + + memcpy(cursor, &layout_bounds.top, sizeof(layout_bounds.top)); + cursor += sizeof(layout_bounds.top); + + memcpy(cursor, &layout_bounds.right, sizeof(layout_bounds.right)); + cursor += sizeof(layout_bounds.right); + + memcpy(cursor, &layout_bounds.bottom, sizeof(layout_bounds.bottom)); + cursor += sizeof(layout_bounds.bottom); + + *out_len = chunk_len; + return buffer; +} + +std::unique_ptr<uint8_t[]> NinePatch::SerializeRoundedRectOutline(size_t* out_len) const { + size_t chunk_len = sizeof(uint32_t) * 6; + auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[chunk_len]); + uint8_t* cursor = buffer.get(); + + memcpy(cursor, &outline.left, sizeof(outline.left)); + cursor += sizeof(outline.left); + + memcpy(cursor, &outline.top, sizeof(outline.top)); + cursor += sizeof(outline.top); + + memcpy(cursor, &outline.right, sizeof(outline.right)); + cursor += sizeof(outline.right); + + memcpy(cursor, &outline.bottom, sizeof(outline.bottom)); + cursor += sizeof(outline.bottom); + + *((float*)cursor) = outline_radius; + cursor += sizeof(outline_radius); + + *((uint32_t*)cursor) = outline_alpha; + + *out_len = chunk_len; + return buffer; +} + +::std::ostream& operator<<(::std::ostream& out, const Range& range) { + return out << "[" << range.start << ", " << range.end << ")"; +} + +::std::ostream& operator<<(::std::ostream& out, const Bounds& bounds) { + return out << "l=" << bounds.left << " t=" << bounds.top << " r=" << bounds.right + << " b=" << bounds.bottom; +} + +template <typename T> +std::ostream& operator<<(std::ostream& os, const std::vector<T>& v) { + for (int i = 0; i < v.size(); ++i) { + os << v[i]; + if (i != v.size() - 1) os << " "; + } + return os; +} + +::std::ostream& operator<<(::std::ostream& out, const NinePatch& nine_patch) { + return out << "horizontalStretch:" << nine_patch.horizontal_stretch_regions + << " verticalStretch:" << nine_patch.vertical_stretch_regions + << " padding: " << nine_patch.padding << ", bounds: " << nine_patch.layout_bounds + << ", outline: " << nine_patch.outline << " rad=" << nine_patch.outline_radius + << " alpha=" << nine_patch.outline_alpha; +} + +} // namespace android diff --git a/libs/androidfw/Png.cpp b/libs/androidfw/Png.cpp new file mode 100644 index 000000000000..fb45cd9b49d0 --- /dev/null +++ b/libs/androidfw/Png.cpp @@ -0,0 +1,1259 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "androidfw/Png.h" + +#include <png.h> +#include <zlib.h> + +#include <iostream> +#include <sstream> +#include <string> +#include <vector> + +#include "android-base/strings.h" +#include "androidfw/BigBuffer.h" +#include "androidfw/ResourceTypes.h" +#include "androidfw/Source.h" + +namespace android { + +constexpr bool kDebug = false; + +struct PngInfo { + ~PngInfo() { + for (png_bytep row : rows) { + if (row != nullptr) { + delete[] row; + } + } + + delete[] xDivs; + delete[] yDivs; + } + + void* serialize9Patch() { + void* serialized = Res_png_9patch::serialize(info9Patch, xDivs, yDivs, colors.data()); + reinterpret_cast<Res_png_9patch*>(serialized)->deviceToFile(); + return serialized; + } + + uint32_t width = 0; + uint32_t height = 0; + std::vector<png_bytep> rows; + + bool is9Patch = false; + Res_png_9patch info9Patch; + int32_t* xDivs = nullptr; + int32_t* yDivs = nullptr; + std::vector<uint32_t> colors; + + // Layout padding. + bool haveLayoutBounds = false; + int32_t layoutBoundsLeft; + int32_t layoutBoundsTop; + int32_t layoutBoundsRight; + int32_t layoutBoundsBottom; + + // Round rect outline description. + int32_t outlineInsetsLeft; + int32_t outlineInsetsTop; + int32_t outlineInsetsRight; + int32_t outlineInsetsBottom; + float outlineRadius; + uint8_t outlineAlpha; +}; + +static void readDataFromStream(png_structp readPtr, png_bytep data, png_size_t length) { + std::istream* input = reinterpret_cast<std::istream*>(png_get_io_ptr(readPtr)); + if (!input->read(reinterpret_cast<char*>(data), length)) { + png_error(readPtr, strerror(errno)); + } +} + +static void writeDataToStream(png_structp writePtr, png_bytep data, png_size_t length) { + BigBuffer* outBuffer = reinterpret_cast<BigBuffer*>(png_get_io_ptr(writePtr)); + png_bytep buf = outBuffer->NextBlock<png_byte>(length); + memcpy(buf, data, length); +} + +static void flushDataToStream(png_structp /*writePtr*/) { +} + +static void logWarning(png_structp readPtr, png_const_charp warningMessage) { + IDiagnostics* diag = reinterpret_cast<IDiagnostics*>(png_get_error_ptr(readPtr)); + diag->Warn(DiagMessage() << warningMessage); +} + +static bool readPng(IDiagnostics* diag, png_structp readPtr, png_infop infoPtr, PngInfo* outInfo) { + if (setjmp(png_jmpbuf(readPtr))) { + diag->Error(DiagMessage() << "failed reading png"); + return false; + } + + png_set_sig_bytes(readPtr, kPngSignatureSize); + png_read_info(readPtr, infoPtr); + + int colorType, bitDepth, interlaceType, compressionType; + png_get_IHDR(readPtr, infoPtr, &outInfo->width, &outInfo->height, &bitDepth, &colorType, + &interlaceType, &compressionType, nullptr); + + if (colorType == PNG_COLOR_TYPE_PALETTE) { + png_set_palette_to_rgb(readPtr); + } + + if (colorType == PNG_COLOR_TYPE_GRAY && bitDepth < 8) { + png_set_expand_gray_1_2_4_to_8(readPtr); + } + + if (png_get_valid(readPtr, infoPtr, PNG_INFO_tRNS)) { + png_set_tRNS_to_alpha(readPtr); + } + + if (bitDepth == 16) { + png_set_strip_16(readPtr); + } + + if (!(colorType & PNG_COLOR_MASK_ALPHA)) { + png_set_add_alpha(readPtr, 0xFF, PNG_FILLER_AFTER); + } + + if (colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_GRAY_ALPHA) { + png_set_gray_to_rgb(readPtr); + } + + png_set_interlace_handling(readPtr); + png_read_update_info(readPtr, infoPtr); + + const uint32_t rowBytes = png_get_rowbytes(readPtr, infoPtr); + outInfo->rows.resize(outInfo->height); + for (size_t i = 0; i < outInfo->height; i++) { + outInfo->rows[i] = new png_byte[rowBytes]; + } + + png_read_image(readPtr, outInfo->rows.data()); + png_read_end(readPtr, infoPtr); + return true; +} + +static void checkNinePatchSerialization(Res_png_9patch* inPatch, void* data) { + size_t patchSize = inPatch->serializedSize(); + void* newData = malloc(patchSize); + memcpy(newData, data, patchSize); + Res_png_9patch* outPatch = inPatch->deserialize(newData); + outPatch->fileToDevice(); + // deserialization is done in place, so outPatch == newData + assert(outPatch == newData); + assert(outPatch->numXDivs == inPatch->numXDivs); + assert(outPatch->numYDivs == inPatch->numYDivs); + assert(outPatch->paddingLeft == inPatch->paddingLeft); + assert(outPatch->paddingRight == inPatch->paddingRight); + assert(outPatch->paddingTop == inPatch->paddingTop); + assert(outPatch->paddingBottom == inPatch->paddingBottom); + /* for (int i = 0; i < outPatch->numXDivs; i++) { + assert(outPatch->getXDivs()[i] == inPatch->getXDivs()[i]); + } + for (int i = 0; i < outPatch->numYDivs; i++) { + assert(outPatch->getYDivs()[i] == inPatch->getYDivs()[i]); + } + for (int i = 0; i < outPatch->numColors; i++) { + assert(outPatch->getColors()[i] == inPatch->getColors()[i]); + }*/ + free(newData); +} + +/*static void dump_image(int w, int h, const png_byte* const* rows, int +color_type) { + int i, j, rr, gg, bb, aa; + + int bpp; + if (color_type == PNG_COLOR_TYPE_PALETTE || color_type == +PNG_COLOR_TYPE_GRAY) { + bpp = 1; + } else if (color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { + bpp = 2; + } else if (color_type == PNG_COLOR_TYPE_RGB || color_type == +PNG_COLOR_TYPE_RGB_ALPHA) { + // We use a padding byte even when there is no alpha + bpp = 4; + } else { + printf("Unknown color type %d.\n", color_type); + } + + for (j = 0; j < h; j++) { + const png_byte* row = rows[j]; + for (i = 0; i < w; i++) { + rr = row[0]; + gg = row[1]; + bb = row[2]; + aa = row[3]; + row += bpp; + + if (i == 0) { + printf("Row %d:", j); + } + switch (bpp) { + case 1: + printf(" (%d)", rr); + break; + case 2: + printf(" (%d %d", rr, gg); + break; + case 3: + printf(" (%d %d %d)", rr, gg, bb); + break; + case 4: + printf(" (%d %d %d %d)", rr, gg, bb, aa); + break; + } + if (i == (w - 1)) { + printf("\n"); + } + } + } +}*/ + +#ifdef MAX +#undef MAX +#endif +#ifdef ABS +#undef ABS +#endif + +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define ABS(a) ((a) < 0 ? -(a) : (a)) + +static void analyze_image(IDiagnostics* diag, const PngInfo& imageInfo, int grayscaleTolerance, + png_colorp rgbPalette, png_bytep alphaPalette, int* paletteEntries, + bool* hasTransparency, int* colorType, png_bytepp outRows) { + int w = imageInfo.width; + int h = imageInfo.height; + int i, j, rr, gg, bb, aa, idx; + uint32_t colors[256], col; + int num_colors = 0; + int maxGrayDeviation = 0; + + bool isOpaque = true; + bool isPalette = true; + bool isGrayscale = true; + + // Scan the entire image and determine if: + // 1. Every pixel has R == G == B (grayscale) + // 2. Every pixel has A == 255 (opaque) + // 3. There are no more than 256 distinct RGBA colors + + if (kDebug) { + printf("Initial image data:\n"); + // dump_image(w, h, imageInfo.rows.data(), PNG_COLOR_TYPE_RGB_ALPHA); + } + + for (j = 0; j < h; j++) { + const png_byte* row = imageInfo.rows[j]; + png_bytep out = outRows[j]; + for (i = 0; i < w; i++) { + rr = *row++; + gg = *row++; + bb = *row++; + aa = *row++; + + int odev = maxGrayDeviation; + maxGrayDeviation = MAX(ABS(rr - gg), maxGrayDeviation); + maxGrayDeviation = MAX(ABS(gg - bb), maxGrayDeviation); + maxGrayDeviation = MAX(ABS(bb - rr), maxGrayDeviation); + if (maxGrayDeviation > odev) { + if (kDebug) { + printf("New max dev. = %d at pixel (%d, %d) = (%d %d %d %d)\n", maxGrayDeviation, i, j, + rr, gg, bb, aa); + } + } + + // Check if image is really grayscale + if (isGrayscale) { + if (rr != gg || rr != bb) { + if (kDebug) { + printf("Found a non-gray pixel at %d, %d = (%d %d %d %d)\n", i, j, rr, gg, bb, aa); + } + isGrayscale = false; + } + } + + // Check if image is really opaque + if (isOpaque) { + if (aa != 0xff) { + if (kDebug) { + printf("Found a non-opaque pixel at %d, %d = (%d %d %d %d)\n", i, j, rr, gg, bb, aa); + } + isOpaque = false; + } + } + + // Check if image is really <= 256 colors + if (isPalette) { + col = (uint32_t)((rr << 24) | (gg << 16) | (bb << 8) | aa); + bool match = false; + for (idx = 0; idx < num_colors; idx++) { + if (colors[idx] == col) { + match = true; + break; + } + } + + // Write the palette index for the pixel to outRows optimistically + // We might overwrite it later if we decide to encode as gray or + // gray + alpha + *out++ = idx; + if (!match) { + if (num_colors == 256) { + if (kDebug) { + printf("Found 257th color at %d, %d\n", i, j); + } + isPalette = false; + } else { + colors[num_colors++] = col; + } + } + } + } + } + + *paletteEntries = 0; + *hasTransparency = !isOpaque; + int bpp = isOpaque ? 3 : 4; + int paletteSize = w * h + bpp * num_colors; + + if (kDebug) { + printf("isGrayscale = %s\n", isGrayscale ? "true" : "false"); + printf("isOpaque = %s\n", isOpaque ? "true" : "false"); + printf("isPalette = %s\n", isPalette ? "true" : "false"); + printf("Size w/ palette = %d, gray+alpha = %d, rgb(a) = %d\n", paletteSize, 2 * w * h, + bpp * w * h); + printf("Max gray deviation = %d, tolerance = %d\n", maxGrayDeviation, grayscaleTolerance); + } + + // Choose the best color type for the image. + // 1. Opaque gray - use COLOR_TYPE_GRAY at 1 byte/pixel + // 2. Gray + alpha - use COLOR_TYPE_PALETTE if the number of distinct + // combinations + // is sufficiently small, otherwise use COLOR_TYPE_GRAY_ALPHA + // 3. RGB(A) - use COLOR_TYPE_PALETTE if the number of distinct colors is + // sufficiently + // small, otherwise use COLOR_TYPE_RGB{_ALPHA} + if (isGrayscale) { + if (isOpaque) { + *colorType = PNG_COLOR_TYPE_GRAY; // 1 byte/pixel + } else { + // Use a simple heuristic to determine whether using a palette will + // save space versus using gray + alpha for each pixel. + // This doesn't take into account chunk overhead, filtering, LZ + // compression, etc. + if (isPalette && (paletteSize < 2 * w * h)) { + *colorType = PNG_COLOR_TYPE_PALETTE; // 1 byte/pixel + 4 bytes/color + } else { + *colorType = PNG_COLOR_TYPE_GRAY_ALPHA; // 2 bytes per pixel + } + } + } else if (isPalette && (paletteSize < bpp * w * h)) { + *colorType = PNG_COLOR_TYPE_PALETTE; + } else { + if (maxGrayDeviation <= grayscaleTolerance) { + diag->Note(DiagMessage() << "forcing image to gray (max deviation = " << maxGrayDeviation + << ")"); + *colorType = isOpaque ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_GRAY_ALPHA; + } else { + *colorType = isOpaque ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA; + } + } + + // Perform postprocessing of the image or palette data based on the final + // color type chosen + + if (*colorType == PNG_COLOR_TYPE_PALETTE) { + // Create separate RGB and Alpha palettes and set the number of colors + *paletteEntries = num_colors; + + // Create the RGB and alpha palettes + for (int idx = 0; idx < num_colors; idx++) { + col = colors[idx]; + rgbPalette[idx].red = (png_byte)((col >> 24) & 0xff); + rgbPalette[idx].green = (png_byte)((col >> 16) & 0xff); + rgbPalette[idx].blue = (png_byte)((col >> 8) & 0xff); + alphaPalette[idx] = (png_byte)(col & 0xff); + } + } else if (*colorType == PNG_COLOR_TYPE_GRAY || *colorType == PNG_COLOR_TYPE_GRAY_ALPHA) { + // If the image is gray or gray + alpha, compact the pixels into outRows + for (j = 0; j < h; j++) { + const png_byte* row = imageInfo.rows[j]; + png_bytep out = outRows[j]; + for (i = 0; i < w; i++) { + rr = *row++; + gg = *row++; + bb = *row++; + aa = *row++; + + if (isGrayscale) { + *out++ = rr; + } else { + *out++ = (png_byte)(rr * 0.2126f + gg * 0.7152f + bb * 0.0722f); + } + if (!isOpaque) { + *out++ = aa; + } + } + } + } +} + +static bool writePng(IDiagnostics* diag, png_structp writePtr, png_infop infoPtr, PngInfo* info, + int grayScaleTolerance) { + if (setjmp(png_jmpbuf(writePtr))) { + diag->Error(DiagMessage() << "failed to write png"); + return false; + } + + uint32_t width, height; + int colorType, bitDepth, interlaceType, compressionType; + + png_unknown_chunk unknowns[3]; + unknowns[0].data = nullptr; + unknowns[1].data = nullptr; + unknowns[2].data = nullptr; + + png_bytepp outRows = (png_bytepp)malloc((int)info->height * sizeof(png_bytep)); + if (outRows == (png_bytepp)0) { + printf("Can't allocate output buffer!\n"); + exit(1); + } + for (uint32_t i = 0; i < info->height; i++) { + outRows[i] = (png_bytep)malloc(2 * (int)info->width); + if (outRows[i] == (png_bytep)0) { + printf("Can't allocate output buffer!\n"); + exit(1); + } + } + + png_set_compression_level(writePtr, Z_BEST_COMPRESSION); + + if (kDebug) { + diag->Note(DiagMessage() << "writing image: w = " << info->width << ", h = " << info->height); + } + + png_color rgbPalette[256]; + png_byte alphaPalette[256]; + bool hasTransparency; + int paletteEntries; + + analyze_image(diag, *info, grayScaleTolerance, rgbPalette, alphaPalette, &paletteEntries, + &hasTransparency, &colorType, outRows); + + // If the image is a 9-patch, we need to preserve it as a ARGB file to make + // sure the pixels will not be pre-dithered/clamped until we decide they are + if (info->is9Patch && (colorType == PNG_COLOR_TYPE_RGB || colorType == PNG_COLOR_TYPE_GRAY || + colorType == PNG_COLOR_TYPE_PALETTE)) { + colorType = PNG_COLOR_TYPE_RGB_ALPHA; + } + + if (kDebug) { + switch (colorType) { + case PNG_COLOR_TYPE_PALETTE: + diag->Note(DiagMessage() << "has " << paletteEntries << " colors" + << (hasTransparency ? " (with alpha)" : "") + << ", using PNG_COLOR_TYPE_PALLETTE"); + break; + case PNG_COLOR_TYPE_GRAY: + diag->Note(DiagMessage() << "is opaque gray, using PNG_COLOR_TYPE_GRAY"); + break; + case PNG_COLOR_TYPE_GRAY_ALPHA: + diag->Note(DiagMessage() << "is gray + alpha, using PNG_COLOR_TYPE_GRAY_ALPHA"); + break; + case PNG_COLOR_TYPE_RGB: + diag->Note(DiagMessage() << "is opaque RGB, using PNG_COLOR_TYPE_RGB"); + break; + case PNG_COLOR_TYPE_RGB_ALPHA: + diag->Note(DiagMessage() << "is RGB + alpha, using PNG_COLOR_TYPE_RGB_ALPHA"); + break; + } + } + + png_set_IHDR(writePtr, infoPtr, info->width, info->height, 8, colorType, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + if (colorType == PNG_COLOR_TYPE_PALETTE) { + png_set_PLTE(writePtr, infoPtr, rgbPalette, paletteEntries); + if (hasTransparency) { + png_set_tRNS(writePtr, infoPtr, alphaPalette, paletteEntries, (png_color_16p)0); + } + png_set_filter(writePtr, 0, PNG_NO_FILTERS); + } else { + png_set_filter(writePtr, 0, PNG_ALL_FILTERS); + } + + if (info->is9Patch) { + int chunkCount = 2 + (info->haveLayoutBounds ? 1 : 0); + int pIndex = info->haveLayoutBounds ? 2 : 1; + int bIndex = 1; + int oIndex = 0; + + // Chunks ordered thusly because older platforms depend on the base 9 patch + // data being last + png_bytep chunkNames = + info->haveLayoutBounds ? (png_bytep) "npOl\0npLb\0npTc\0" : (png_bytep) "npOl\0npTc"; + + // base 9 patch data + if (kDebug) { + diag->Note(DiagMessage() << "adding 9-patch info.."); + } + memcpy((char*)unknowns[pIndex].name, "npTc", 5); + unknowns[pIndex].data = (png_byte*)info->serialize9Patch(); + unknowns[pIndex].size = info->info9Patch.serializedSize(); + // TODO: remove the check below when everything works + checkNinePatchSerialization(&info->info9Patch, unknowns[pIndex].data); + + // automatically generated 9 patch outline data + int chunkSize = sizeof(png_uint_32) * 6; + memcpy((char*)unknowns[oIndex].name, "npOl", 5); + unknowns[oIndex].data = (png_byte*)calloc(chunkSize, 1); + png_byte outputData[chunkSize]; + memcpy(&outputData, &info->outlineInsetsLeft, 4 * sizeof(png_uint_32)); + ((float*)outputData)[4] = info->outlineRadius; + ((png_uint_32*)outputData)[5] = info->outlineAlpha; + memcpy(unknowns[oIndex].data, &outputData, chunkSize); + unknowns[oIndex].size = chunkSize; + + // optional optical inset / layout bounds data + if (info->haveLayoutBounds) { + int chunkSize = sizeof(png_uint_32) * 4; + memcpy((char*)unknowns[bIndex].name, "npLb", 5); + unknowns[bIndex].data = (png_byte*)calloc(chunkSize, 1); + memcpy(unknowns[bIndex].data, &info->layoutBoundsLeft, chunkSize); + unknowns[bIndex].size = chunkSize; + } + + for (int i = 0; i < chunkCount; i++) { + unknowns[i].location = PNG_HAVE_PLTE; + } + png_set_keep_unknown_chunks(writePtr, PNG_HANDLE_CHUNK_ALWAYS, chunkNames, chunkCount); + png_set_unknown_chunks(writePtr, infoPtr, unknowns, chunkCount); + +#if PNG_LIBPNG_VER < 10600 + // Deal with unknown chunk location bug in 1.5.x and earlier. + png_set_unknown_chunk_location(writePtr, infoPtr, 0, PNG_HAVE_PLTE); + if (info->haveLayoutBounds) { + png_set_unknown_chunk_location(writePtr, infoPtr, 1, PNG_HAVE_PLTE); + } +#endif + } + + png_write_info(writePtr, infoPtr); + + png_bytepp rows; + if (colorType == PNG_COLOR_TYPE_RGB || colorType == PNG_COLOR_TYPE_RGB_ALPHA) { + if (colorType == PNG_COLOR_TYPE_RGB) { + png_set_filler(writePtr, 0, PNG_FILLER_AFTER); + } + rows = info->rows.data(); + } else { + rows = outRows; + } + png_write_image(writePtr, rows); + + if (kDebug) { + printf("Final image data:\n"); + // dump_image(info->width, info->height, rows, colorType); + } + + png_write_end(writePtr, infoPtr); + + for (uint32_t i = 0; i < info->height; i++) { + free(outRows[i]); + } + free(outRows); + free(unknowns[0].data); + free(unknowns[1].data); + free(unknowns[2].data); + + png_get_IHDR(writePtr, infoPtr, &width, &height, &bitDepth, &colorType, &interlaceType, + &compressionType, nullptr); + + if (kDebug) { + diag->Note(DiagMessage() << "image written: w = " << width << ", h = " << height + << ", d = " << bitDepth << ", colors = " << colorType + << ", inter = " << interlaceType << ", comp = " << compressionType); + } + return true; +} + +constexpr uint32_t kColorWhite = 0xffffffffu; +constexpr uint32_t kColorTick = 0xff000000u; +constexpr uint32_t kColorLayoutBoundsTick = 0xff0000ffu; + +enum class TickType { kNone, kTick, kLayoutBounds, kBoth }; + +static TickType tickType(png_bytep p, bool transparent, const char** outError) { + png_uint_32 color = p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); + + if (transparent) { + if (p[3] == 0) { + return TickType::kNone; + } + if (color == kColorLayoutBoundsTick) { + return TickType::kLayoutBounds; + } + if (color == kColorTick) { + return TickType::kTick; + } + + // Error cases + if (p[3] != 0xff) { + *outError = + "Frame pixels must be either solid or transparent " + "(not intermediate alphas)"; + return TickType::kNone; + } + + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + *outError = "Ticks in transparent frame must be black or red"; + } + return TickType::kTick; + } + + if (p[3] != 0xFF) { + *outError = "White frame must be a solid color (no alpha)"; + } + if (color == kColorWhite) { + return TickType::kNone; + } + if (color == kColorTick) { + return TickType::kTick; + } + if (color == kColorLayoutBoundsTick) { + return TickType::kLayoutBounds; + } + + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + *outError = "Ticks in white frame must be black or red"; + return TickType::kNone; + } + return TickType::kTick; +} + +enum class TickState { kStart, kInside1, kOutside1 }; + +static bool getHorizontalTicks(png_bytep row, int width, bool transparent, bool required, + int32_t* outLeft, int32_t* outRight, const char** outError, + uint8_t* outDivs, bool multipleAllowed) { + *outLeft = *outRight = -1; + TickState state = TickState::kStart; + bool found = false; + + for (int i = 1; i < width - 1; i++) { + if (tickType(row + i * 4, transparent, outError) == TickType::kTick) { + if (state == TickState::kStart || (state == TickState::kOutside1 && multipleAllowed)) { + *outLeft = i - 1; + *outRight = width - 2; + found = true; + if (outDivs != NULL) { + *outDivs += 2; + } + state = TickState::kInside1; + } else if (state == TickState::kOutside1) { + *outError = "Can't have more than one marked region along edge"; + *outLeft = i; + return false; + } + } else if (!*outError) { + if (state == TickState::kInside1) { + // We're done with this div. Move on to the next. + *outRight = i - 1; + outRight += 2; + outLeft += 2; + state = TickState::kOutside1; + } + } else { + *outLeft = i; + return false; + } + } + + if (required && !found) { + *outError = "No marked region found along edge"; + *outLeft = -1; + return false; + } + return true; +} + +static bool getVerticalTicks(png_bytepp rows, int offset, int height, bool transparent, + bool required, int32_t* outTop, int32_t* outBottom, + const char** outError, uint8_t* outDivs, bool multipleAllowed) { + *outTop = *outBottom = -1; + TickState state = TickState::kStart; + bool found = false; + + for (int i = 1; i < height - 1; i++) { + if (tickType(rows[i] + offset, transparent, outError) == TickType::kTick) { + if (state == TickState::kStart || (state == TickState::kOutside1 && multipleAllowed)) { + *outTop = i - 1; + *outBottom = height - 2; + found = true; + if (outDivs != NULL) { + *outDivs += 2; + } + state = TickState::kInside1; + } else if (state == TickState::kOutside1) { + *outError = "Can't have more than one marked region along edge"; + *outTop = i; + return false; + } + } else if (!*outError) { + if (state == TickState::kInside1) { + // We're done with this div. Move on to the next. + *outBottom = i - 1; + outTop += 2; + outBottom += 2; + state = TickState::kOutside1; + } + } else { + *outTop = i; + return false; + } + } + + if (required && !found) { + *outError = "No marked region found along edge"; + *outTop = -1; + return false; + } + return true; +} + +static bool getHorizontalLayoutBoundsTicks(png_bytep row, int width, bool transparent, + bool /* required */, int32_t* outLeft, int32_t* outRight, + const char** outError) { + *outLeft = *outRight = 0; + + // Look for left tick + if (tickType(row + 4, transparent, outError) == TickType::kLayoutBounds) { + // Starting with a layout padding tick + int i = 1; + while (i < width - 1) { + (*outLeft)++; + i++; + if (tickType(row + i * 4, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + + // Look for right tick + if (tickType(row + (width - 2) * 4, transparent, outError) == TickType::kLayoutBounds) { + // Ending with a layout padding tick + int i = width - 2; + while (i > 1) { + (*outRight)++; + i--; + if (tickType(row + i * 4, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + return true; +} + +static bool getVerticalLayoutBoundsTicks(png_bytepp rows, int offset, int height, bool transparent, + bool /* required */, int32_t* outTop, int32_t* outBottom, + const char** outError) { + *outTop = *outBottom = 0; + + // Look for top tick + if (tickType(rows[1] + offset, transparent, outError) == TickType::kLayoutBounds) { + // Starting with a layout padding tick + int i = 1; + while (i < height - 1) { + (*outTop)++; + i++; + if (tickType(rows[i] + offset, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + + // Look for bottom tick + if (tickType(rows[height - 2] + offset, transparent, outError) == TickType::kLayoutBounds) { + // Ending with a layout padding tick + int i = height - 2; + while (i > 1) { + (*outBottom)++; + i--; + if (tickType(rows[i] + offset, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + return true; +} + +static void findMaxOpacity(png_bytepp rows, int startX, int startY, int endX, int endY, int dX, + int dY, int* outInset) { + uint8_t maxOpacity = 0; + int inset = 0; + *outInset = 0; + for (int x = startX, y = startY; x != endX && y != endY; x += dX, y += dY, inset++) { + png_byte* color = rows[y] + x * 4; + uint8_t opacity = color[3]; + if (opacity > maxOpacity) { + maxOpacity = opacity; + *outInset = inset; + } + if (opacity == 0xff) return; + } +} + +static uint8_t maxAlphaOverRow(png_bytep row, int startX, int endX) { + uint8_t maxAlpha = 0; + for (int x = startX; x < endX; x++) { + uint8_t alpha = (row + x * 4)[3]; + if (alpha > maxAlpha) maxAlpha = alpha; + } + return maxAlpha; +} + +static uint8_t maxAlphaOverCol(png_bytepp rows, int offsetX, int startY, int endY) { + uint8_t maxAlpha = 0; + for (int y = startY; y < endY; y++) { + uint8_t alpha = (rows[y] + offsetX * 4)[3]; + if (alpha > maxAlpha) maxAlpha = alpha; + } + return maxAlpha; +} + +static void getOutline(PngInfo* image) { + int midX = image->width / 2; + int midY = image->height / 2; + int endX = image->width - 2; + int endY = image->height - 2; + + // find left and right extent of nine patch content on center row + if (image->width > 4) { + findMaxOpacity(image->rows.data(), 1, midY, midX, -1, 1, 0, &image->outlineInsetsLeft); + findMaxOpacity(image->rows.data(), endX, midY, midX, -1, -1, 0, &image->outlineInsetsRight); + } else { + image->outlineInsetsLeft = 0; + image->outlineInsetsRight = 0; + } + + // find top and bottom extent of nine patch content on center column + if (image->height > 4) { + findMaxOpacity(image->rows.data(), midX, 1, -1, midY, 0, 1, &image->outlineInsetsTop); + findMaxOpacity(image->rows.data(), midX, endY, -1, midY, 0, -1, &image->outlineInsetsBottom); + } else { + image->outlineInsetsTop = 0; + image->outlineInsetsBottom = 0; + } + + int innerStartX = 1 + image->outlineInsetsLeft; + int innerStartY = 1 + image->outlineInsetsTop; + int innerEndX = endX - image->outlineInsetsRight; + int innerEndY = endY - image->outlineInsetsBottom; + int innerMidX = (innerEndX + innerStartX) / 2; + int innerMidY = (innerEndY + innerStartY) / 2; + + // assuming the image is a round rect, compute the radius by marching + // diagonally from the top left corner towards the center + image->outlineAlpha = + std::max(maxAlphaOverRow(image->rows[innerMidY], innerStartX, innerEndX), + maxAlphaOverCol(image->rows.data(), innerMidX, innerStartY, innerStartY)); + + int diagonalInset = 0; + findMaxOpacity(image->rows.data(), innerStartX, innerStartY, innerMidX, innerMidY, 1, 1, + &diagonalInset); + + /* Determine source radius based upon inset: + * sqrt(r^2 + r^2) = sqrt(i^2 + i^2) + r + * sqrt(2) * r = sqrt(2) * i + r + * (sqrt(2) - 1) * r = sqrt(2) * i + * r = sqrt(2) / (sqrt(2) - 1) * i + */ + image->outlineRadius = 3.4142f * diagonalInset; + + if (kDebug) { + printf("outline insets %d %d %d %d, rad %f, alpha %x\n", image->outlineInsetsLeft, + image->outlineInsetsTop, image->outlineInsetsRight, image->outlineInsetsBottom, + image->outlineRadius, image->outlineAlpha); + } +} + +static uint32_t getColor(png_bytepp rows, int left, int top, int right, int bottom) { + png_bytep color = rows[top] + left * 4; + + if (left > right || top > bottom) { + return Res_png_9patch::TRANSPARENT_COLOR; + } + + while (top <= bottom) { + for (int i = left; i <= right; i++) { + png_bytep p = rows[top] + i * 4; + if (color[3] == 0) { + if (p[3] != 0) { + return Res_png_9patch::NO_COLOR; + } + } else if (p[0] != color[0] || p[1] != color[1] || p[2] != color[2] || p[3] != color[3]) { + return Res_png_9patch::NO_COLOR; + } + } + top++; + } + + if (color[3] == 0) { + return Res_png_9patch::TRANSPARENT_COLOR; + } + return (color[3] << 24) | (color[0] << 16) | (color[1] << 8) | color[2]; +} + +static bool do9Patch(PngInfo* image, std::string* outError) { + image->is9Patch = true; + + int W = image->width; + int H = image->height; + int i, j; + + const int maxSizeXDivs = W * sizeof(int32_t); + const int maxSizeYDivs = H * sizeof(int32_t); + int32_t* xDivs = image->xDivs = new int32_t[W]; + int32_t* yDivs = image->yDivs = new int32_t[H]; + uint8_t numXDivs = 0; + uint8_t numYDivs = 0; + + int8_t numColors; + int numRows; + int numCols; + int top; + int left; + int right; + int bottom; + memset(xDivs, -1, maxSizeXDivs); + memset(yDivs, -1, maxSizeYDivs); + image->info9Patch.paddingLeft = image->info9Patch.paddingRight = -1; + image->info9Patch.paddingTop = image->info9Patch.paddingBottom = -1; + image->layoutBoundsLeft = image->layoutBoundsRight = 0; + image->layoutBoundsTop = image->layoutBoundsBottom = 0; + + png_bytep p = image->rows[0]; + bool transparent = p[3] == 0; + bool hasColor = false; + + const char* errorMsg = nullptr; + int errorPixel = -1; + const char* errorEdge = nullptr; + + int colorIndex = 0; + std::vector<png_bytep> newRows; + + // Validate size... + if (W < 3 || H < 3) { + errorMsg = "Image must be at least 3x3 (1x1 without frame) pixels"; + goto getout; + } + + // Validate frame... + if (!transparent && (p[0] != 0xFF || p[1] != 0xFF || p[2] != 0xFF || p[3] != 0xFF)) { + errorMsg = "Must have one-pixel frame that is either transparent or white"; + goto getout; + } + + // Find left and right of sizing areas... + if (!getHorizontalTicks(p, W, transparent, true, &xDivs[0], &xDivs[1], &errorMsg, &numXDivs, + true)) { + errorPixel = xDivs[0]; + errorEdge = "top"; + goto getout; + } + + // Find top and bottom of sizing areas... + if (!getVerticalTicks(image->rows.data(), 0, H, transparent, true, &yDivs[0], &yDivs[1], + &errorMsg, &numYDivs, true)) { + errorPixel = yDivs[0]; + errorEdge = "left"; + goto getout; + } + + // Copy patch size data into image... + image->info9Patch.numXDivs = numXDivs; + image->info9Patch.numYDivs = numYDivs; + + // Find left and right of padding area... + if (!getHorizontalTicks(image->rows[H - 1], W, transparent, false, &image->info9Patch.paddingLeft, + &image->info9Patch.paddingRight, &errorMsg, nullptr, false)) { + errorPixel = image->info9Patch.paddingLeft; + errorEdge = "bottom"; + goto getout; + } + + // Find top and bottom of padding area... + if (!getVerticalTicks(image->rows.data(), (W - 1) * 4, H, transparent, false, + &image->info9Patch.paddingTop, &image->info9Patch.paddingBottom, &errorMsg, + nullptr, false)) { + errorPixel = image->info9Patch.paddingTop; + errorEdge = "right"; + goto getout; + } + + // Find left and right of layout padding... + getHorizontalLayoutBoundsTicks(image->rows[H - 1], W, transparent, false, + &image->layoutBoundsLeft, &image->layoutBoundsRight, &errorMsg); + + getVerticalLayoutBoundsTicks(image->rows.data(), (W - 1) * 4, H, transparent, false, + &image->layoutBoundsTop, &image->layoutBoundsBottom, &errorMsg); + + image->haveLayoutBounds = image->layoutBoundsLeft != 0 || image->layoutBoundsRight != 0 || + image->layoutBoundsTop != 0 || image->layoutBoundsBottom != 0; + + if (image->haveLayoutBounds) { + if (kDebug) { + printf("layoutBounds=%d %d %d %d\n", image->layoutBoundsLeft, image->layoutBoundsTop, + image->layoutBoundsRight, image->layoutBoundsBottom); + } + } + + // use opacity of pixels to estimate the round rect outline + getOutline(image); + + // If padding is not yet specified, take values from size. + if (image->info9Patch.paddingLeft < 0) { + image->info9Patch.paddingLeft = xDivs[0]; + image->info9Patch.paddingRight = W - 2 - xDivs[1]; + } else { + // Adjust value to be correct! + image->info9Patch.paddingRight = W - 2 - image->info9Patch.paddingRight; + } + if (image->info9Patch.paddingTop < 0) { + image->info9Patch.paddingTop = yDivs[0]; + image->info9Patch.paddingBottom = H - 2 - yDivs[1]; + } else { + // Adjust value to be correct! + image->info9Patch.paddingBottom = H - 2 - image->info9Patch.paddingBottom; + } + + /* if (kDebug) { + printf("Size ticks for %s: x0=%d, x1=%d, y0=%d, y1=%d\n", imageName, + xDivs[0], xDivs[1], + yDivs[0], yDivs[1]); + printf("padding ticks for %s: l=%d, r=%d, t=%d, b=%d\n", imageName, + image->info9Patch.paddingLeft, image->info9Patch.paddingRight, + image->info9Patch.paddingTop, + image->info9Patch.paddingBottom); + }*/ + + // Remove frame from image. + newRows.resize(H - 2); + for (i = 0; i < H - 2; i++) { + newRows[i] = image->rows[i + 1]; + memmove(newRows[i], newRows[i] + 4, (W - 2) * 4); + } + image->rows.swap(newRows); + + image->width -= 2; + W = image->width; + image->height -= 2; + H = image->height; + + // Figure out the number of rows and columns in the N-patch + numCols = numXDivs + 1; + if (xDivs[0] == 0) { // Column 1 is strechable + numCols--; + } + if (xDivs[numXDivs - 1] == W) { + numCols--; + } + numRows = numYDivs + 1; + if (yDivs[0] == 0) { // Row 1 is strechable + numRows--; + } + if (yDivs[numYDivs - 1] == H) { + numRows--; + } + + // Make sure the amount of rows and columns will fit in the number of + // colors we can use in the 9-patch format. + if (numRows * numCols > 0x7F) { + errorMsg = "Too many rows and columns in 9-patch perimeter"; + goto getout; + } + + numColors = numRows * numCols; + image->info9Patch.numColors = numColors; + image->colors.resize(numColors); + + // Fill in color information for each patch. + + uint32_t c; + top = 0; + + // The first row always starts with the top being at y=0 and the bottom + // being either yDivs[1] (if yDivs[0]=0) of yDivs[0]. In the former case + // the first row is stretchable along the Y axis, otherwise it is fixed. + // The last row always ends with the bottom being bitmap.height and the top + // being either yDivs[numYDivs-2] (if yDivs[numYDivs-1]=bitmap.height) or + // yDivs[numYDivs-1]. In the former case the last row is stretchable along + // the Y axis, otherwise it is fixed. + // + // The first and last columns are similarly treated with respect to the X + // axis. + // + // The above is to help explain some of the special casing that goes on the + // code below. + + // The initial yDiv and whether the first row is considered stretchable or + // not depends on whether yDiv[0] was zero or not. + for (j = (yDivs[0] == 0 ? 1 : 0); j <= numYDivs && top < H; j++) { + if (j == numYDivs) { + bottom = H; + } else { + bottom = yDivs[j]; + } + left = 0; + // The initial xDiv and whether the first column is considered + // stretchable or not depends on whether xDiv[0] was zero or not. + for (i = xDivs[0] == 0 ? 1 : 0; i <= numXDivs && left < W; i++) { + if (i == numXDivs) { + right = W; + } else { + right = xDivs[i]; + } + c = getColor(image->rows.data(), left, top, right - 1, bottom - 1); + image->colors[colorIndex++] = c; + if (kDebug) { + if (c != Res_png_9patch::NO_COLOR) { + hasColor = true; + } + } + left = right; + } + top = bottom; + } + + assert(colorIndex == numColors); + + if (kDebug && hasColor) { + for (i = 0; i < numColors; i++) { + if (i == 0) printf("Colors:\n"); + printf(" #%08x", image->colors[i]); + if (i == numColors - 1) printf("\n"); + } + } +getout: + if (errorMsg) { + std::stringstream err; + err << "9-patch malformed: " << errorMsg; + if (errorEdge) { + err << "." << std::endl; + if (errorPixel >= 0) { + err << "Found at pixel #" << errorPixel << " along " << errorEdge << " edge"; + } else { + err << "Found along " << errorEdge << " edge"; + } + } + *outError = err.str(); + return false; + } + return true; +} + +bool Png::process(const Source& source, std::istream* input, BigBuffer* outBuffer, + const PngOptions& options) { + png_byte signature[kPngSignatureSize]; + + // Read the PNG signature first. + if (!input->read(reinterpret_cast<char*>(signature), kPngSignatureSize)) { + mDiag->Error(DiagMessage() << strerror(errno)); + return false; + } + + // If the PNG signature doesn't match, bail early. + if (png_sig_cmp(signature, 0, kPngSignatureSize) != 0) { + mDiag->Error(DiagMessage() << "not a valid png file"); + return false; + } + + bool result = false; + png_structp readPtr = nullptr; + png_infop infoPtr = nullptr; + png_structp writePtr = nullptr; + png_infop writeInfoPtr = nullptr; + PngInfo pngInfo = {}; + + readPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, nullptr, nullptr); + if (!readPtr) { + mDiag->Error(DiagMessage() << "failed to allocate read ptr"); + goto bail; + } + + infoPtr = png_create_info_struct(readPtr); + if (!infoPtr) { + mDiag->Error(DiagMessage() << "failed to allocate info ptr"); + goto bail; + } + + png_set_error_fn(readPtr, reinterpret_cast<png_voidp>(mDiag), nullptr, logWarning); + + // Set the read function to read from std::istream. + png_set_read_fn(readPtr, (png_voidp)input, readDataFromStream); + + if (!readPng(mDiag, readPtr, infoPtr, &pngInfo)) { + goto bail; + } + + if (android::base::EndsWith(source.path, ".9.png")) { + std::string errorMsg; + if (!do9Patch(&pngInfo, &errorMsg)) { + mDiag->Error(DiagMessage() << errorMsg); + goto bail; + } + } + + writePtr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, nullptr, nullptr); + if (!writePtr) { + mDiag->Error(DiagMessage() << "failed to allocate write ptr"); + goto bail; + } + + writeInfoPtr = png_create_info_struct(writePtr); + if (!writeInfoPtr) { + mDiag->Error(DiagMessage() << "failed to allocate write info ptr"); + goto bail; + } + + png_set_error_fn(writePtr, nullptr, nullptr, logWarning); + + // Set the write function to write to std::ostream. + png_set_write_fn(writePtr, (png_voidp)outBuffer, writeDataToStream, flushDataToStream); + + if (!writePng(mDiag, writePtr, writeInfoPtr, &pngInfo, options.grayscale_tolerance)) { + goto bail; + } + + result = true; +bail: + if (readPtr) { + png_destroy_read_struct(&readPtr, &infoPtr, nullptr); + } + + if (writePtr) { + png_destroy_write_struct(&writePtr, &writeInfoPtr); + } + return result; +} + +} // namespace android diff --git a/libs/androidfw/PngChunkFilter.cpp b/libs/androidfw/PngChunkFilter.cpp new file mode 100644 index 000000000000..331b94803e93 --- /dev/null +++ b/libs/androidfw/PngChunkFilter.cpp @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2016 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. + */ + +#include "android-base/stringprintf.h" +#include "android-base/strings.h" +#include "androidfw/Png.h" +#include "androidfw/Streams.h" +#include "androidfw/StringPiece.h" + +using ::android::base::StringPrintf; + +namespace android { + +static constexpr const char* kPngSignature = "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"; + +// Useful helper function that encodes individual bytes into a uint32 +// at compile time. +constexpr uint32_t u32(uint8_t a, uint8_t b, uint8_t c, uint8_t d) { + return (((uint32_t)a) << 24) | (((uint32_t)b) << 16) | (((uint32_t)c) << 8) | ((uint32_t)d); +} + +// Allow list of PNG chunk types that we want to keep in the resulting PNG. +enum PngChunkTypes { + kPngChunkIHDR = u32(73, 72, 68, 82), + kPngChunkIDAT = u32(73, 68, 65, 84), + kPngChunkIEND = u32(73, 69, 78, 68), + kPngChunkPLTE = u32(80, 76, 84, 69), + kPngChunktRNS = u32(116, 82, 78, 83), + kPngChunksRGB = u32(115, 82, 71, 66), +}; + +static uint32_t Peek32LE(const char* data) { + uint32_t word = ((uint32_t)data[0]) & 0x000000ff; + word <<= 8; + word |= ((uint32_t)data[1]) & 0x000000ff; + word <<= 8; + word |= ((uint32_t)data[2]) & 0x000000ff; + word <<= 8; + word |= ((uint32_t)data[3]) & 0x000000ff; + return word; +} + +static bool IsPngChunkAllowed(uint32_t type) { + switch (type) { + case kPngChunkIHDR: + case kPngChunkIDAT: + case kPngChunkIEND: + case kPngChunkPLTE: + case kPngChunktRNS: + case kPngChunksRGB: + return true; + default: + return false; + } +} + +PngChunkFilter::PngChunkFilter(StringPiece data) : data_(data) { + if (android::base::StartsWith(data_, kPngSignature)) { + window_start_ = 0; + window_end_ = kPngSignatureSize; + } else { + error_msg_ = "file does not start with PNG signature"; + } +} + +bool PngChunkFilter::ConsumeWindow(const void** buffer, size_t* len) { + if (window_start_ != window_end_) { + // We have bytes to give from our window. + const size_t bytes_read = window_end_ - window_start_; + *buffer = data_.data() + window_start_; + *len = bytes_read; + window_start_ = window_end_; + return true; + } + return false; +} + +bool PngChunkFilter::Next(const void** buffer, size_t* len) { + if (HadError()) { + return false; + } + + // In case BackUp was called, we must consume the window. + if (ConsumeWindow(buffer, len)) { + return true; + } + + // Advance the window as far as possible (until we meet a chunk that + // we want to strip). + while (window_end_ < data_.size()) { + // Chunk length (4 bytes) + type (4 bytes) + crc32 (4 bytes) = 12 bytes. + const size_t kMinChunkHeaderSize = 3 * sizeof(uint32_t); + + // Is there enough room for a chunk header? + if (data_.size() - window_end_ < kMinChunkHeaderSize) { + error_msg_ = StringPrintf("Not enough space for a PNG chunk @ byte %zu/%zu", window_end_, + data_.size()); + return false; + } + + // Verify the chunk length. + const uint32_t chunk_len = Peek32LE(data_.data() + window_end_); + if ((size_t)chunk_len > data_.size() - window_end_ - kMinChunkHeaderSize) { + // Overflow. + const uint32_t chunk_type = Peek32LE(data_.data() + window_end_ + sizeof(uint32_t)); + error_msg_ = StringPrintf( + "PNG chunk type %08x is too large: chunk length is %zu but chunk " + "starts at byte %zu/%zu", + chunk_type, (size_t)chunk_len, window_end_ + kMinChunkHeaderSize, data_.size()); + return false; + } + + // Do we strip this chunk? + const uint32_t chunk_type = Peek32LE(data_.data() + window_end_ + sizeof(uint32_t)); + if (IsPngChunkAllowed(chunk_type)) { + // Advance the window to include this chunk. + window_end_ += kMinChunkHeaderSize + chunk_len; + + // Special case the IEND chunk, which MUST appear last and libpng stops parsing once it hits + // such a chunk (let's do the same). + if (chunk_type == kPngChunkIEND) { + // Truncate the data to the end of this chunk. There may be garbage trailing after + // (b/38169876) + data_ = data_.substr(0, window_end_); + break; + } + + } else { + // We want to strip this chunk. If we accumulated a window, + // we must return the window now. + if (window_start_ != window_end_) { + break; + } + + // The window is empty, so we can advance past this chunk + // and keep looking for the next good chunk, + window_end_ += kMinChunkHeaderSize + chunk_len; + window_start_ = window_end_; + } + } + + if (ConsumeWindow(buffer, len)) { + return true; + } + return false; +} + +void PngChunkFilter::BackUp(size_t count) { + if (HadError()) { + return; + } + window_start_ -= count; +} + +bool PngChunkFilter::Rewind() { + if (HadError()) { + return false; + } + window_start_ = 0; + window_end_ = kPngSignatureSize; + return true; +} +} // namespace android diff --git a/libs/androidfw/PngCrunch.cpp b/libs/androidfw/PngCrunch.cpp new file mode 100644 index 000000000000..cf3c0eeff402 --- /dev/null +++ b/libs/androidfw/PngCrunch.cpp @@ -0,0 +1,730 @@ +/* + * Copyright (C) 2016 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. + */ + +#include <png.h> +#include <zlib.h> + +#include <algorithm> +#include <unordered_map> +#include <unordered_set> + +#include "android-base/errors.h" +#include "android-base/logging.h" +#include "android-base/macros.h" +#include "androidfw/Png.h" + +namespace android { + +// Custom deleter that destroys libpng read and info structs. +class PngReadStructDeleter { + public: + PngReadStructDeleter(png_structp read_ptr, png_infop info_ptr) + : read_ptr_(read_ptr), info_ptr_(info_ptr) { + } + + ~PngReadStructDeleter() { + png_destroy_read_struct(&read_ptr_, &info_ptr_, nullptr); + } + + private: + png_structp read_ptr_; + png_infop info_ptr_; + + DISALLOW_COPY_AND_ASSIGN(PngReadStructDeleter); +}; + +// Custom deleter that destroys libpng write and info structs. +class PngWriteStructDeleter { + public: + PngWriteStructDeleter(png_structp write_ptr, png_infop info_ptr) + : write_ptr_(write_ptr), info_ptr_(info_ptr) { + } + + ~PngWriteStructDeleter() { + png_destroy_write_struct(&write_ptr_, &info_ptr_); + } + + private: + png_structp write_ptr_; + png_infop info_ptr_; + + DISALLOW_COPY_AND_ASSIGN(PngWriteStructDeleter); +}; + +// Custom warning logging method that uses IDiagnostics. +static void LogWarning(png_structp png_ptr, png_const_charp warning_msg) { + android::IDiagnostics* diag = (android::IDiagnostics*)png_get_error_ptr(png_ptr); + diag->Warn(android::DiagMessage() << warning_msg); +} + +// Custom error logging method that uses IDiagnostics. +static void LogError(png_structp png_ptr, png_const_charp error_msg) { + android::IDiagnostics* diag = (android::IDiagnostics*)png_get_error_ptr(png_ptr); + diag->Error(android::DiagMessage() << error_msg); + + // Causes libpng to longjmp to the spot where setjmp was set. This is how libpng does + // error handling. If this custom error handler method were to return, libpng would, by + // default, print the error message to stdout and call the same png_longjmp method. + png_longjmp(png_ptr, 1); +} + +static void ReadDataFromStream(png_structp png_ptr, png_bytep buffer, png_size_t len) { + InputStream* in = (InputStream*)png_get_io_ptr(png_ptr); + + const void* in_buffer; + size_t in_len; + if (!in->Next(&in_buffer, &in_len)) { + if (in->HadError()) { + std::stringstream error_msg_builder; + error_msg_builder << "failed reading from input"; + if (!in->GetError().empty()) { + error_msg_builder << ": " << in->GetError(); + } + std::string err = error_msg_builder.str(); + png_error(png_ptr, err.c_str()); + } + return; + } + + const size_t bytes_read = std::min(in_len, len); + memcpy(buffer, in_buffer, bytes_read); + if (bytes_read != in_len) { + in->BackUp(in_len - bytes_read); + } +} + +static void WriteDataToStream(png_structp png_ptr, png_bytep buffer, png_size_t len) { + OutputStream* out = (OutputStream*)png_get_io_ptr(png_ptr); + + void* out_buffer; + size_t out_len; + while (len > 0) { + if (!out->Next(&out_buffer, &out_len)) { + if (out->HadError()) { + std::stringstream err_msg_builder; + err_msg_builder << "failed writing to output"; + if (!out->GetError().empty()) { + err_msg_builder << ": " << out->GetError(); + } + std::string err = out->GetError(); + png_error(png_ptr, err.c_str()); + } + return; + } + + const size_t bytes_written = std::min(out_len, len); + memcpy(out_buffer, buffer, bytes_written); + + // Advance the input buffer. + buffer += bytes_written; + len -= bytes_written; + + // Advance the output buffer. + out_len -= bytes_written; + } + + // If the entire output buffer wasn't used, backup. + if (out_len > 0) { + out->BackUp(out_len); + } +} + +std::unique_ptr<Image> ReadPng(InputStream* in, IDiagnostics* diag) { + // Read the first 8 bytes of the file looking for the PNG signature. + // Bail early if it does not match. + const png_byte* signature; + size_t buffer_size; + if (!in->Next((const void**)&signature, &buffer_size)) { + if (in->HadError()) { + diag->Error(android::DiagMessage() << "failed to read PNG signature: " << in->GetError()); + } else { + diag->Error(android::DiagMessage() << "not enough data for PNG signature"); + } + return {}; + } + + if (buffer_size < kPngSignatureSize || png_sig_cmp(signature, 0, kPngSignatureSize) != 0) { + diag->Error(android::DiagMessage() << "file signature does not match PNG signature"); + return {}; + } + + // Start at the beginning of the first chunk. + in->BackUp(buffer_size - kPngSignatureSize); + + // Create and initialize the png_struct with the default error and warning handlers. + // The header version is also passed in to ensure that this was built against the same + // version of libpng. + png_structp read_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (read_ptr == nullptr) { + diag->Error(android::DiagMessage() << "failed to create libpng read png_struct"); + return {}; + } + + // Create and initialize the memory for image header and data. + png_infop info_ptr = png_create_info_struct(read_ptr); + if (info_ptr == nullptr) { + diag->Error(android::DiagMessage() << "failed to create libpng read png_info"); + png_destroy_read_struct(&read_ptr, nullptr, nullptr); + return {}; + } + + // Automatically release PNG resources at end of scope. + PngReadStructDeleter png_read_deleter(read_ptr, info_ptr); + + // libpng uses longjmp to jump to an error handling routine. + // setjmp will only return true if it was jumped to, aka there was + // an error. + if (setjmp(png_jmpbuf(read_ptr))) { + return {}; + } + + // Handle warnings ourselves via IDiagnostics. + png_set_error_fn(read_ptr, (png_voidp)&diag, LogError, LogWarning); + + // Set up the read functions which read from our custom data sources. + png_set_read_fn(read_ptr, (png_voidp)in, ReadDataFromStream); + + // Skip the signature that we already read. + png_set_sig_bytes(read_ptr, kPngSignatureSize); + + // Read the chunk headers. + png_read_info(read_ptr, info_ptr); + + // Extract image meta-data from the various chunk headers. + uint32_t width, height; + int bit_depth, color_type, interlace_method, compression_method, filter_method; + png_get_IHDR(read_ptr, info_ptr, &width, &height, &bit_depth, &color_type, &interlace_method, + &compression_method, &filter_method); + + // When the image is read, expand it so that it is in RGBA 8888 format + // so that image handling is uniform. + + if (color_type == PNG_COLOR_TYPE_PALETTE) { + png_set_palette_to_rgb(read_ptr); + } + + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) { + png_set_expand_gray_1_2_4_to_8(read_ptr); + } + + if (png_get_valid(read_ptr, info_ptr, PNG_INFO_tRNS)) { + png_set_tRNS_to_alpha(read_ptr); + } + + if (bit_depth == 16) { + png_set_strip_16(read_ptr); + } + + if (!(color_type & PNG_COLOR_MASK_ALPHA)) { + png_set_add_alpha(read_ptr, 0xFF, PNG_FILLER_AFTER); + } + + if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { + png_set_gray_to_rgb(read_ptr); + } + + if (interlace_method != PNG_INTERLACE_NONE) { + png_set_interlace_handling(read_ptr); + } + + // Once all the options for reading have been set, we need to flush + // them to libpng. + png_read_update_info(read_ptr, info_ptr); + + // 9-patch uses int32_t to index images, so we cap the image dimensions to + // something + // that can always be represented by 9-patch. + if (width > std::numeric_limits<int32_t>::max() || height > std::numeric_limits<int32_t>::max()) { + diag->Error(android::DiagMessage() + << "PNG image dimensions are too large: " << width << "x" << height); + return {}; + } + + std::unique_ptr<Image> output_image = std::make_unique<Image>(); + output_image->width = static_cast<int32_t>(width); + output_image->height = static_cast<int32_t>(height); + + const size_t row_bytes = png_get_rowbytes(read_ptr, info_ptr); + CHECK(row_bytes == 4 * width); // RGBA + + // Allocate one large block to hold the image. + output_image->data = std::unique_ptr<uint8_t[]>(new uint8_t[height * row_bytes]); + + // Create an array of rows that index into the data block. + output_image->rows = std::unique_ptr<uint8_t*[]>(new uint8_t*[height]); + for (uint32_t h = 0; h < height; h++) { + output_image->rows[h] = output_image->data.get() + (h * row_bytes); + } + + // Actually read the image pixels. + png_read_image(read_ptr, output_image->rows.get()); + + // Finish reading. This will read any other chunks after the image data. + png_read_end(read_ptr, info_ptr); + + return output_image; +} + +// Experimentally chosen constant to be added to the overhead of using color type +// PNG_COLOR_TYPE_PALETTE to account for the uncompressability of the palette chunk. +// Without this, many small PNGs encoded with palettes are larger after compression than +// the same PNGs encoded as RGBA. +constexpr static const size_t kPaletteOverheadConstant = 1024u * 10u; + +// Pick a color type by which to encode the image, based on which color type will take +// the least amount of disk space. +// +// 9-patch images traditionally have not been encoded with palettes. +// The original rationale was to avoid dithering until after scaling, +// but I don't think this would be an issue with palettes. Either way, +// our naive size estimation tends to be wrong for small images like 9-patches +// and using palettes balloons the size of the resulting 9-patch. +// In order to not regress in size, restrict 9-patch to not use palettes. + +// The options are: +// +// - RGB +// - RGBA +// - RGB + cheap alpha +// - Color palette +// - Color palette + cheap alpha +// - Color palette + alpha palette +// - Grayscale +// - Grayscale + cheap alpha +// - Grayscale + alpha +// +static int PickColorType(int32_t width, int32_t height, bool grayscale, + bool convertible_to_grayscale, bool has_nine_patch, + size_t color_palette_size, size_t alpha_palette_size) { + const size_t palette_chunk_size = 16 + color_palette_size * 3; + const size_t alpha_chunk_size = 16 + alpha_palette_size; + const size_t color_alpha_data_chunk_size = 16 + 4 * width * height; + const size_t color_data_chunk_size = 16 + 3 * width * height; + const size_t grayscale_alpha_data_chunk_size = 16 + 2 * width * height; + const size_t palette_data_chunk_size = 16 + width * height; + + if (grayscale) { + if (alpha_palette_size == 0) { + // This is the smallest the data can be. + return PNG_COLOR_TYPE_GRAY; + } else if (color_palette_size <= 256 && !has_nine_patch) { + // This grayscale has alpha and can fit within a palette. + // See if it is worth fitting into a palette. + const size_t palette_threshold = palette_chunk_size + alpha_chunk_size + + palette_data_chunk_size + kPaletteOverheadConstant; + if (grayscale_alpha_data_chunk_size > palette_threshold) { + return PNG_COLOR_TYPE_PALETTE; + } + } + return PNG_COLOR_TYPE_GRAY_ALPHA; + } + + if (color_palette_size <= 256 && !has_nine_patch) { + // This image can fit inside a palette. Let's see if it is worth it. + size_t total_size_with_palette = palette_data_chunk_size + palette_chunk_size; + size_t total_size_without_palette = color_data_chunk_size; + if (alpha_palette_size > 0) { + total_size_with_palette += alpha_palette_size; + total_size_without_palette = color_alpha_data_chunk_size; + } + + if (total_size_without_palette > total_size_with_palette + kPaletteOverheadConstant) { + return PNG_COLOR_TYPE_PALETTE; + } + } + + if (convertible_to_grayscale) { + if (alpha_palette_size == 0) { + return PNG_COLOR_TYPE_GRAY; + } else { + return PNG_COLOR_TYPE_GRAY_ALPHA; + } + } + + if (alpha_palette_size == 0) { + return PNG_COLOR_TYPE_RGB; + } + return PNG_COLOR_TYPE_RGBA; +} + +// Assigns indices to the color and alpha palettes, encodes them, and then invokes +// png_set_PLTE/png_set_tRNS. +// This must be done before writing image data. +// Image data must be transformed to use the indices assigned within the palette. +static void WritePalette(png_structp write_ptr, png_infop write_info_ptr, + std::unordered_map<uint32_t, int>* color_palette, + std::unordered_set<uint32_t>* alpha_palette) { + CHECK(color_palette->size() <= 256); + CHECK(alpha_palette->size() <= 256); + + // Populate the PNG palette struct and assign indices to the color palette. + + // Colors in the alpha palette should have smaller indices. + // This will ensure that we can truncate the alpha palette if it is + // smaller than the color palette. + int index = 0; + for (uint32_t color : *alpha_palette) { + (*color_palette)[color] = index++; + } + + // Assign the rest of the entries. + for (auto& entry : *color_palette) { + if (entry.second == -1) { + entry.second = index++; + } + } + + // Create the PNG color palette struct. + auto color_palette_bytes = std::unique_ptr<png_color[]>(new png_color[color_palette->size()]); + + std::unique_ptr<png_byte[]> alpha_palette_bytes; + if (!alpha_palette->empty()) { + alpha_palette_bytes = std::unique_ptr<png_byte[]>(new png_byte[alpha_palette->size()]); + } + + for (const auto& entry : *color_palette) { + const uint32_t color = entry.first; + const int index = entry.second; + CHECK(index >= 0); + CHECK(static_cast<size_t>(index) < color_palette->size()); + + png_colorp slot = color_palette_bytes.get() + index; + slot->red = color >> 24; + slot->green = color >> 16; + slot->blue = color >> 8; + + const png_byte alpha = color & 0x000000ff; + if (alpha != 0xff && alpha_palette_bytes) { + CHECK(static_cast<size_t>(index) < alpha_palette->size()); + alpha_palette_bytes[index] = alpha; + } + } + + // The bytes get copied here, so it is safe to release color_palette_bytes at + // the end of function + // scope. + png_set_PLTE(write_ptr, write_info_ptr, color_palette_bytes.get(), color_palette->size()); + + if (alpha_palette_bytes) { + png_set_tRNS(write_ptr, write_info_ptr, alpha_palette_bytes.get(), alpha_palette->size(), + nullptr); + } +} + +// Write the 9-patch custom PNG chunks to write_info_ptr. This must be done +// before writing image data. +static void WriteNinePatch(png_structp write_ptr, png_infop write_info_ptr, + const NinePatch* nine_patch) { + // The order of the chunks is important. + // 9-patch code in older platforms expects the 9-patch chunk to be last. + + png_unknown_chunk unknown_chunks[3]; + memset(unknown_chunks, 0, sizeof(unknown_chunks)); + + size_t index = 0; + size_t chunk_len = 0; + + std::unique_ptr<uint8_t[]> serialized_outline = + nine_patch->SerializeRoundedRectOutline(&chunk_len); + strcpy((char*)unknown_chunks[index].name, "npOl"); + unknown_chunks[index].size = chunk_len; + unknown_chunks[index].data = (png_bytep)serialized_outline.get(); + unknown_chunks[index].location = PNG_HAVE_PLTE; + index++; + + std::unique_ptr<uint8_t[]> serialized_layout_bounds; + if (nine_patch->layout_bounds.nonZero()) { + serialized_layout_bounds = nine_patch->SerializeLayoutBounds(&chunk_len); + strcpy((char*)unknown_chunks[index].name, "npLb"); + unknown_chunks[index].size = chunk_len; + unknown_chunks[index].data = (png_bytep)serialized_layout_bounds.get(); + unknown_chunks[index].location = PNG_HAVE_PLTE; + index++; + } + + std::unique_ptr<uint8_t[]> serialized_nine_patch = nine_patch->SerializeBase(&chunk_len); + strcpy((char*)unknown_chunks[index].name, "npTc"); + unknown_chunks[index].size = chunk_len; + unknown_chunks[index].data = (png_bytep)serialized_nine_patch.get(); + unknown_chunks[index].location = PNG_HAVE_PLTE; + index++; + + // Handle all unknown chunks. We are manually setting the chunks here, + // so we will only ever handle our custom chunks. + png_set_keep_unknown_chunks(write_ptr, PNG_HANDLE_CHUNK_ALWAYS, nullptr, 0); + + // Set the actual chunks here. The data gets copied, so our buffers can + // safely go out of scope. + png_set_unknown_chunks(write_ptr, write_info_ptr, unknown_chunks, index); +} + +bool WritePng(const Image* image, const NinePatch* nine_patch, OutputStream* out, + const PngOptions& options, IDiagnostics* diag, bool verbose) { + // Create and initialize the write png_struct with the default error and + // warning handlers. + // The header version is also passed in to ensure that this was built against the same + // version of libpng. + png_structp write_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (write_ptr == nullptr) { + diag->Error(android::DiagMessage() << "failed to create libpng write png_struct"); + return false; + } + + // Allocate memory to store image header data. + png_infop write_info_ptr = png_create_info_struct(write_ptr); + if (write_info_ptr == nullptr) { + diag->Error(android::DiagMessage() << "failed to create libpng write png_info"); + png_destroy_write_struct(&write_ptr, nullptr); + return false; + } + + // Automatically release PNG resources at end of scope. + PngWriteStructDeleter png_write_deleter(write_ptr, write_info_ptr); + + // libpng uses longjmp to jump to error handling routines. + // setjmp will return true only if it was jumped to, aka, there was an error. + if (setjmp(png_jmpbuf(write_ptr))) { + return false; + } + + // Handle warnings with our IDiagnostics. + png_set_error_fn(write_ptr, (png_voidp)&diag, LogError, LogWarning); + + // Set up the write functions which write to our custom data sources. + png_set_write_fn(write_ptr, (png_voidp)out, WriteDataToStream, nullptr); + + // We want small files and can take the performance hit to achieve this goal. + png_set_compression_level(write_ptr, Z_BEST_COMPRESSION); + + // Begin analysis of the image data. + // Scan the entire image and determine if: + // 1. Every pixel has R == G == B (grayscale) + // 2. Every pixel has A == 255 (opaque) + // 3. There are no more than 256 distinct RGBA colors (palette). + std::unordered_map<uint32_t, int> color_palette; + std::unordered_set<uint32_t> alpha_palette; + bool needs_to_zero_rgb_channels_of_transparent_pixels = false; + bool grayscale = true; + int max_gray_deviation = 0; + + for (int32_t y = 0; y < image->height; y++) { + const uint8_t* row = image->rows[y]; + for (int32_t x = 0; x < image->width; x++) { + int red = *row++; + int green = *row++; + int blue = *row++; + int alpha = *row++; + + if (alpha == 0) { + // The color is completely transparent. + // For purposes of palettes and grayscale optimization, + // treat all channels as 0x00. + needs_to_zero_rgb_channels_of_transparent_pixels = + needs_to_zero_rgb_channels_of_transparent_pixels || + (red != 0 || green != 0 || blue != 0); + red = green = blue = 0; + } + + // Insert the color into the color palette. + const uint32_t color = red << 24 | green << 16 | blue << 8 | alpha; + color_palette[color] = -1; + + // If the pixel has non-opaque alpha, insert it into the + // alpha palette. + if (alpha != 0xff) { + alpha_palette.insert(color); + } + + // Check if the image is indeed grayscale. + if (grayscale) { + if (red != green || red != blue) { + grayscale = false; + } + } + + // Calculate the gray scale deviation so that it can be compared + // with the threshold. + max_gray_deviation = std::max(std::abs(red - green), max_gray_deviation); + max_gray_deviation = std::max(std::abs(green - blue), max_gray_deviation); + max_gray_deviation = std::max(std::abs(blue - red), max_gray_deviation); + } + } + + if (verbose) { + android::DiagMessage msg; + msg << " paletteSize=" << color_palette.size() << " alphaPaletteSize=" << alpha_palette.size() + << " maxGrayDeviation=" << max_gray_deviation + << " grayScale=" << (grayscale ? "true" : "false"); + diag->Note(msg); + } + + const bool convertible_to_grayscale = max_gray_deviation <= options.grayscale_tolerance; + + const int new_color_type = + PickColorType(image->width, image->height, grayscale, convertible_to_grayscale, + nine_patch != nullptr, color_palette.size(), alpha_palette.size()); + + if (verbose) { + android::DiagMessage msg; + msg << "encoding PNG "; + if (nine_patch) { + msg << "(with 9-patch) as "; + } + switch (new_color_type) { + case PNG_COLOR_TYPE_GRAY: + msg << "GRAY"; + break; + case PNG_COLOR_TYPE_GRAY_ALPHA: + msg << "GRAY + ALPHA"; + break; + case PNG_COLOR_TYPE_RGB: + msg << "RGB"; + break; + case PNG_COLOR_TYPE_RGB_ALPHA: + msg << "RGBA"; + break; + case PNG_COLOR_TYPE_PALETTE: + msg << "PALETTE"; + break; + default: + msg << "unknown type " << new_color_type; + break; + } + diag->Note(msg); + } + + png_set_IHDR(write_ptr, write_info_ptr, image->width, image->height, 8, new_color_type, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + if (new_color_type & PNG_COLOR_MASK_PALETTE) { + // Assigns indices to the palette, and writes the encoded palette to the + // libpng writePtr. + WritePalette(write_ptr, write_info_ptr, &color_palette, &alpha_palette); + png_set_filter(write_ptr, 0, PNG_NO_FILTERS); + } else { + png_set_filter(write_ptr, 0, PNG_ALL_FILTERS); + } + + if (nine_patch) { + WriteNinePatch(write_ptr, write_info_ptr, nine_patch); + } + + // Flush our updates to the header. + png_write_info(write_ptr, write_info_ptr); + + // Write out each row of image data according to its encoding. + if (new_color_type == PNG_COLOR_TYPE_PALETTE) { + // 1 byte/pixel. + auto out_row = std::unique_ptr<png_byte[]>(new png_byte[image->width]); + + for (int32_t y = 0; y < image->height; y++) { + png_const_bytep in_row = image->rows[y]; + for (int32_t x = 0; x < image->width; x++) { + int rr = *in_row++; + int gg = *in_row++; + int bb = *in_row++; + int aa = *in_row++; + if (aa == 0) { + // Zero out color channels when transparent. + rr = gg = bb = 0; + } + + const uint32_t color = rr << 24 | gg << 16 | bb << 8 | aa; + const int idx = color_palette[color]; + CHECK(idx != -1); + out_row[x] = static_cast<png_byte>(idx); + } + png_write_row(write_ptr, out_row.get()); + } + } else if (new_color_type == PNG_COLOR_TYPE_GRAY || new_color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { + const size_t bpp = new_color_type == PNG_COLOR_TYPE_GRAY ? 1 : 2; + auto out_row = std::unique_ptr<png_byte[]>(new png_byte[image->width * bpp]); + + for (int32_t y = 0; y < image->height; y++) { + png_const_bytep in_row = image->rows[y]; + for (int32_t x = 0; x < image->width; x++) { + int rr = in_row[x * 4]; + int gg = in_row[x * 4 + 1]; + int bb = in_row[x * 4 + 2]; + int aa = in_row[x * 4 + 3]; + if (aa == 0) { + // Zero out the gray channel when transparent. + rr = gg = bb = 0; + } + + if (grayscale) { + // The image was already grayscale, red == green == blue. + out_row[x * bpp] = in_row[x * 4]; + } else { + // The image is convertible to grayscale, use linear-luminance of + // sRGB colorspace: + // https://en.wikipedia.org/wiki/Grayscale#Colorimetric_.28luminance-preserving.29_conversion_to_grayscale + out_row[x * bpp] = (png_byte)(rr * 0.2126f + gg * 0.7152f + bb * 0.0722f); + } + + if (bpp == 2) { + // Write out alpha if we have it. + out_row[x * bpp + 1] = aa; + } + } + png_write_row(write_ptr, out_row.get()); + } + } else if (new_color_type == PNG_COLOR_TYPE_RGB || new_color_type == PNG_COLOR_TYPE_RGBA) { + const size_t bpp = new_color_type == PNG_COLOR_TYPE_RGB ? 3 : 4; + if (needs_to_zero_rgb_channels_of_transparent_pixels) { + // The source RGBA data can't be used as-is, because we need to zero out + // the RGB values of transparent pixels. + auto out_row = std::unique_ptr<png_byte[]>(new png_byte[image->width * bpp]); + + for (int32_t y = 0; y < image->height; y++) { + png_const_bytep in_row = image->rows[y]; + for (int32_t x = 0; x < image->width; x++) { + int rr = *in_row++; + int gg = *in_row++; + int bb = *in_row++; + int aa = *in_row++; + if (aa == 0) { + // Zero out the RGB channels when transparent. + rr = gg = bb = 0; + } + out_row[x * bpp] = rr; + out_row[x * bpp + 1] = gg; + out_row[x * bpp + 2] = bb; + if (bpp == 4) { + out_row[x * bpp + 3] = aa; + } + } + png_write_row(write_ptr, out_row.get()); + } + } else { + // The source image can be used as-is, just tell libpng whether or not to + // ignore the alpha channel. + if (new_color_type == PNG_COLOR_TYPE_RGB) { + // Delete the extraneous alpha values that we appended to our buffer + // when reading the original values. + png_set_filler(write_ptr, 0, PNG_FILLER_AFTER); + } + png_write_image(write_ptr, image->rows.get()); + } + } else { + LOG(FATAL) << "unreachable"; + } + + png_write_end(write_ptr, write_info_ptr); + return true; +} + +} // namespace android diff --git a/libs/androidfw/include/androidfw/BigBufferStream.h b/libs/androidfw/include/androidfw/BigBufferStream.h new file mode 100644 index 000000000000..e55fe0d653cc --- /dev/null +++ b/libs/androidfw/include/androidfw/BigBufferStream.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2017 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. + */ + +#pragma once + +#include "BigBuffer.h" +#include "Streams.h" + +namespace android { + +class BigBufferInputStream : public KnownSizeInputStream { + public: + inline explicit BigBufferInputStream(const BigBuffer* buffer) + : buffer_(buffer), iter_(buffer->begin()) { + } + virtual ~BigBufferInputStream() = default; + + bool Next(const void** data, size_t* size) override; + + void BackUp(size_t count) override; + + bool CanRewind() const override; + + bool Rewind() override; + + size_t ByteCount() const override; + + bool HadError() const override; + + size_t TotalSize() const override; + + bool ReadFullyAtOffset(void* data, size_t byte_count, off64_t offset) override; + + private: + DISALLOW_COPY_AND_ASSIGN(BigBufferInputStream); + + const BigBuffer* buffer_; + BigBuffer::const_iterator iter_; + size_t offset_ = 0; + size_t bytes_read_ = 0; +}; + +class BigBufferOutputStream : public OutputStream { + public: + inline explicit BigBufferOutputStream(BigBuffer* buffer) : buffer_(buffer) { + } + virtual ~BigBufferOutputStream() = default; + + bool Next(void** data, size_t* size) override; + + void BackUp(size_t count) override; + + size_t ByteCount() const override; + + bool HadError() const override; + + private: + DISALLOW_COPY_AND_ASSIGN(BigBufferOutputStream); + + BigBuffer* buffer_; +}; + +} // namespace android
\ No newline at end of file diff --git a/libs/androidfw/include/androidfw/FileStream.h b/libs/androidfw/include/androidfw/FileStream.h new file mode 100644 index 000000000000..fb84a91a00de --- /dev/null +++ b/libs/androidfw/include/androidfw/FileStream.h @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2017 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. + */ + +#pragma once + +#include <memory> +#include <string> + +#include "Streams.h" +#include "android-base/macros.h" +#include "android-base/unique_fd.h" + +namespace android { + +constexpr size_t kDefaultBufferCapacity = 4096u; + +class FileInputStream : public InputStream { + public: + explicit FileInputStream(const std::string& path, + size_t buffer_capacity = kDefaultBufferCapacity); + + // Take ownership of `fd`. + explicit FileInputStream(int fd, size_t buffer_capacity = kDefaultBufferCapacity); + + bool Next(const void** data, size_t* size) override; + + void BackUp(size_t count) override; + + size_t ByteCount() const override; + + bool HadError() const override; + + std::string GetError() const override; + + bool ReadFullyAtOffset(void* data, size_t byte_count, off64_t offset) override; + + private: + DISALLOW_COPY_AND_ASSIGN(FileInputStream); + + android::base::unique_fd fd_; + std::string error_; + std::unique_ptr<uint8_t[]> buffer_; + size_t buffer_capacity_ = 0u; + size_t buffer_offset_ = 0u; + size_t buffer_size_ = 0u; + size_t total_byte_count_ = 0u; +}; + +class FileOutputStream : public OutputStream { + public: + explicit FileOutputStream(const std::string& path, + size_t buffer_capacity = kDefaultBufferCapacity); + + // Does not take ownership of `fd`. + explicit FileOutputStream(int fd, size_t buffer_capacity = kDefaultBufferCapacity); + + // Takes ownership of `fd`. + explicit FileOutputStream(android::base::unique_fd fd, + size_t buffer_capacity = kDefaultBufferCapacity); + + ~FileOutputStream(); + + bool Next(void** data, size_t* size) override; + + // Immediately flushes out the contents of the buffer to disk. + bool Flush(); + + void BackUp(size_t count) override; + + size_t ByteCount() const override; + + bool HadError() const override; + + std::string GetError() const override; + + private: + DISALLOW_COPY_AND_ASSIGN(FileOutputStream); + + bool FlushImpl(); + + android::base::unique_fd owned_fd_; + int fd_; + std::string error_; + std::unique_ptr<uint8_t[]> buffer_; + size_t buffer_capacity_ = 0u; + size_t buffer_offset_ = 0u; + size_t total_byte_count_ = 0u; +}; + +} // namespace android
\ No newline at end of file diff --git a/libs/androidfw/include/androidfw/Image.h b/libs/androidfw/include/androidfw/Image.h new file mode 100644 index 000000000000..c18c34c25bf9 --- /dev/null +++ b/libs/androidfw/include/androidfw/Image.h @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2016 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. + */ + +#pragma once + +#include <cstdint> +#include <memory> +#include <string> +#include <vector> + +#include "android-base/macros.h" + +namespace android { + +/** + * An in-memory image, loaded from disk, with pixels in RGBA_8888 format. + */ +class Image { + public: + explicit Image() = default; + + /** + * A `height` sized array of pointers, where each element points to a + * `width` sized row of RGBA_8888 pixels. + */ + std::unique_ptr<uint8_t*[]> rows; + + /** + * The width of the image in RGBA_8888 pixels. This is int32_t because of + * 9-patch data + * format limitations. + */ + int32_t width = 0; + + /** + * The height of the image in RGBA_8888 pixels. This is int32_t because of + * 9-patch data + * format limitations. + */ + int32_t height = 0; + + /** + * Buffer to the raw image data stored sequentially. + * Use `rows` to access the data on a row-by-row basis. + */ + std::unique_ptr<uint8_t[]> data; + + private: + DISALLOW_COPY_AND_ASSIGN(Image); +}; + +/** + * A range of pixel values, starting at 'start' and ending before 'end' + * exclusive. Or rather [a, b). + */ +struct Range { + int32_t start = 0; + int32_t end = 0; + + explicit Range() = default; + inline explicit Range(int32_t s, int32_t e) : start(s), end(e) { + } +}; + +inline bool operator==(const Range& left, const Range& right) { + return left.start == right.start && left.end == right.end; +} + +/** + * Inset lengths from all edges of a rectangle. `left` and `top` are measured + * from the left and top + * edges, while `right` and `bottom` are measured from the right and bottom + * edges, respectively. + */ +struct Bounds { + int32_t left = 0; + int32_t top = 0; + int32_t right = 0; + int32_t bottom = 0; + + explicit Bounds() = default; + inline explicit Bounds(int32_t l, int32_t t, int32_t r, int32_t b) + : left(l), top(t), right(r), bottom(b) { + } + + bool nonZero() const; +}; + +inline bool Bounds::nonZero() const { + return left != 0 || top != 0 || right != 0 || bottom != 0; +} + +inline bool operator==(const Bounds& left, const Bounds& right) { + return left.left == right.left && left.top == right.top && left.right == right.right && + left.bottom == right.bottom; +} + +/** + * Contains 9-patch data from a source image. All measurements exclude the 1px + * border of the + * source 9-patch image. + */ +class NinePatch { + public: + static std::unique_ptr<NinePatch> Create(uint8_t** rows, const int32_t width, + const int32_t height, std::string* err_out); + + /** + * Packs the RGBA_8888 data pointed to by pixel into a uint32_t + * with format 0xAARRGGBB (the way 9-patch expects it). + */ + static uint32_t PackRGBA(const uint8_t* pixel); + + /** + * 9-patch content padding/insets. All positions are relative to the 9-patch + * NOT including the 1px thick source border. + */ + Bounds padding; + + /** + * Optical layout bounds/insets. This overrides the padding for + * layout purposes. All positions are relative to the 9-patch + * NOT including the 1px thick source border. + * See + * https://developer.android.com/about/versions/android-4.3.html#OpticalBounds + */ + Bounds layout_bounds; + + /** + * Outline of the image, calculated based on opacity. + */ + Bounds outline; + + /** + * The computed radius of the outline. If non-zero, the outline is a + * rounded-rect. + */ + float outline_radius = 0.0f; + + /** + * The largest alpha value within the outline. + */ + uint32_t outline_alpha = 0x000000ffu; + + /** + * Horizontal regions of the image that are stretchable. + * All positions are relative to the 9-patch + * NOT including the 1px thick source border. + */ + std::vector<Range> horizontal_stretch_regions; + + /** + * Vertical regions of the image that are stretchable. + * All positions are relative to the 9-patch + * NOT including the 1px thick source border. + */ + std::vector<Range> vertical_stretch_regions; + + /** + * The colors within each region, fixed or stretchable. + * For w*h regions, the color of region (x,y) is addressable + * via index y*w + x. + */ + std::vector<uint32_t> region_colors; + + /** + * Returns serialized data containing the original basic 9-patch meta data. + * Optical layout bounds and round rect outline data must be serialized + * separately using SerializeOpticalLayoutBounds() and + * SerializeRoundedRectOutline(). + */ + std::unique_ptr<uint8_t[]> SerializeBase(size_t* out_len) const; + + /** + * Serializes the layout bounds. + */ + std::unique_ptr<uint8_t[]> SerializeLayoutBounds(size_t* out_len) const; + + /** + * Serializes the rounded-rect outline. + */ + std::unique_ptr<uint8_t[]> SerializeRoundedRectOutline(size_t* out_len) const; + + private: + explicit NinePatch() = default; + + DISALLOW_COPY_AND_ASSIGN(NinePatch); +}; + +::std::ostream& operator<<(::std::ostream& out, const Range& range); +::std::ostream& operator<<(::std::ostream& out, const Bounds& bounds); +::std::ostream& operator<<(::std::ostream& out, const NinePatch& nine_patch); + +} // namespace android
\ No newline at end of file diff --git a/libs/androidfw/include/androidfw/Png.h b/libs/androidfw/include/androidfw/Png.h new file mode 100644 index 000000000000..2ece43e08110 --- /dev/null +++ b/libs/androidfw/include/androidfw/Png.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2015 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. + */ + +#pragma once + +#include <string> + +#include "BigBuffer.h" +#include "IDiagnostics.h" +#include "Image.h" +#include "Source.h" +#include "Streams.h" +#include "android-base/macros.h" + +namespace android { +// Size in bytes of the PNG signature. +constexpr size_t kPngSignatureSize = 8u; + +struct PngOptions { + int grayscale_tolerance = 0; +}; + +/** + * Deprecated. Removing once new PNG crunching code is proved to be correct. + */ +class Png { + public: + explicit Png(IDiagnostics* diag) : mDiag(diag) { + } + + bool process(const Source& source, std::istream* input, BigBuffer* outBuffer, + const PngOptions& options); + + private: + DISALLOW_COPY_AND_ASSIGN(Png); + + IDiagnostics* mDiag; +}; + +/** + * An InputStream that filters out unimportant PNG chunks. + */ +class PngChunkFilter : public InputStream { + public: + explicit PngChunkFilter(StringPiece data); + virtual ~PngChunkFilter() = default; + + bool Next(const void** buffer, size_t* len) override; + void BackUp(size_t count) override; + + bool CanRewind() const override { + return true; + } + bool Rewind() override; + size_t ByteCount() const override { + return window_start_; + } + + bool HadError() const override { + return !error_msg_.empty(); + } + std::string GetError() const override { + return error_msg_; + } + + private: + DISALLOW_COPY_AND_ASSIGN(PngChunkFilter); + + bool ConsumeWindow(const void** buffer, size_t* len); + + StringPiece data_; + size_t window_start_ = 0; + size_t window_end_ = 0; + std::string error_msg_; +}; +/** + * Reads a PNG from the InputStream into memory as an RGBA Image. + */ +std::unique_ptr<Image> ReadPng(InputStream* in, IDiagnostics* diag); + +/** + * Writes the RGBA Image, with optional 9-patch meta-data, into the OutputStream + * as a PNG. + */ +bool WritePng(const Image* image, const NinePatch* nine_patch, OutputStream* out, + const PngOptions& options, IDiagnostics* diag, bool verbose); +} // namespace android
\ No newline at end of file diff --git a/libs/androidfw/include/androidfw/Streams.h b/libs/androidfw/include/androidfw/Streams.h new file mode 100644 index 000000000000..2daf0e2fb06d --- /dev/null +++ b/libs/androidfw/include/androidfw/Streams.h @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2016 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. + */ + +#pragma once + +#include <string> +#include "android-base/off64_t.h" + +namespace android { + +// InputStream interface that mimics protobuf's ZeroCopyInputStream, +// with added error handling methods to better report issues. +class InputStream { + public: + virtual ~InputStream() = default; + + // Returns a chunk of data for reading. data and size must not be nullptr. + // Returns true so long as there is more data to read, returns false if an error occurred + // or no data remains. If an error occurred, check HadError(). + // The stream owns the buffer returned from this method and the buffer is invalidated + // anytime another mutable method is called. + virtual bool Next(const void** data, size_t* size) = 0; + + // Backup count bytes, where count is smaller or equal to the size of the last buffer returned + // from Next(). + // Useful when the last block returned from Next() wasn't fully read. + virtual void BackUp(size_t count) = 0; + + // Returns true if this InputStream can rewind. If so, Rewind() can be called. + virtual bool CanRewind() const { + return false; + }; + + // Rewinds the stream to the beginning so it can be read again. + // Returns true if the rewind succeeded. + // This does nothing if CanRewind() returns false. + virtual bool Rewind() { + return false; + } + + // Returns the number of bytes that have been read from the stream. + virtual size_t ByteCount() const = 0; + + // Returns an error message if HadError() returned true. + virtual std::string GetError() const { + return {}; + } + + // Returns true if an error occurred. Errors are permanent. + virtual bool HadError() const = 0; + + virtual bool ReadFullyAtOffset(void* data, size_t byte_count, off64_t offset) { + (void)data; + (void)byte_count; + (void)offset; + return false; + } +}; + +// A sub-InputStream interface that knows the total size of its stream. +class KnownSizeInputStream : public InputStream { + public: + virtual size_t TotalSize() const = 0; +}; + +// OutputStream interface that mimics protobuf's ZeroCopyOutputStream, +// with added error handling methods to better report issues. +class OutputStream { + public: + virtual ~OutputStream() = default; + + // Returns a buffer to which data can be written to. The data written to this buffer will + // eventually be written to the stream. Call BackUp() if the data written doesn't occupy the + // entire buffer. + // Return false if there was an error. + // The stream owns the buffer returned from this method and the buffer is invalidated + // anytime another mutable method is called. + virtual bool Next(void** data, size_t* size) = 0; + + // Backup count bytes, where count is smaller or equal to the size of the last buffer returned + // from Next(). + // Useful for when the last block returned from Next() wasn't fully written to. + virtual void BackUp(size_t count) = 0; + + // Returns the number of bytes that have been written to the stream. + virtual size_t ByteCount() const = 0; + + // Returns an error message if HadError() returned true. + virtual std::string GetError() const { + return {}; + } + + // Returns true if an error occurred. Errors are permanent. + virtual bool HadError() const = 0; +}; + +} // namespace android
\ No newline at end of file diff --git a/libs/androidfw/tests/FileStream_test.cpp b/libs/androidfw/tests/FileStream_test.cpp new file mode 100644 index 000000000000..978597507a6d --- /dev/null +++ b/libs/androidfw/tests/FileStream_test.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2017 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. + */ + +#include "androidfw/FileStream.h" +#include "androidfw/StringPiece.h" + +#include "android-base/file.h" +#include "android-base/macros.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using ::android::StringPiece; +using ::testing::Eq; +using ::testing::NotNull; +using ::testing::StrEq; + +namespace android { + +TEST(FileInputStreamTest, NextAndBackup) { + std::string input = "this is a cool string"; + TemporaryFile file; + ASSERT_THAT(TEMP_FAILURE_RETRY(write(file.fd, input.c_str(), input.size())), Eq(21)); + lseek64(file.fd, 0, SEEK_SET); + + // Use a small buffer size so that we can call Next() a few times. + FileInputStream in(file.release(), 10u); + ASSERT_FALSE(in.HadError()); + EXPECT_THAT(in.ByteCount(), Eq(0u)); + + const void* buffer; + size_t size; + ASSERT_TRUE(in.Next(&buffer, &size)) << in.GetError(); + ASSERT_THAT(size, Eq(10u)); + ASSERT_THAT(buffer, NotNull()); + EXPECT_THAT(in.ByteCount(), Eq(10u)); + EXPECT_THAT(StringPiece(reinterpret_cast<const char*>(buffer), size), Eq("this is a ")); + + ASSERT_TRUE(in.Next(&buffer, &size)); + ASSERT_THAT(size, Eq(10u)); + ASSERT_THAT(buffer, NotNull()); + EXPECT_THAT(in.ByteCount(), Eq(20u)); + EXPECT_THAT(StringPiece(reinterpret_cast<const char*>(buffer), size), Eq("cool strin")); + + in.BackUp(5u); + EXPECT_THAT(in.ByteCount(), Eq(15u)); + + ASSERT_TRUE(in.Next(&buffer, &size)); + ASSERT_THAT(size, Eq(5u)); + ASSERT_THAT(buffer, NotNull()); + ASSERT_THAT(in.ByteCount(), Eq(20u)); + EXPECT_THAT(StringPiece(reinterpret_cast<const char*>(buffer), size), Eq("strin")); + + // Backup 1 more than possible. Should clamp. + in.BackUp(11u); + EXPECT_THAT(in.ByteCount(), Eq(10u)); + + ASSERT_TRUE(in.Next(&buffer, &size)); + ASSERT_THAT(size, Eq(10u)); + ASSERT_THAT(buffer, NotNull()); + ASSERT_THAT(in.ByteCount(), Eq(20u)); + EXPECT_THAT(StringPiece(reinterpret_cast<const char*>(buffer), size), Eq("cool strin")); + + ASSERT_TRUE(in.Next(&buffer, &size)); + ASSERT_THAT(size, Eq(1u)); + ASSERT_THAT(buffer, NotNull()); + ASSERT_THAT(in.ByteCount(), Eq(21u)); + EXPECT_THAT(StringPiece(reinterpret_cast<const char*>(buffer), size), Eq("g")); + + EXPECT_FALSE(in.Next(&buffer, &size)); + EXPECT_FALSE(in.HadError()); +} + +TEST(FileOutputStreamTest, NextAndBackup) { + const std::string input = "this is a cool string"; + + TemporaryFile file; + + FileOutputStream out(file.fd, 10u); + ASSERT_FALSE(out.HadError()); + EXPECT_THAT(out.ByteCount(), Eq(0u)); + + void* buffer; + size_t size; + ASSERT_TRUE(out.Next(&buffer, &size)); + ASSERT_THAT(size, Eq(10u)); + ASSERT_THAT(buffer, NotNull()); + EXPECT_THAT(out.ByteCount(), Eq(10u)); + memcpy(reinterpret_cast<char*>(buffer), input.c_str(), size); + + ASSERT_TRUE(out.Next(&buffer, &size)); + ASSERT_THAT(size, Eq(10u)); + ASSERT_THAT(buffer, NotNull()); + EXPECT_THAT(out.ByteCount(), Eq(20u)); + memcpy(reinterpret_cast<char*>(buffer), input.c_str() + 10u, size); + + ASSERT_TRUE(out.Next(&buffer, &size)); + ASSERT_THAT(size, Eq(10u)); + ASSERT_THAT(buffer, NotNull()); + EXPECT_THAT(out.ByteCount(), Eq(30u)); + reinterpret_cast<char*>(buffer)[0] = input[20u]; + out.BackUp(size - 1); + EXPECT_THAT(out.ByteCount(), Eq(21u)); + + ASSERT_TRUE(out.Flush()); + + lseek64(file.fd, 0, SEEK_SET); + + std::string actual; + ASSERT_TRUE(android::base::ReadFdToString(file.fd, &actual)); + EXPECT_THAT(actual, StrEq(input)); +} + +} // namespace android diff --git a/libs/androidfw/tests/NinePatch_test.cpp b/libs/androidfw/tests/NinePatch_test.cpp new file mode 100644 index 000000000000..7ee8e9ebd624 --- /dev/null +++ b/libs/androidfw/tests/NinePatch_test.cpp @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2016 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. + */ + +#include "androidfw/Image.h" +#include "androidfw/ResourceTypes.h" +#include "gtest/gtest.h" + +namespace android { + +// Pixels are in RGBA_8888 packing. + +#define RED "\xff\x00\x00\xff" +#define BLUE "\x00\x00\xff\xff" +#define GREEN "\xff\x00\x00\xff" +#define GR_70 "\xff\x00\x00\xb3" +#define GR_50 "\xff\x00\x00\x80" +#define GR_20 "\xff\x00\x00\x33" +#define BLACK "\x00\x00\x00\xff" +#define WHITE "\xff\xff\xff\xff" +#define TRANS "\x00\x00\x00\x00" + +static uint8_t* k2x2[] = { + (uint8_t*)WHITE WHITE, + (uint8_t*)WHITE WHITE, +}; + +static uint8_t* kMixedNeutralColor3x3[] = { + (uint8_t*)WHITE BLACK TRANS, + (uint8_t*)TRANS RED TRANS, + (uint8_t*)WHITE WHITE WHITE, +}; + +static uint8_t* kTransparentNeutralColor3x3[] = { + (uint8_t*)TRANS BLACK TRANS, + (uint8_t*)BLACK RED BLACK, + (uint8_t*)TRANS BLACK TRANS, +}; + +static uint8_t* kSingleStretch7x6[] = { + (uint8_t*)WHITE WHITE BLACK BLACK BLACK WHITE WHITE, + (uint8_t*)WHITE RED RED RED RED RED WHITE, + (uint8_t*)BLACK RED RED RED RED RED WHITE, + (uint8_t*)BLACK RED RED RED RED RED WHITE, + (uint8_t*)WHITE RED RED RED RED RED WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE WHITE, +}; + +static uint8_t* kMultipleStretch10x7[] = { + (uint8_t*)WHITE WHITE BLACK WHITE BLACK BLACK WHITE BLACK WHITE WHITE, + (uint8_t*)BLACK RED BLUE RED BLUE BLUE RED BLUE RED WHITE, + (uint8_t*)BLACK RED BLUE RED BLUE BLUE RED BLUE RED WHITE, + (uint8_t*)WHITE RED BLUE RED BLUE BLUE RED BLUE RED WHITE, + (uint8_t*)BLACK RED BLUE RED BLUE BLUE RED BLUE RED WHITE, + (uint8_t*)BLACK RED BLUE RED BLUE BLUE RED BLUE RED WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE, +}; + +static uint8_t* kPadding6x5[] = { + (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE, (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE BLACK, (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE, + (uint8_t*)WHITE WHITE BLACK BLACK WHITE WHITE, +}; + +static uint8_t* kLayoutBoundsWrongEdge3x3[] = { + (uint8_t*)WHITE RED WHITE, + (uint8_t*)RED WHITE WHITE, + (uint8_t*)WHITE WHITE WHITE, +}; + +static uint8_t* kLayoutBoundsNotEdgeAligned5x5[] = { + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, (uint8_t*)WHITE WHITE WHITE WHITE WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE RED, (uint8_t*)WHITE WHITE WHITE WHITE WHITE, + (uint8_t*)WHITE WHITE RED WHITE WHITE, +}; + +static uint8_t* kLayoutBounds5x5[] = { + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, (uint8_t*)WHITE WHITE WHITE WHITE RED, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, (uint8_t*)WHITE WHITE WHITE WHITE RED, + (uint8_t*)WHITE RED WHITE RED WHITE, +}; + +static uint8_t* kAsymmetricLayoutBounds5x5[] = { + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, (uint8_t*)WHITE WHITE WHITE WHITE RED, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, (uint8_t*)WHITE WHITE WHITE WHITE WHITE, + (uint8_t*)WHITE RED WHITE WHITE WHITE, +}; + +static uint8_t* kPaddingAndLayoutBounds5x5[] = { + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, (uint8_t*)WHITE WHITE WHITE WHITE RED, + (uint8_t*)WHITE WHITE WHITE WHITE BLACK, (uint8_t*)WHITE WHITE WHITE WHITE RED, + (uint8_t*)WHITE RED BLACK RED WHITE, +}; + +static uint8_t* kColorfulImage5x5[] = { + (uint8_t*)WHITE BLACK WHITE BLACK WHITE, (uint8_t*)BLACK RED BLUE GREEN WHITE, + (uint8_t*)BLACK RED GREEN GREEN WHITE, (uint8_t*)WHITE TRANS BLUE GREEN WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, +}; + +static uint8_t* kOutlineOpaque10x10[] = { + (uint8_t*)WHITE BLACK BLACK BLACK BLACK BLACK BLACK BLACK BLACK WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GREEN GREEN GREEN GREEN TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GREEN GREEN GREEN GREEN TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GREEN GREEN GREEN GREEN TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GREEN GREEN GREEN GREEN TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE, +}; + +static uint8_t* kOutlineTranslucent10x10[] = { + (uint8_t*)WHITE BLACK BLACK BLACK BLACK BLACK BLACK BLACK BLACK WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GR_20 GR_20 GR_20 GR_20 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GR_50 GR_50 GR_50 GR_50 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS GR_20 GR_50 GR_70 GR_70 GR_50 GR_20 TRANS WHITE, + (uint8_t*)WHITE TRANS GR_20 GR_50 GR_70 GR_70 GR_50 GR_20 TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GR_50 GR_50 GR_50 GR_50 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS GR_20 GR_20 GR_20 GR_20 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE, +}; + +static uint8_t* kOutlineOffsetTranslucent12x10[] = { + (uint8_t*)WHITE WHITE WHITE BLACK BLACK BLACK BLACK BLACK BLACK BLACK BLACK WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS GR_20 GR_20 GR_20 GR_20 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS GR_50 GR_50 GR_50 GR_50 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS GR_20 GR_50 GR_70 GR_70 GR_50 GR_20 TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS GR_20 GR_50 GR_70 GR_70 GR_50 GR_20 TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS GR_50 GR_50 GR_50 GR_50 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS GR_20 GR_20 GR_20 GR_20 TRANS TRANS WHITE, + (uint8_t*)WHITE TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS TRANS WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE WHITE, +}; + +static uint8_t* kOutlineRadius5x5[] = { + (uint8_t*)WHITE BLACK BLACK BLACK WHITE, (uint8_t*)BLACK TRANS GREEN TRANS WHITE, + (uint8_t*)BLACK GREEN GREEN GREEN WHITE, (uint8_t*)BLACK TRANS GREEN TRANS WHITE, + (uint8_t*)WHITE WHITE WHITE WHITE WHITE, +}; + +static uint8_t* kStretchAndPadding5x5[] = { + (uint8_t*)WHITE WHITE BLACK WHITE WHITE, (uint8_t*)WHITE RED RED RED WHITE, + (uint8_t*)BLACK RED RED RED BLACK, (uint8_t*)WHITE RED RED RED WHITE, + (uint8_t*)WHITE WHITE BLACK WHITE WHITE, +}; + +TEST(NinePatchTest, Minimum3x3) { + std::string err; + EXPECT_EQ(nullptr, NinePatch::Create(k2x2, 2, 2, &err)); + EXPECT_FALSE(err.empty()); +} + +TEST(NinePatchTest, MixedNeutralColors) { + std::string err; + EXPECT_EQ(nullptr, NinePatch::Create(kMixedNeutralColor3x3, 3, 3, &err)); + EXPECT_FALSE(err.empty()); +} + +TEST(NinePatchTest, TransparentNeutralColor) { + std::string err; + EXPECT_NE(nullptr, NinePatch::Create(kTransparentNeutralColor3x3, 3, 3, &err)); +} + +TEST(NinePatchTest, SingleStretchRegion) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kSingleStretch7x6, 7, 6, &err); + ASSERT_NE(nullptr, nine_patch); + + ASSERT_EQ(1u, nine_patch->horizontal_stretch_regions.size()); + ASSERT_EQ(1u, nine_patch->vertical_stretch_regions.size()); + + EXPECT_EQ(Range(1, 4), nine_patch->horizontal_stretch_regions.front()); + EXPECT_EQ(Range(1, 3), nine_patch->vertical_stretch_regions.front()); +} + +TEST(NinePatchTest, MultipleStretchRegions) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kMultipleStretch10x7, 10, 7, &err); + ASSERT_NE(nullptr, nine_patch); + + ASSERT_EQ(3u, nine_patch->horizontal_stretch_regions.size()); + ASSERT_EQ(2u, nine_patch->vertical_stretch_regions.size()); + + EXPECT_EQ(Range(1, 2), nine_patch->horizontal_stretch_regions[0]); + EXPECT_EQ(Range(3, 5), nine_patch->horizontal_stretch_regions[1]); + EXPECT_EQ(Range(6, 7), nine_patch->horizontal_stretch_regions[2]); + + EXPECT_EQ(Range(0, 2), nine_patch->vertical_stretch_regions[0]); + EXPECT_EQ(Range(3, 5), nine_patch->vertical_stretch_regions[1]); +} + +TEST(NinePatchTest, InferPaddingFromStretchRegions) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kMultipleStretch10x7, 10, 7, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(1, 0, 1, 0), nine_patch->padding); +} + +TEST(NinePatchTest, Padding) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kPadding6x5, 6, 5, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(1, 1, 1, 1), nine_patch->padding); +} + +TEST(NinePatchTest, LayoutBoundsAreOnWrongEdge) { + std::string err; + EXPECT_EQ(nullptr, NinePatch::Create(kLayoutBoundsWrongEdge3x3, 3, 3, &err)); + EXPECT_FALSE(err.empty()); +} + +TEST(NinePatchTest, LayoutBoundsMustTouchEdges) { + std::string err; + EXPECT_EQ(nullptr, NinePatch::Create(kLayoutBoundsNotEdgeAligned5x5, 5, 5, &err)); + EXPECT_FALSE(err.empty()); +} + +TEST(NinePatchTest, LayoutBounds) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kLayoutBounds5x5, 5, 5, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(1, 1, 1, 1), nine_patch->layout_bounds); + + nine_patch = NinePatch::Create(kAsymmetricLayoutBounds5x5, 5, 5, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(1, 1, 0, 0), nine_patch->layout_bounds); +} + +TEST(NinePatchTest, PaddingAndLayoutBounds) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kPaddingAndLayoutBounds5x5, 5, 5, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(1, 1, 1, 1), nine_patch->padding); + EXPECT_EQ(Bounds(1, 1, 1, 1), nine_patch->layout_bounds); +} + +TEST(NinePatchTest, RegionColorsAreCorrect) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kColorfulImage5x5, 5, 5, &err); + ASSERT_NE(nullptr, nine_patch); + + std::vector<uint32_t> expected_colors = { + NinePatch::PackRGBA((uint8_t*)RED), (uint32_t)android::Res_png_9patch::NO_COLOR, + NinePatch::PackRGBA((uint8_t*)GREEN), (uint32_t)android::Res_png_9patch::TRANSPARENT_COLOR, + NinePatch::PackRGBA((uint8_t*)BLUE), NinePatch::PackRGBA((uint8_t*)GREEN), + }; + EXPECT_EQ(expected_colors, nine_patch->region_colors); +} + +TEST(NinePatchTest, OutlineFromOpaqueImage) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kOutlineOpaque10x10, 10, 10, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(2, 2, 2, 2), nine_patch->outline); + EXPECT_EQ(0x000000ffu, nine_patch->outline_alpha); + EXPECT_EQ(0.0f, nine_patch->outline_radius); +} + +TEST(NinePatchTest, OutlineFromTranslucentImage) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kOutlineTranslucent10x10, 10, 10, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(3, 3, 3, 3), nine_patch->outline); + EXPECT_EQ(0x000000b3u, nine_patch->outline_alpha); + EXPECT_EQ(0.0f, nine_patch->outline_radius); +} + +TEST(NinePatchTest, OutlineFromOffCenterImage) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = + NinePatch::Create(kOutlineOffsetTranslucent12x10, 12, 10, &err); + ASSERT_NE(nullptr, nine_patch); + + // TODO(adamlesinski): The old AAPT algorithm searches from the outside to the + // middle for each inset. If the outline is shifted, the search may not find a + // closer bounds. + // This check should be: + // EXPECT_EQ(Bounds(5, 3, 3, 3), ninePatch->outline); + // but until I know what behavior I'm breaking, I will leave it at the + // incorrect: + EXPECT_EQ(Bounds(4, 3, 3, 3), nine_patch->outline); + + EXPECT_EQ(0x000000b3u, nine_patch->outline_alpha); + EXPECT_EQ(0.0f, nine_patch->outline_radius); +} + +TEST(NinePatchTest, OutlineRadius) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kOutlineRadius5x5, 5, 5, &err); + ASSERT_NE(nullptr, nine_patch); + EXPECT_EQ(Bounds(0, 0, 0, 0), nine_patch->outline); + EXPECT_EQ(3.4142f, nine_patch->outline_radius); +} + +::testing::AssertionResult BigEndianOne(uint8_t* cursor) { + if (cursor[0] == 0 && cursor[1] == 0 && cursor[2] == 0 && cursor[3] == 1) { + return ::testing::AssertionSuccess(); + } + return ::testing::AssertionFailure() << "Not BigEndian 1"; +} + +TEST(NinePatchTest, SerializePngEndianness) { + std::string err; + std::unique_ptr<NinePatch> nine_patch = NinePatch::Create(kStretchAndPadding5x5, 5, 5, &err); + ASSERT_NE(nullptr, nine_patch); + + size_t len; + std::unique_ptr<uint8_t[]> data = nine_patch->SerializeBase(&len); + ASSERT_NE(nullptr, data); + ASSERT_NE(0u, len); + + // Skip past wasDeserialized + numXDivs + numYDivs + numColors + xDivsOffset + + // yDivsOffset + // (12 bytes) + uint8_t* cursor = data.get() + 12; + + // Check that padding is big-endian. Expecting value 1. + EXPECT_TRUE(BigEndianOne(cursor)); + EXPECT_TRUE(BigEndianOne(cursor + 4)); + EXPECT_TRUE(BigEndianOne(cursor + 8)); + EXPECT_TRUE(BigEndianOne(cursor + 12)); +} + +} // namespace android diff --git a/libs/hwui/Mesh.cpp b/libs/hwui/Mesh.cpp index e59bc9565a59..37a7d74330e9 100644 --- a/libs/hwui/Mesh.cpp +++ b/libs/hwui/Mesh.cpp @@ -90,8 +90,8 @@ std::tuple<bool, SkString> Mesh::validate() { FAIL_MESH_VALIDATE("%s mode requires at least %zu vertices but vertex count is %zu.", modeToStr(meshMode), min_vcount_for_mode(meshMode), mVertexCount); } - SkASSERT(!fICount); - SkASSERT(!fIOffset); + LOG_ALWAYS_FATAL_IF(mIndexCount != 0); + LOG_ALWAYS_FATAL_IF(mIndexOffset != 0); } if (!sm.ok()) { diff --git a/libs/hwui/jni/YuvToJpegEncoder.cpp b/libs/hwui/jni/YuvToJpegEncoder.cpp index c55066af3612..0275e4f13b3b 100644 --- a/libs/hwui/jni/YuvToJpegEncoder.cpp +++ b/libs/hwui/jni/YuvToJpegEncoder.cpp @@ -2,6 +2,7 @@ #include "SkStream.h" #include "YuvToJpegEncoder.h" #include <ui/PixelFormat.h> +#include <utils/Errors.h> #include <hardware/hardware.h> #include "graphics_jni_helpers.h" @@ -295,7 +296,7 @@ void Yuv422IToJpegEncoder::configSamplingFactors(jpeg_compress_struct* cinfo) { } /////////////////////////////////////////////////////////////////////////////// -using namespace android::ultrahdr; +using namespace ultrahdr; ultrahdr_color_gamut P010Yuv420ToJpegREncoder::findColorGamut(JNIEnv* env, int aDataSpace) { switch (aDataSpace & ADataSpace::STANDARD_MASK) { diff --git a/libs/hwui/jni/YuvToJpegEncoder.h b/libs/hwui/jni/YuvToJpegEncoder.h index a3a322453be7..629f1e64726b 100644 --- a/libs/hwui/jni/YuvToJpegEncoder.h +++ b/libs/hwui/jni/YuvToJpegEncoder.h @@ -108,7 +108,7 @@ public: * @param aDataSpace data space defined in data_space.h. * @return color gamut for JPEG/R. */ - static android::ultrahdr::ultrahdr_color_gamut findColorGamut(JNIEnv* env, int aDataSpace); + static ultrahdr::ultrahdr_color_gamut findColorGamut(JNIEnv* env, int aDataSpace); /** Map data space (defined in DataSpace.java and data_space.h) to the transfer function * used in JPEG/R @@ -117,8 +117,8 @@ public: * @param aDataSpace data space defined in data_space.h. * @return color gamut for JPEG/R. */ - static android::ultrahdr::ultrahdr_transfer_function findHdrTransferFunction( - JNIEnv* env, int aDataSpace); + static ultrahdr::ultrahdr_transfer_function findHdrTransferFunction(JNIEnv* env, + int aDataSpace); }; #endif // _ANDROID_GRAPHICS_YUV_TO_JPEG_ENCODER_H_ diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index 12cb69da772b..d74748936d15 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -105,7 +105,7 @@ IRenderPipeline::DrawResult SkiaVulkanPipeline::draw( ProfileType::None != Properties::getProfileType())) { SkCanvas* profileCanvas = backBuffer->getCanvas(); SkAutoCanvasRestore saver(profileCanvas, true); - profileCanvas->concat(mVkSurface->getCurrentPreTransform()); + profileCanvas->concat(preTransform); SkiaProfileRenderer profileRenderer(profileCanvas, frame.width(), frame.height()); profiler->draw(profileRenderer); } |