diff options
| author | 2021-03-17 12:30:09 -0400 | |
|---|---|---|
| committer | 2021-03-17 15:12:31 -0400 | |
| commit | b42ce6661bdebc865a514020e6aae128f4eae0e4 (patch) | |
| tree | 4122f4f3998038ae805d4910221b6d5fc9e9f8cf | |
| parent | 82769e003bac367c7fb518e8c43c91a01c7d1b3a (diff) | |
Properly handle empty response rect in ScrollCaptureController
Add tests for ScrollCaptureController for this bug and other typical
flows.
Bug: 182926096
Test: atest ScrollCaptureControllerTest
Change-Id: Ica2104c7c98b99b9322a6efe6bbc5b33c01d8b86
4 files changed, 233 insertions, 13 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageTileSet.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageTileSet.java index 07adc7bd7053..730702ec8685 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageTileSet.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageTileSet.java @@ -34,6 +34,8 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import javax.inject.Inject; + /** * Owns a series of partial screen captures (tiles). * <p> @@ -47,6 +49,7 @@ class ImageTileSet { private CallbackRegistry<OnBoundsChangedListener, ImageTileSet, Rect> mOnBoundsListeners; private CallbackRegistry<OnContentChangedListener, ImageTileSet, Rect> mContentListeners; + @Inject ImageTileSet(@UiThread Handler handler) { mHandler = handler; } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java index 3ac884b98136..31cdadab070d 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java @@ -29,11 +29,9 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; -import android.os.RemoteException; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; -import android.view.IScrollCaptureConnection; import android.view.IWindowManager; import android.view.ScrollCaptureResponse; import android.view.View; @@ -101,12 +99,12 @@ public class LongScreenshotActivity extends Activity { @Inject public LongScreenshotActivity(UiEventLogger uiEventLogger, ImageExporter imageExporter, @Main Executor mainExecutor, @Background Executor bgExecutor, IWindowManager wms, - Context context) { + Context context, ScrollCaptureController scrollCaptureController) { mUiEventLogger = uiEventLogger; mUiExecutor = mainExecutor; mBackgroundExecutor = bgExecutor; mImageExporter = imageExporter; - mScrollCaptureController = new ScrollCaptureController(context, bgExecutor, wms); + mScrollCaptureController = scrollCaptureController; } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java index 4f699041fdb3..d3dd048a989e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java @@ -18,19 +18,16 @@ package com.android.systemui.screenshot; import android.content.Context; import android.graphics.Bitmap; -import android.graphics.HardwareRenderer; -import android.graphics.RecordingCanvas; import android.graphics.Rect; -import android.graphics.RenderNode; import android.graphics.drawable.Drawable; import android.provider.Settings; import android.util.Log; -import android.view.IWindowManager; import android.view.ScrollCaptureResponse; import androidx.concurrent.futures.CallbackToFutureAdapter; import androidx.concurrent.futures.CallbackToFutureAdapter.Completer; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.screenshot.ScrollCaptureClient.CaptureResult; import com.android.systemui.screenshot.ScrollCaptureClient.Session; @@ -39,6 +36,8 @@ import com.google.common.util.concurrent.ListenableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import javax.inject.Inject; + /** * Interaction controller between the UI and ScrollCaptureClient. */ @@ -131,11 +130,13 @@ public class ScrollCaptureController { } } - ScrollCaptureController(Context context, Executor bgExecutor, IWindowManager wms) { + @Inject + ScrollCaptureController(Context context, @Background Executor bgExecutor, + ScrollCaptureClient client, ImageTileSet imageTileSet) { mContext = context; mBgExecutor = bgExecutor; - mImageTileSet = new ImageTileSet(context.getMainThreadHandler()); - mClient = new ScrollCaptureClient(mContext, wms); + mClient = client; + mImageTileSet = imageTileSet; } /** @@ -252,8 +253,10 @@ public class ScrollCaptureController { return; } - int nextTop = (mScrollingUp) - ? result.captured.top - mSession.getTileHeight() : result.captured.bottom; + // Partial or empty results caused the direction the flip, so we can reliably use the + // requested edges to determine the next top. + int nextTop = (mScrollingUp) ? result.requested.top - mSession.getTileHeight() + : result.requested.bottom; requestNextTile(nextTop); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureControllerTest.java new file mode 100644 index 000000000000..410d9de9e0ac --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureControllerTest.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.graphics.Rect; +import android.hardware.HardwareBuffer; +import android.media.Image; +import android.testing.AndroidTestingRunner; +import android.view.ScrollCaptureResponse; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.systemui.SysuiTestCase; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.ExecutionException; + +/** + * Tests for ScrollCaptureController which manages sequential image acquisition for long + * screenshots. + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class ScrollCaptureControllerTest extends SysuiTestCase { + + private static class FakeSession implements ScrollCaptureClient.Session { + public int availableTop = Integer.MIN_VALUE; + public int availableBottom = Integer.MAX_VALUE; + // If true, return an empty rect any time a partial result would have been returned. + public boolean emptyInsteadOfPartial = false; + + @Override + public ListenableFuture<ScrollCaptureClient.CaptureResult> requestTile(int top) { + Rect requested = new Rect(0, top, getPageWidth(), top + getTileHeight()); + Rect fullContent = new Rect(0, availableTop, getPageWidth(), availableBottom); + Rect captured = new Rect(requested); + captured.intersect(fullContent); + if (emptyInsteadOfPartial && captured.height() != getTileHeight()) { + captured = new Rect(); + } + Image image = mock(Image.class); + when(image.getHardwareBuffer()).thenReturn(mock(HardwareBuffer.class)); + ScrollCaptureClient.CaptureResult result = + new ScrollCaptureClient.CaptureResult(image, requested, captured); + return Futures.immediateFuture(result); + } + + public int getMaxHeight() { + return getTileHeight() * getMaxTiles(); + } + + @Override + public int getMaxTiles() { + return 10; + } + + @Override + public int getTileHeight() { + return 50; + } + + @Override + public int getPageHeight() { + return 100; + } + + @Override + public int getPageWidth() { + return 100; + } + + @Override + public Rect getWindowBounds() { + return null; + } + + @Override + public ListenableFuture<Void> end() { + return Futures.immediateVoidFuture(); + } + + @Override + public void release() { + } + } + + private ScrollCaptureController mController; + private FakeSession mSession; + private ScrollCaptureClient mScrollCaptureClient; + + @Before + public void setUp() { + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + mSession = new FakeSession(); + mScrollCaptureClient = mock(ScrollCaptureClient.class); + when(mScrollCaptureClient.request(anyInt(), anyInt())).thenReturn( + Futures.immediateFuture(new ScrollCaptureResponse.Builder().build())); + when(mScrollCaptureClient.start(any(), anyFloat())).thenReturn( + Futures.immediateFuture(mSession)); + mController = new ScrollCaptureController(context, context.getMainExecutor(), + mScrollCaptureClient, new ImageTileSet(context.getMainThreadHandler())); + } + + @Test + public void testInfinite() throws ExecutionException, InterruptedException { + ScrollCaptureController.LongScreenshot screenshot = + mController.run(new ScrollCaptureResponse.Builder().build()).get(); + assertEquals(mSession.getMaxHeight(), screenshot.getHeight()); + // TODO: the top and bottom ratio in the infinite case should be extracted and tested. + assertEquals(-150, screenshot.getTop()); + assertEquals(350, screenshot.getBottom()); + } + + @Test + public void testLimitedBottom() throws ExecutionException, InterruptedException { + // We hit the bottom of the content, so expect it to scroll back up and go above the -150 + // default top position + mSession.availableBottom = 275; + ScrollCaptureController.LongScreenshot screenshot = + mController.run(new ScrollCaptureResponse.Builder().build()).get(); + // Bottom tile will be 25px tall, 10 tiles total + assertEquals(mSession.getMaxHeight() - 25, screenshot.getHeight()); + assertEquals(-200, screenshot.getTop()); + assertEquals(mSession.availableBottom, screenshot.getBottom()); + } + + @Test + public void testLimitedTopAndBottom() throws ExecutionException, InterruptedException { + mSession.availableBottom = 275; + mSession.availableTop = -200; + ScrollCaptureController.LongScreenshot screenshot = + mController.run(new ScrollCaptureResponse.Builder().build()).get(); + assertEquals(mSession.availableBottom - mSession.availableTop, screenshot.getHeight()); + assertEquals(mSession.availableTop, screenshot.getTop()); + assertEquals(mSession.availableBottom, screenshot.getBottom()); + } + + @Test + public void testVeryLimitedTopInfiniteBottom() throws ExecutionException, InterruptedException { + // Hit the boundary before the "headroom" is hit in the up direction, then go down + // infinitely. + mSession.availableTop = -55; + ScrollCaptureController.LongScreenshot screenshot = + mController.run(new ScrollCaptureResponse.Builder().build()).get(); + // The top tile will be 5px tall, so subtract 45px from the theoretical max. + assertEquals(mSession.getMaxHeight() - 45, screenshot.getHeight()); + assertEquals(mSession.availableTop, screenshot.getTop()); + assertEquals(mSession.availableTop + mSession.getMaxHeight() - 45, screenshot.getBottom()); + } + + @Test + public void testVeryLimitedTopLimitedBottom() throws ExecutionException, InterruptedException { + mSession.availableBottom = 275; + mSession.availableTop = -55; + ScrollCaptureController.LongScreenshot screenshot = + mController.run(new ScrollCaptureResponse.Builder().build()).get(); + assertEquals(mSession.availableBottom - mSession.availableTop, screenshot.getHeight()); + assertEquals(mSession.availableTop, screenshot.getTop()); + assertEquals(mSession.availableBottom, screenshot.getBottom()); + } + + @Test + public void testLimitedTopAndBottomWithEmpty() throws ExecutionException, InterruptedException { + mSession.emptyInsteadOfPartial = true; + mSession.availableBottom = 275; + mSession.availableTop = -167; + ScrollCaptureController.LongScreenshot screenshot = + mController.run(new ScrollCaptureResponse.Builder().build()).get(); + // Expecting output from -150 to 250 + assertEquals(400, screenshot.getHeight()); + assertEquals(-150, screenshot.getTop()); + assertEquals(250, screenshot.getBottom()); + } + + @Test + public void testVeryLimitedTopWithEmpty() throws ExecutionException, InterruptedException { + // Hit the boundary before the "headroom" is hit in the up direction, then go down + // infinitely. + mSession.availableTop = -55; + mSession.emptyInsteadOfPartial = true; + ScrollCaptureController.LongScreenshot screenshot = + mController.run(new ScrollCaptureResponse.Builder().build()).get(); + assertEquals(mSession.getMaxHeight(), screenshot.getHeight()); + assertEquals(-50, screenshot.getTop()); + assertEquals(-50 + mSession.getMaxHeight(), screenshot.getBottom()); + } +} |