Merge "Demo: show scroll chip and capture tall screenshot when tapped"
This commit is contained in:
25
packages/SystemUI/res/drawable/ic_screenshot_scroll.xml
Normal file
25
packages/SystemUI/res/drawable/ic_screenshot_scroll.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<!-- ic_unfold_more_24px.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,5.83L15.17,9l1.41,-1.41L12,3 7.41,7.59 8.83,9 12,5.83zM12,18.17L8.83,15l-1.41,1.41L12,21l4.59,-4.59L15.17,15 12,18.17z"/>
|
||||
</vector>
|
||||
@@ -28,6 +28,6 @@
|
||||
android:visibility="gone"
|
||||
android:background="@drawable/screenshot_rounded_corners"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="@string/screenshot_edit"
|
||||
android:contentDescription="@string/screenshot_edit_label"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
@@ -46,7 +46,7 @@
|
||||
android:layout_height="@dimen/screenshot_dismiss_button_tappable_size"
|
||||
android:elevation="7dp"
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/screenshot_dismiss_ui_description">
|
||||
android:contentDescription="@string/screenshot_dismiss_description">
|
||||
<ImageView
|
||||
android:id="@+id/global_screenshot_dismiss_image"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -28,6 +28,6 @@
|
||||
android:visibility="gone"
|
||||
android:background="@drawable/screenshot_rounded_corners"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="@string/screenshot_edit"
|
||||
android:contentDescription="@string/screenshot_edit_label"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
@@ -56,6 +56,9 @@
|
||||
android:id="@+id/screenshot_share_chip"/>
|
||||
<include layout="@layout/global_screenshot_action_chip"
|
||||
android:id="@+id/screenshot_edit_chip"/>
|
||||
<include layout="@layout/global_screenshot_action_chip"
|
||||
android:id="@+id/screenshot_scroll_chip"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</HorizontalScrollView>
|
||||
<include layout="@layout/global_screenshot_preview"/>
|
||||
|
||||
@@ -233,10 +233,16 @@
|
||||
<!-- Notification text displayed when we fail to take a screenshot. [CHAR LIMIT=100] -->
|
||||
<string name="screenshot_failed_to_capture_text">Taking screenshots isn\'t allowed by the app or
|
||||
your organization</string>
|
||||
<!-- Label for UI element which allows editing the screenshot [CHAR LIMIT=30] -->
|
||||
<string name="screenshot_edit_label">Edit</string>
|
||||
<!-- Content description indicating that tapping the element will allow editing the screenshot [CHAR LIMIT=NONE] -->
|
||||
<string name="screenshot_edit">Edit screenshot</string>
|
||||
<string name="screenshot_edit_description">Edit screenshot</string>
|
||||
<!-- Label for UI element which allows scrolling and extending the screenshot to be taller [CHAR LIMIT=30] -->
|
||||
<string name="screenshot_scroll_label">Scroll</string>
|
||||
<!-- Content description UI element which allows scrolling and extending the screenshot to be taller [CHAR LIMIT=NONE] -->
|
||||
<string name="screenshot_scroll_description">Scroll screenshot</string>
|
||||
<!-- Content description indicating that tapping a button will dismiss the screenshots UI [CHAR LIMIT=NONE] -->
|
||||
<string name="screenshot_dismiss_ui_description">Dismiss screenshot</string>
|
||||
<string name="screenshot_dismiss_description">Dismiss screenshot</string>
|
||||
<!-- Content description indicating that the view is a preview of the screenshot that was just taken [CHAR LIMIT=NONE] -->
|
||||
<string name="screenshot_preview_description">Screenshot preview</string>
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import android.animation.AnimatorListenerAdapter;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Notification;
|
||||
import android.app.WindowContext;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
@@ -43,6 +42,7 @@ import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.provider.DeviceConfig;
|
||||
import android.provider.Settings;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
@@ -58,8 +58,10 @@ import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
|
||||
import com.android.internal.logging.UiEventLogger;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.util.DeviceConfigProxy;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
@@ -143,6 +145,8 @@ public class ScreenshotController {
|
||||
private final DisplayMetrics mDisplayMetrics;
|
||||
private final AccessibilityManager mAccessibilityManager;
|
||||
private final MediaActionSound mCameraSound;
|
||||
private final ScrollCaptureClient mScrollCaptureClient;
|
||||
private final DeviceConfigProxy mConfigProxy;
|
||||
|
||||
private final Binder mWindowToken;
|
||||
private ScreenshotView mScreenshotView;
|
||||
@@ -173,11 +177,16 @@ public class ScreenshotController {
|
||||
};
|
||||
|
||||
@Inject
|
||||
ScreenshotController(Context context, ScreenshotSmartActions screenshotSmartActions,
|
||||
ScreenshotController(
|
||||
Context context,
|
||||
ScreenshotSmartActions screenshotSmartActions,
|
||||
ScreenshotNotificationsController screenshotNotificationsController,
|
||||
UiEventLogger uiEventLogger) {
|
||||
ScrollCaptureClient scrollCaptureClient,
|
||||
UiEventLogger uiEventLogger,
|
||||
DeviceConfigProxy configProxy) {
|
||||
mScreenshotSmartActions = screenshotSmartActions;
|
||||
mNotificationsController = screenshotNotificationsController;
|
||||
mScrollCaptureClient = scrollCaptureClient;
|
||||
mUiEventLogger = uiEventLogger;
|
||||
|
||||
final DisplayManager dm = requireNonNull(context.getSystemService(DisplayManager.class));
|
||||
@@ -186,6 +195,7 @@ public class ScreenshotController {
|
||||
mWindowManager = mContext.getSystemService(WindowManager.class);
|
||||
|
||||
mAccessibilityManager = AccessibilityManager.getInstance(mContext);
|
||||
mConfigProxy = configProxy;
|
||||
|
||||
reloadAssets();
|
||||
Configuration config = mContext.getResources().getConfiguration();
|
||||
@@ -193,6 +203,7 @@ public class ScreenshotController {
|
||||
mDirectionLTR = config.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
|
||||
mOrientationPortrait = config.orientation == ORIENTATION_PORTRAIT;
|
||||
mWindowToken = new Binder("ScreenshotController");
|
||||
mScrollCaptureClient.setHostWindowToken(mWindowToken);
|
||||
|
||||
// Setup the window that we are going to use
|
||||
mWindowLayoutParams = new WindowManager.LayoutParams(
|
||||
@@ -455,6 +466,19 @@ public class ScreenshotController {
|
||||
|
||||
// Start the post-screenshot animation
|
||||
startAnimation(finisher, screenRect, screenInsets, showFlash);
|
||||
|
||||
if (mConfigProxy.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
|
||||
SystemUiDeviceConfigFlags.SCREENSHOT_SCROLLING_ENABLED, false)) {
|
||||
mScrollCaptureClient.request(DEFAULT_DISPLAY, (connection) ->
|
||||
mScreenshotView.showScrollChip(() ->
|
||||
runScrollCapture(connection,
|
||||
() -> dismissScreenshot(false))));
|
||||
}
|
||||
}
|
||||
|
||||
private void runScrollCapture(ScrollCaptureClient.Connection connection,
|
||||
Runnable after) {
|
||||
new ScrollCaptureController(mContext, connection).run(after);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -113,6 +113,7 @@ public class ScreenshotView extends FrameLayout implements
|
||||
private FrameLayout mDismissButton;
|
||||
private ScreenshotActionChip mShareChip;
|
||||
private ScreenshotActionChip mEditChip;
|
||||
private ScreenshotActionChip mScrollChip;
|
||||
|
||||
private final ArrayList<ScreenshotActionChip> mSmartChips = new ArrayList<>();
|
||||
private PendingInteraction mPendingInteraction;
|
||||
@@ -152,6 +153,20 @@ public class ScreenshotView extends FrameLayout implements
|
||||
mContext.getDisplay().getRealMetrics(mDisplayMetrics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to display the scroll action chip when support is detected.
|
||||
*
|
||||
* @param onClick the action to take when the chip is clicked.
|
||||
*/
|
||||
public void showScrollChip(Runnable onClick) {
|
||||
mScrollChip.setVisibility(VISIBLE);
|
||||
mScrollChip.setOnClickListener((v) ->
|
||||
onClick.run()
|
||||
// TODO Logging, store event consumer to a field
|
||||
//onElementTapped.accept(ScreenshotEvent.SCREENSHOT_SCROLL_TAPPED);
|
||||
);
|
||||
}
|
||||
|
||||
@Override // ViewTreeObserver.OnComputeInternalInsetsListener
|
||||
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
|
||||
inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
|
||||
@@ -193,6 +208,7 @@ public class ScreenshotView extends FrameLayout implements
|
||||
mScreenshotSelectorView = requireNonNull(findViewById(R.id.global_screenshot_selector));
|
||||
mShareChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_share_chip));
|
||||
mEditChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_edit_chip));
|
||||
mScrollChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_scroll_chip));
|
||||
|
||||
mScreenshotPreview.setClipToOutline(true);
|
||||
mScreenshotPreview.setOutlineProvider(new ViewOutlineProvider() {
|
||||
@@ -387,7 +403,7 @@ public class ScreenshotView extends FrameLayout implements
|
||||
});
|
||||
chips.add(mShareChip);
|
||||
|
||||
mEditChip.setText(mContext.getString(com.android.internal.R.string.screenshot_edit));
|
||||
mEditChip.setText(mContext.getString(R.string.screenshot_edit_label));
|
||||
mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true);
|
||||
mEditChip.setOnClickListener(v -> {
|
||||
mEditChip.setIsPending(true);
|
||||
@@ -402,6 +418,11 @@ public class ScreenshotView extends FrameLayout implements
|
||||
mPendingInteraction = PendingInteraction.PREVIEW;
|
||||
});
|
||||
|
||||
mScrollChip.setText(mContext.getString(R.string.screenshot_scroll_label));
|
||||
mScrollChip.setIcon(Icon.createWithResource(mContext,
|
||||
R.drawable.ic_screenshot_scroll), true);
|
||||
chips.add(mScrollChip);
|
||||
|
||||
// remove the margin from the last chip so that it's correctly aligned with the end
|
||||
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)
|
||||
mActionsView.getChildAt(0).getLayoutParams();
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package com.android.systemui.screenshot;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import android.annotation.UiContext;
|
||||
import android.app.ActivityTaskManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.HardwareBuffer;
|
||||
import android.media.Image;
|
||||
import android.media.ImageReader;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import android.view.IScrollCaptureCallbacks;
|
||||
import android.view.IScrollCaptureConnection;
|
||||
import android.view.IWindowManager;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.view.ScrollCaptureViewSupport;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* High level interface to scroll capture API.
|
||||
*/
|
||||
public class ScrollCaptureClient {
|
||||
|
||||
@VisibleForTesting
|
||||
static final int MATCH_ANY_TASK = ActivityTaskManager.INVALID_TASK_ID;
|
||||
|
||||
private static final String TAG = "ScrollCaptureClient";
|
||||
|
||||
/** Whether to log method names and arguments for most calls */
|
||||
private static final boolean DEBUG_TRACE = false;
|
||||
|
||||
/**
|
||||
* A connection to a remote window. Starts a capture session.
|
||||
*/
|
||||
public interface Connection {
|
||||
/**
|
||||
* Session start should be deferred until UI is active because of resource allocation and
|
||||
* potential visible side effects in the target window.
|
||||
*
|
||||
* @param maxBuffers the maximum number of buffers (tiles) that may be in use at one
|
||||
* time, tiles are not cached anywhere so set this to a large enough
|
||||
* number to retain offscreen content until it is no longer needed
|
||||
* @param sessionConsumer listener to receive the session once active
|
||||
*/
|
||||
void start(int maxBuffers, Consumer<Session> sessionConsumer);
|
||||
|
||||
/**
|
||||
* Close the connection.
|
||||
*/
|
||||
void close();
|
||||
}
|
||||
|
||||
static class CaptureResult {
|
||||
public final Image image;
|
||||
/**
|
||||
* The area requested, in content rect space, relative to scroll-bounds.
|
||||
*/
|
||||
public final Rect requested;
|
||||
/**
|
||||
* The actual area captured, in content rect space, relative to scroll-bounds. This may be
|
||||
* cropped or empty depending on available content.
|
||||
*/
|
||||
public final Rect captured;
|
||||
|
||||
// Error?
|
||||
|
||||
private CaptureResult(Image image, Rect request, Rect captured) {
|
||||
this.image = image;
|
||||
this.requested = request;
|
||||
this.captured = captured;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the connection to a target window and provides a mechanism for requesting tiles.
|
||||
*/
|
||||
interface Session {
|
||||
/**
|
||||
* Request the given horizontal strip. Values are y-coordinates in captured space, relative
|
||||
* to start position.
|
||||
*
|
||||
* @param contentRect the area to capture, in content rect space, relative to scroll-bounds
|
||||
* @param consumer listener to be informed of the result
|
||||
*/
|
||||
void requestTile(Rect contentRect, Consumer<CaptureResult> consumer);
|
||||
|
||||
/**
|
||||
* End the capture session, return the target app to original state. The returned
|
||||
* stage must be waited for to complete to allow the target app a chance to restore to
|
||||
* original state before becoming visible.
|
||||
*
|
||||
* @return a stage presenting the session shutdown
|
||||
*/
|
||||
void end(Runnable listener);
|
||||
|
||||
int getMaxTileHeight();
|
||||
|
||||
int getMaxTileWidth();
|
||||
}
|
||||
|
||||
private final IWindowManager mWindowManagerService;
|
||||
private IBinder mHostWindowToken;
|
||||
|
||||
@Inject
|
||||
public ScrollCaptureClient(@UiContext Context context, IWindowManager windowManagerService) {
|
||||
requireNonNull(context.getDisplay(), "context must be associated with a Display!");
|
||||
mWindowManagerService = windowManagerService;
|
||||
}
|
||||
|
||||
public void setHostWindowToken(IBinder token) {
|
||||
mHostWindowToken = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for scroll capture support.
|
||||
*
|
||||
* @param displayId id for the display containing the target window
|
||||
* @param consumer receives a connection when available
|
||||
*/
|
||||
public void request(int displayId, Consumer<Connection> consumer) {
|
||||
request(displayId, MATCH_ANY_TASK, consumer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for scroll capture support.
|
||||
*
|
||||
* @param displayId id for the display containing the target window
|
||||
* @param taskId id for the task containing the target window or {@link #MATCH_ANY_TASK}.
|
||||
* @param consumer receives a connection when available
|
||||
*/
|
||||
public void request(int displayId, int taskId, Consumer<Connection> consumer) {
|
||||
try {
|
||||
if (DEBUG_TRACE) {
|
||||
Log.d(TAG, "requestScrollCapture(displayId=" + displayId + ", " + mHostWindowToken
|
||||
+ ", taskId=" + taskId + ", consumer=" + consumer + ")");
|
||||
}
|
||||
mWindowManagerService.requestScrollCapture(displayId, mHostWindowToken, taskId,
|
||||
new ControllerCallbacks(consumer));
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Ignored remote exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ControllerCallbacks extends IScrollCaptureCallbacks.Stub implements
|
||||
Connection, Session, IBinder.DeathRecipient {
|
||||
|
||||
private IScrollCaptureConnection mConnection;
|
||||
private Consumer<Connection> mConnectionConsumer;
|
||||
private Consumer<Session> mSessionConsumer;
|
||||
private Consumer<CaptureResult> mResultConsumer;
|
||||
private Runnable mShutdownListener;
|
||||
|
||||
private ImageReader mReader;
|
||||
private Rect mScrollBounds;
|
||||
private Rect mRequestRect;
|
||||
private boolean mStarted;
|
||||
|
||||
private ControllerCallbacks(Consumer<Connection> connectionConsumer) {
|
||||
mConnectionConsumer = connectionConsumer;
|
||||
}
|
||||
|
||||
// IScrollCaptureCallbacks
|
||||
|
||||
@Override
|
||||
public void onConnected(IScrollCaptureConnection connection, Rect scrollBounds,
|
||||
Point positionInWindow) throws RemoteException {
|
||||
if (DEBUG_TRACE) {
|
||||
Log.d(TAG, "onConnected(connection=" + connection + ", scrollBounds=" + scrollBounds
|
||||
+ ", positionInWindow=" + positionInWindow + ")");
|
||||
}
|
||||
mConnection = connection;
|
||||
mConnection.asBinder().linkToDeath(this, 0);
|
||||
mScrollBounds = scrollBounds;
|
||||
mConnectionConsumer.accept(this);
|
||||
mConnectionConsumer = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnavailable() throws RemoteException {
|
||||
if (DEBUG_TRACE) {
|
||||
Log.d(TAG, "onUnavailable");
|
||||
}
|
||||
// The targeted app does not support scroll capture
|
||||
// or the window could not be found... etc etc.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCaptureStarted() {
|
||||
if (DEBUG_TRACE) {
|
||||
Log.d(TAG, "onCaptureStarted()");
|
||||
}
|
||||
mSessionConsumer.accept(this);
|
||||
mSessionConsumer = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCaptureBufferSent(long frameNumber, Rect contentArea) {
|
||||
Image image = null;
|
||||
if (frameNumber != ScrollCaptureViewSupport.NO_FRAME_PRODUCED) {
|
||||
image = mReader.acquireNextImage();
|
||||
}
|
||||
if (DEBUG_TRACE) {
|
||||
Log.d(TAG, "onCaptureBufferSent(frameNumber=" + frameNumber
|
||||
+ ", contentArea=" + contentArea + ") image=" + image);
|
||||
}
|
||||
// Save and clear first, since the consumer will likely request the next
|
||||
// tile, otherwise the new consumer will be wiped out.
|
||||
Consumer<CaptureResult> consumer = mResultConsumer;
|
||||
mResultConsumer = null;
|
||||
consumer.accept(new CaptureResult(image, mRequestRect, contentArea));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionClosed() {
|
||||
if (DEBUG_TRACE) {
|
||||
Log.d(TAG, "onConnectionClosed()");
|
||||
}
|
||||
disconnect();
|
||||
if (mShutdownListener != null) {
|
||||
mShutdownListener.run();
|
||||
mShutdownListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Misc
|
||||
|
||||
private void disconnect() {
|
||||
if (mConnection != null) {
|
||||
mConnection.asBinder().unlinkToDeath(this, 0);
|
||||
}
|
||||
mConnection = null;
|
||||
}
|
||||
|
||||
// ScrollCaptureController.Connection
|
||||
|
||||
// -> Error handling: BiConsumer<Session, Throwable> ?
|
||||
@Override
|
||||
public void start(int maxBufferCount, Consumer<Session> sessionConsumer) {
|
||||
if (DEBUG_TRACE) {
|
||||
Log.d(TAG, "start(maxBufferCount=" + maxBufferCount
|
||||
+ ", sessionConsumer=" + sessionConsumer + ")");
|
||||
}
|
||||
mReader = ImageReader.newInstance(mScrollBounds.width(), mScrollBounds.height(),
|
||||
PixelFormat.RGBA_8888, maxBufferCount, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
|
||||
mSessionConsumer = sessionConsumer;
|
||||
try {
|
||||
mConnection.startCapture(mReader.getSurface());
|
||||
mStarted = true;
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "should not be happening :-(");
|
||||
// ?
|
||||
//mSessionListener.onError(e);
|
||||
//mSessionListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
end(null);
|
||||
}
|
||||
|
||||
// ScrollCaptureController.Session
|
||||
|
||||
@Override
|
||||
public void end(Runnable listener) {
|
||||
if (DEBUG_TRACE) {
|
||||
Log.d(TAG, "end(listener=" + listener + ")");
|
||||
}
|
||||
if (mStarted) {
|
||||
mShutdownListener = listener;
|
||||
try {
|
||||
// listener called from onConnectionClosed callback
|
||||
mConnection.endCapture();
|
||||
} catch (RemoteException e) {
|
||||
Log.d(TAG, "Ignored exception from endCapture()", e);
|
||||
disconnect();
|
||||
listener.run();
|
||||
}
|
||||
} else {
|
||||
disconnect();
|
||||
listener.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxTileHeight() {
|
||||
return mScrollBounds.height();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxTileWidth() {
|
||||
return mScrollBounds.width();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestTile(Rect contentRect, Consumer<CaptureResult> consumer) {
|
||||
if (DEBUG_TRACE) {
|
||||
Log.d(TAG, "requestTile(contentRect=" + contentRect + "consumer=" + consumer + ")");
|
||||
}
|
||||
mRequestRect = new Rect(contentRect);
|
||||
mResultConsumer = consumer;
|
||||
try {
|
||||
mConnection.requestImage(mRequestRect);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Caught remote exception from requestImage", e);
|
||||
// ?
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The process hosting the window went away abruptly!
|
||||
*/
|
||||
@Override
|
||||
public void binderDied() {
|
||||
if (DEBUG_TRACE) {
|
||||
Log.d(TAG, "binderDied()");
|
||||
}
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,46 +16,231 @@
|
||||
|
||||
package com.android.systemui.screenshot;
|
||||
|
||||
import android.os.IBinder;
|
||||
import android.view.IWindowManager;
|
||||
import static android.graphics.ColorSpace.Named.SRGB;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorSpace;
|
||||
import android.graphics.Picture;
|
||||
import android.graphics.Rect;
|
||||
import android.media.ExifInterface;
|
||||
import android.media.Image;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.UserHandle;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.systemui.screenshot.ScrollCaptureClient.Connection;
|
||||
import com.android.systemui.screenshot.ScrollCaptureClient.Session;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.sql.Date;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Stub
|
||||
* Interaction controller between the UI and ScrollCaptureClient.
|
||||
*/
|
||||
public class ScrollCaptureController {
|
||||
private static final String TAG = "ScrollCaptureController";
|
||||
|
||||
public static final int STATUS_A = 0;
|
||||
public static final int STATUS_B = 1;
|
||||
public static final int MAX_PAGES = 5;
|
||||
public static final int MAX_HEIGHT = 12000;
|
||||
|
||||
private final IWindowManager mWindowManagerService;
|
||||
private StatusListener mListener;
|
||||
private final Connection mConnection;
|
||||
private final Context mContext;
|
||||
private Picture mPicture;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param windowManagerService
|
||||
*/
|
||||
@Inject
|
||||
public ScrollCaptureController(IWindowManager windowManagerService) {
|
||||
mWindowManagerService = windowManagerService;
|
||||
}
|
||||
|
||||
interface StatusListener {
|
||||
void onScrollCaptureStatus(boolean available);
|
||||
public ScrollCaptureController(Context context, Connection connection) {
|
||||
mContext = context;
|
||||
mConnection = connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run scroll capture!
|
||||
*
|
||||
* @param window
|
||||
* @param listener
|
||||
* @param after action to take after the flow is complete
|
||||
*/
|
||||
public void getStatus(IBinder window, StatusListener listener) {
|
||||
mListener = listener;
|
||||
// try {
|
||||
// mWindowManagerService.requestScrollCapture(window, new ClientCallbacks());
|
||||
// } catch (RemoteException e) {
|
||||
// }
|
||||
public void run(final Runnable after) {
|
||||
mConnection.start(MAX_PAGES, (session) -> startCapture(session, after));
|
||||
}
|
||||
|
||||
private void startCapture(Session session, final Runnable after) {
|
||||
Rect requestRect = new Rect(0, 0,
|
||||
session.getMaxTileWidth(), session.getMaxTileHeight());
|
||||
Consumer<ScrollCaptureClient.CaptureResult> consumer =
|
||||
new Consumer<ScrollCaptureClient.CaptureResult>() {
|
||||
|
||||
int mFrameCount = 0;
|
||||
|
||||
@Override
|
||||
public void accept(ScrollCaptureClient.CaptureResult result) {
|
||||
mFrameCount++;
|
||||
boolean emptyFrame = result.captured.height() == 0;
|
||||
if (!emptyFrame) {
|
||||
mPicture = stackBelow(mPicture, result.image, result.captured.width(),
|
||||
result.captured.height());
|
||||
}
|
||||
if (emptyFrame || mFrameCount > MAX_PAGES
|
||||
|| requestRect.bottom > MAX_HEIGHT) {
|
||||
Uri uri = null;
|
||||
if (mPicture != null) {
|
||||
// This is probably on a binder thread right now ¯\_(ツ)_/¯
|
||||
uri = writeImage(Bitmap.createBitmap(mPicture));
|
||||
// Release those buffers!
|
||||
mPicture.close();
|
||||
}
|
||||
if (uri != null) {
|
||||
launchViewer(uri);
|
||||
} else {
|
||||
Toast.makeText(mContext, "Failed to create tall screenshot",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
session.end(after); // end session, close connection, after.run()
|
||||
return;
|
||||
}
|
||||
requestRect.offset(0, session.getMaxTileHeight());
|
||||
session.requestTile(requestRect, /* consumer */ this);
|
||||
}
|
||||
};
|
||||
|
||||
// fire it up!
|
||||
session.requestTile(requestRect, consumer);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Combine the top {@link Picture} with an {@link Image} by appending the image directly
|
||||
* below, creating a result that is the combined height of both.
|
||||
* <p>
|
||||
* Note: no pixel data is transferred here, only a record of drawing commands. Backing
|
||||
* hardware buffers must not be modified/recycled until the picture is
|
||||
* {@link Picture#close closed}.
|
||||
*
|
||||
* @param top the existing picture
|
||||
* @param below the image to append below
|
||||
* @param cropWidth the width of the pixel data to use from the image
|
||||
* @param cropHeight the height of the pixel data to use from the image
|
||||
*
|
||||
* @return a new Picture which draws the previous picture with the image below it
|
||||
*/
|
||||
private static Picture stackBelow(Picture top, Image below, int cropWidth, int cropHeight) {
|
||||
int width = cropWidth;
|
||||
int height = cropHeight;
|
||||
if (top != null) {
|
||||
height += top.getHeight();
|
||||
width = Math.max(width, top.getWidth());
|
||||
}
|
||||
Picture combined = new Picture();
|
||||
Canvas canvas = combined.beginRecording(width, height);
|
||||
int y = 0;
|
||||
if (top != null) {
|
||||
canvas.drawPicture(top, new Rect(0, 0, top.getWidth(), top.getHeight()));
|
||||
y += top.getHeight();
|
||||
}
|
||||
canvas.drawBitmap(Bitmap.wrapHardwareBuffer(
|
||||
below.getHardwareBuffer(), ColorSpace.get(SRGB)), 0, y, null);
|
||||
combined.endRecording();
|
||||
return combined;
|
||||
}
|
||||
|
||||
Uri writeImage(Bitmap image) {
|
||||
ContentResolver resolver = mContext.getContentResolver();
|
||||
long mImageTime = System.currentTimeMillis();
|
||||
String imageDate = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(mImageTime));
|
||||
String mImageFileName = String.format("tall_Screenshot_%s.png", imageDate);
|
||||
String mScreenshotId = String.format("Screenshot_%s", UUID.randomUUID());
|
||||
try {
|
||||
// Save the screenshot to the MediaStore
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES
|
||||
+ File.separator + Environment.DIRECTORY_SCREENSHOTS);
|
||||
values.put(MediaStore.MediaColumns.DISPLAY_NAME, mImageFileName);
|
||||
values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png");
|
||||
values.put(MediaStore.MediaColumns.DATE_ADDED, mImageTime / 1000);
|
||||
values.put(MediaStore.MediaColumns.DATE_MODIFIED, mImageTime / 1000);
|
||||
values.put(
|
||||
MediaStore.MediaColumns.DATE_EXPIRES,
|
||||
(mImageTime + DateUtils.DAY_IN_MILLIS) / 1000);
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 1);
|
||||
|
||||
final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
values);
|
||||
try {
|
||||
try (OutputStream out = resolver.openOutputStream(uri)) {
|
||||
if (!image.compress(Bitmap.CompressFormat.PNG, 100, out)) {
|
||||
throw new IOException("Failed to compress");
|
||||
}
|
||||
}
|
||||
|
||||
// Next, write metadata to help index the screenshot
|
||||
try (ParcelFileDescriptor pfd = resolver.openFile(uri, "rw", null)) {
|
||||
final ExifInterface exif = new ExifInterface(pfd.getFileDescriptor());
|
||||
|
||||
exif.setAttribute(ExifInterface.TAG_SOFTWARE,
|
||||
"Android " + Build.DISPLAY);
|
||||
|
||||
exif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH,
|
||||
Integer.toString(image.getWidth()));
|
||||
exif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH,
|
||||
Integer.toString(image.getHeight()));
|
||||
|
||||
final ZonedDateTime time = ZonedDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(mImageTime), ZoneId.systemDefault());
|
||||
exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL,
|
||||
DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss").format(time));
|
||||
exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
|
||||
DateTimeFormatter.ofPattern("SSS").format(time));
|
||||
|
||||
if (Objects.equals(time.getOffset(), ZoneOffset.UTC)) {
|
||||
exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+00:00");
|
||||
} else {
|
||||
exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL,
|
||||
DateTimeFormatter.ofPattern("XXX").format(time));
|
||||
}
|
||||
exif.saveAttributes();
|
||||
}
|
||||
|
||||
// Everything went well above, publish it!
|
||||
values.clear();
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 0);
|
||||
values.putNull(MediaStore.MediaColumns.DATE_EXPIRES);
|
||||
resolver.update(uri, values, null, null);
|
||||
return uri;
|
||||
} catch (Exception e) {
|
||||
resolver.delete(uri, null);
|
||||
throw e;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "unable to save screenshot", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void launchViewer(Uri uri) {
|
||||
Intent editIntent = new Intent(Intent.ACTION_VIEW);
|
||||
editIntent.setType("image/png");
|
||||
editIntent.setData(uri);
|
||||
editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
mContext.startActivityAsUser(editIntent, UserHandle.CURRENT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,13 @@ public class AAAPlusPlusVerifySysuiRequiredTestPropertiesTest extends SysuiTestC
|
||||
filter.add(s -> s.startsWith("com.android.systemui")
|
||||
|| s.startsWith("com.android.keyguard"));
|
||||
|
||||
// Screenshots run in an isolated process and should not be run
|
||||
// with the main process dependency graph because it will not exist
|
||||
// at runtime and could lead to incorrect tests which assume
|
||||
// the main SystemUI process. Therefore, exclude this package
|
||||
// from the base class whitelist.
|
||||
filter.add(s -> !s.startsWith("com.android.systemui.screenshot"));
|
||||
|
||||
try {
|
||||
return scanner.getClassPathEntries(filter);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package com.android.systemui.screenshot;
|
||||
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.HardwareRenderer;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RecordingCanvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RenderNode;
|
||||
import android.os.RemoteException;
|
||||
import android.view.IScrollCaptureCallbacks;
|
||||
import android.view.IScrollCaptureConnection;
|
||||
import android.view.Surface;
|
||||
|
||||
/**
|
||||
* An IScrollCaptureConnection which returns a sequence of solid filled rectangles in the
|
||||
* locations requested, in alternating colors.
|
||||
*/
|
||||
class FakeScrollCaptureConnection extends IScrollCaptureConnection.Stub {
|
||||
private final int[] mColors = {Color.RED, Color.GREEN, Color.BLUE};
|
||||
private IScrollCaptureCallbacks mCallbacks;
|
||||
private Surface mSurface;
|
||||
private Paint mPaint;
|
||||
private int mNextColor;
|
||||
private HwuiContext mHwuiContext;
|
||||
|
||||
FakeScrollCaptureConnection(IScrollCaptureCallbacks cb) {
|
||||
mCallbacks = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startCapture(Surface surface) {
|
||||
mSurface = surface;
|
||||
mHwuiContext = new HwuiContext(false, surface);
|
||||
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
mPaint.setStyle(Paint.Style.FILL);
|
||||
try {
|
||||
mCallbacks.onCaptureStarted();
|
||||
} catch (RemoteException e) {
|
||||
e.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestImage(Rect rect) {
|
||||
Canvas canvas = mHwuiContext.lockCanvas(rect.width(), rect.height());
|
||||
mPaint.setColor(mColors[mNextColor]);
|
||||
canvas.drawRect(rect, mPaint);
|
||||
mNextColor = (mNextColor++) % mColors.length;
|
||||
long frameNumber = mSurface.getNextFrameNumber();
|
||||
mHwuiContext.unlockAndPost(canvas);
|
||||
try {
|
||||
mCallbacks.onCaptureBufferSent(frameNumber, rect);
|
||||
} catch (RemoteException e) {
|
||||
e.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endCapture() {
|
||||
try {
|
||||
mCallbacks.onConnectionClosed();
|
||||
} catch (RemoteException e) {
|
||||
e.rethrowAsRuntimeException();
|
||||
} finally {
|
||||
mHwuiContext.destroy();
|
||||
mSurface = null;
|
||||
mCallbacks = null;
|
||||
}
|
||||
}
|
||||
|
||||
// From android.view.Surface, but issues render requests synchronously with waitForPresent(true)
|
||||
private static final class HwuiContext {
|
||||
private final RenderNode mRenderNode;
|
||||
private final HardwareRenderer mHardwareRenderer;
|
||||
private RecordingCanvas mCanvas;
|
||||
private final boolean mIsWideColorGamut;
|
||||
|
||||
HwuiContext(boolean isWideColorGamut, Surface surface) {
|
||||
mRenderNode = RenderNode.create("HwuiCanvas", null);
|
||||
mRenderNode.setClipToBounds(false);
|
||||
mRenderNode.setForceDarkAllowed(false);
|
||||
mIsWideColorGamut = isWideColorGamut;
|
||||
|
||||
mHardwareRenderer = new HardwareRenderer();
|
||||
mHardwareRenderer.setContentRoot(mRenderNode);
|
||||
mHardwareRenderer.setSurface(surface, true);
|
||||
mHardwareRenderer.setColorMode(
|
||||
isWideColorGamut
|
||||
? ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
|
||||
: ActivityInfo.COLOR_MODE_DEFAULT);
|
||||
mHardwareRenderer.setLightSourceAlpha(0.0f, 0.0f);
|
||||
mHardwareRenderer.setLightSourceGeometry(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
Canvas lockCanvas(int width, int height) {
|
||||
if (mCanvas != null) {
|
||||
throw new IllegalStateException("Surface was already locked!");
|
||||
}
|
||||
mCanvas = mRenderNode.beginRecording(width, height);
|
||||
return mCanvas;
|
||||
}
|
||||
|
||||
void unlockAndPost(Canvas canvas) {
|
||||
if (canvas != mCanvas) {
|
||||
throw new IllegalArgumentException("canvas object must be the same instance that "
|
||||
+ "was previously returned by lockCanvas");
|
||||
}
|
||||
mRenderNode.endRecording();
|
||||
mCanvas = null;
|
||||
mHardwareRenderer.createRenderRequest()
|
||||
.setVsyncTime(System.nanoTime())
|
||||
.setWaitForPresent(true) // sync!
|
||||
.syncAndDraw();
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
mHardwareRenderer.destroy();
|
||||
}
|
||||
|
||||
boolean isWideColorGamut() {
|
||||
return mIsWideColorGamut;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package com.android.systemui.screenshot;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.timeout;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.os.RemoteException;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.view.Display;
|
||||
import android.view.IScrollCaptureCallbacks;
|
||||
import android.view.IWindowManager;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
import com.android.systemui.screenshot.ScrollCaptureClient.CaptureResult;
|
||||
import com.android.systemui.screenshot.ScrollCaptureClient.Connection;
|
||||
import com.android.systemui.screenshot.ScrollCaptureClient.Session;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
public class ScrollCaptureClientTest extends SysuiTestCase {
|
||||
private Context mContext;
|
||||
private IWindowManager mWm;
|
||||
|
||||
@Spy private TestableConsumer<Session> mSessionConsumer;
|
||||
@Spy private TestableConsumer<Connection> mConnectionConsumer;
|
||||
@Spy private TestableConsumer<CaptureResult> mResultConsumer;
|
||||
@Mock private Runnable mRunnable;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
Context context = InstrumentationRegistry.getInstrumentation().getContext();
|
||||
DisplayManager displayManager = requireNonNull(
|
||||
context.getSystemService(DisplayManager.class));
|
||||
mContext = context.createDisplayContext(
|
||||
displayManager.getDisplay(Display.DEFAULT_DISPLAY));
|
||||
mWm = mock(IWindowManager.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBasicClientFlow() throws RemoteException {
|
||||
doAnswer((Answer<Void>) invocation -> {
|
||||
IScrollCaptureCallbacks cb = invocation.getArgument(3);
|
||||
cb.onConnected(
|
||||
new FakeScrollCaptureConnection(cb),
|
||||
/* scrollBounds */ new Rect(0, 0, 100, 100),
|
||||
/* positionInWindow */ new Point(0, 0));
|
||||
return null;
|
||||
}).when(mWm).requestScrollCapture(/* displayId */ anyInt(), /* token */ isNull(),
|
||||
/* taskId */ anyInt(), any(IScrollCaptureCallbacks.class));
|
||||
|
||||
// Create client
|
||||
ScrollCaptureClient client = new ScrollCaptureClient(mContext, mWm);
|
||||
|
||||
client.request(Display.DEFAULT_DISPLAY, mConnectionConsumer);
|
||||
verify(mConnectionConsumer, timeout(100)).accept(any(Connection.class));
|
||||
|
||||
Connection conn = mConnectionConsumer.getValue();
|
||||
|
||||
conn.start(5, mSessionConsumer);
|
||||
verify(mSessionConsumer, timeout(100)).accept(any(Session.class));
|
||||
|
||||
Session session = mSessionConsumer.getValue();
|
||||
Rect request = new Rect(0, 0, session.getMaxTileWidth(), session.getMaxTileHeight());
|
||||
|
||||
session.requestTile(request, mResultConsumer);
|
||||
verify(mResultConsumer, timeout(100)).accept(any(CaptureResult.class));
|
||||
|
||||
CaptureResult result = mResultConsumer.getValue();
|
||||
assertThat(result.requested).isEqualTo(request);
|
||||
assertThat(result.captured).isEqualTo(result.requested);
|
||||
assertThat(result.image).isNotNull();
|
||||
|
||||
session.end(mRunnable);
|
||||
verify(mRunnable, timeout(100)).run();
|
||||
|
||||
// TODO verify image
|
||||
// TODO test threading
|
||||
// TODO test failures
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package com.android.systemui.screenshot;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/** Accepts and retains the most recent value for verification */
|
||||
class TestableConsumer<T> implements Consumer<T> {
|
||||
T mValue;
|
||||
|
||||
@Override
|
||||
public void accept(T t) {
|
||||
mValue = t;
|
||||
}
|
||||
|
||||
public T getValue() {
|
||||
return mValue;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user