| /* |
| * 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 android.util.MathUtils.constrain; |
| |
| import static com.google.common.util.concurrent.Futures.immediateFuture; |
| |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.when; |
| |
| import static java.lang.Math.abs; |
| import static java.lang.Math.max; |
| import static java.lang.Math.min; |
| |
| import android.graphics.Rect; |
| import android.hardware.HardwareBuffer; |
| import android.media.Image; |
| import android.util.Log; |
| |
| import com.google.common.util.concurrent.Futures; |
| import com.google.common.util.concurrent.ListenableFuture; |
| |
| /** |
| * A flexible test double for {@link ScrollCaptureClient.Session}. |
| * <p> |
| * FakeSession provides the ability to emulate both the available scrollable content range as well |
| * as the current visible bounds. Visible bounds may vary because the target view itself may be |
| * slid vertically during capture, with portions may become clipped by parent views. This scenario |
| * frequently occurs with UIs constructed from nested scrolling views or collapsing headers. |
| */ |
| class FakeSession implements ScrollCaptureClient.Session { |
| private static final String TAG = "FakeSession"; |
| // Available range of content |
| private final Rect mAvailable; |
| |
| /** bounds for scrollDelta (y), range with bottom adjusted to account for page height. */ |
| private final Rect mAvailableTop; |
| |
| private final Rect mVisiblePage; |
| private final int mTileHeight; |
| private final int mMaxTiles; |
| |
| private int mScrollDelta; |
| private int mPageHeight; |
| private int mTargetHeight; |
| |
| FakeSession(int pageHeight, float maxPages, int tileHeight, int visiblePageTop, |
| int visiblePageBottom, int availableTop, int availableBottom, |
| int maxTiles) { |
| mPageHeight = pageHeight; |
| mTileHeight = tileHeight; |
| mAvailable = new Rect(0, availableTop, getPageWidth(), availableBottom); |
| mAvailableTop = new Rect(mAvailable); |
| mAvailableTop.inset(0, 0, 0, pageHeight); |
| mVisiblePage = new Rect(0, visiblePageTop, getPageWidth(), visiblePageBottom); |
| mTargetHeight = (int) (pageHeight * maxPages); |
| mMaxTiles = maxTiles; |
| } |
| |
| private static Image mockImage() { |
| Image image = mock(Image.class); |
| when(image.getHardwareBuffer()).thenReturn(mock(HardwareBuffer.class)); |
| return image; |
| } |
| |
| public int getScrollDelta() { |
| return mScrollDelta; |
| } |
| |
| @Override |
| public ListenableFuture<ScrollCaptureClient.CaptureResult> requestTile(int requestedTop) { |
| Rect requested = new Rect(0, requestedTop, getPageWidth(), requestedTop + getTileHeight()); |
| Log.d(TAG, "requested: " + requested); |
| Rect page = new Rect(0, 0, getPageWidth(), mPageHeight); |
| page.offset(0, mScrollDelta); |
| Log.d(TAG, "page: " + page); |
| // Simulate behavior from lower levels by replicating 'requestChildRectangleOnScreen' |
| if (!page.contains(requested)) { |
| Log.d(TAG, "requested not within page, scrolling"); |
| // distance+direction needed to scroll to align each edge of request with |
| // corresponding edge of the page |
| int distTop = requested.top - page.top; // positive means already visible |
| int distBottom = requested.bottom - page.bottom; // negative means already visible |
| Log.d(TAG, "distTop = " + distTop); |
| Log.d(TAG, "distBottom = " + distBottom); |
| |
| boolean scrollUp = false; |
| if (distTop < 0 && distBottom > 0) { |
| scrollUp = abs(distTop) < distBottom; |
| } else if (distTop < 0) { |
| scrollUp = true; |
| } |
| |
| // determine which edges are currently clipped |
| if (scrollUp) { |
| Log.d(TAG, "trying to scroll up by " + -distTop + " px"); |
| // need to scroll up to align top edge to visible-top |
| mScrollDelta += distTop; |
| Log.d(TAG, "new scrollDelta = " + mScrollDelta); |
| } else { |
| Log.d(TAG, "trying to scroll down by " + distBottom + " px"); |
| // scroll down to align bottom edge with visible bottom, but keep top visible |
| int topEdgeDistance = max(0, requestedTop - page.top); |
| mScrollDelta += min(distBottom, topEdgeDistance); |
| Log.d(TAG, "new scrollDelta = " + mScrollDelta); |
| } |
| |
| // Clamp to available content |
| mScrollDelta = constrain(mScrollDelta, mAvailableTop.top, mAvailableTop.bottom); |
| Log.d(TAG, "scrollDelta, adjusted to available range = " + mScrollDelta); |
| |
| // Reset to apply a changed scroll delta possibly. |
| page.offsetTo(0, 0); |
| page.offset(0, mScrollDelta); |
| |
| Log.d(TAG, "page (after scroll): " + page); |
| Log.d(TAG, "requested (after scroll): " + requested); |
| } |
| Log.d(TAG, "mVisiblePage = " + mVisiblePage); |
| Log.d(TAG, "scrollDelta = " + mScrollDelta); |
| |
| Rect target = new Rect(requested); |
| Rect visible = new Rect(mVisiblePage); |
| visible.offset(0, mScrollDelta); |
| |
| Log.d(TAG, "target: " + target); |
| Log.d(TAG, "visible: " + visible); |
| |
| // if any of the requested rect is available to scroll into the view: |
| if (target.intersect(page) && target.intersect(visible)) { |
| Log.d(TAG, "returning captured = " + target); |
| ScrollCaptureClient.CaptureResult result = |
| new ScrollCaptureClient.CaptureResult(mockImage(), requested, target); |
| return immediateFuture(result); |
| } |
| Log.d(TAG, "no part of requested rect is within page, returning empty"); |
| ScrollCaptureClient.CaptureResult result = |
| new ScrollCaptureClient.CaptureResult(null, requested, new Rect()); |
| return immediateFuture(result); |
| } |
| |
| |
| @Override |
| public int getMaxTiles() { |
| return mMaxTiles; |
| } |
| |
| @Override |
| public int getTargetHeight() { |
| return mTargetHeight; |
| } |
| |
| @Override |
| public int getTileHeight() { |
| return mTileHeight; |
| } |
| |
| @Override |
| public int getPageWidth() { |
| return 100; |
| } |
| |
| @Override |
| public int getPageHeight() { |
| return mPageHeight; |
| } |
| |
| @Override |
| public Rect getWindowBounds() { |
| throw new IllegalStateException("Not implemented"); |
| } |
| |
| @Override |
| public ListenableFuture<Void> end() { |
| return Futures.immediateVoidFuture(); |
| } |
| |
| @Override |
| public void release() { |
| } |
| } |