blob: d24d78c6f3dab793598b1b5263cef15853cfca0b [file] [log] [blame]
package com.android.internal.util;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import java.util.function.Consumer;
public class ScreenshotHelper {
private static final String TAG = "ScreenshotHelper";
private static final String SYSUI_PACKAGE = "com.android.systemui";
private static final String SYSUI_SCREENSHOT_SERVICE =
"com.android.systemui.screenshot.TakeScreenshotService";
private static final String SYSUI_SCREENSHOT_ERROR_RECEIVER =
"com.android.systemui.screenshot.ScreenshotServiceErrorReceiver";
// Time until we give up on the screenshot & show an error instead.
private final int SCREENSHOT_TIMEOUT_MS = 10000;
private final Object mScreenshotLock = new Object();
private ServiceConnection mScreenshotConnection = null;
private final Context mContext;
public ScreenshotHelper(Context context) {
mContext = context;
}
/**
* Request a screenshot be taken with a specific timeout.
*
* Added to support reducing unit test duration; the method variant without a timeout argument
* is recommended for general use.
*
* @param screenshotType The type of screenshot, for example either
* {@link android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN}
* or
* {@link android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION}
* @param hasStatus {@code true} if the status bar is currently showing. {@code false}
* if
* not.
* @param hasNav {@code true} if the navigation bar is currently showing. {@code
* false}
* if not.
* @param handler A handler used in case the screenshot times out
* @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the
* screenshot was taken.
*/
public void takeScreenshot(final int screenshotType, final boolean hasStatus,
final boolean hasNav, @NonNull Handler handler,
@Nullable Consumer<Uri> completionConsumer) {
takeScreenshot(screenshotType, hasStatus, hasNav, SCREENSHOT_TIMEOUT_MS, handler,
completionConsumer);
}
/**
* Request a screenshot be taken.
*
* Added to support reducing unit test duration; the method variant without a timeout argument
* is recommended for general use.
*
* @param screenshotType The type of screenshot, for example either
* {@link android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN}
* or
* {@link android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION}
* @param hasStatus {@code true} if the status bar is currently showing. {@code false}
* if
* not.
* @param hasNav {@code true} if the navigation bar is currently showing. {@code
* false}
* if not.
* @param timeoutMs If the screenshot hasn't been completed within this time period,
* the screenshot attempt will be cancelled and `completionConsumer`
* will be run.
* @param handler A handler used in case the screenshot times out
* @param completionConsumer Consumes `null` if a screenshot was not taken, and the URI of the
* screenshot if the screenshot was taken.
*/
public void takeScreenshot(final int screenshotType, final boolean hasStatus,
final boolean hasNav, long timeoutMs, @NonNull Handler handler,
@Nullable Consumer<Uri> completionConsumer) {
synchronized (mScreenshotLock) {
if (mScreenshotConnection != null) {
return;
}
final ComponentName serviceComponent = new ComponentName(SYSUI_PACKAGE,
SYSUI_SCREENSHOT_SERVICE);
final Intent serviceIntent = new Intent();
final Runnable mScreenshotTimeout = new Runnable() {
@Override
public void run() {
synchronized (mScreenshotLock) {
if (mScreenshotConnection != null) {
mContext.unbindService(mScreenshotConnection);
mScreenshotConnection = null;
notifyScreenshotError();
}
}
if (completionConsumer != null) {
completionConsumer.accept(null);
}
}
};
serviceIntent.setComponent(serviceComponent);
ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mScreenshotLock) {
if (mScreenshotConnection != this) {
return;
}
Messenger messenger = new Messenger(service);
Message msg = Message.obtain(null, screenshotType);
final ServiceConnection myConn = this;
Handler h = new Handler(handler.getLooper()) {
@Override
public void handleMessage(Message msg) {
synchronized (mScreenshotLock) {
if (mScreenshotConnection == myConn) {
mContext.unbindService(mScreenshotConnection);
mScreenshotConnection = null;
handler.removeCallbacks(mScreenshotTimeout);
}
}
if (completionConsumer != null) {
completionConsumer.accept((Uri) msg.obj);
}
}
};
msg.replyTo = new Messenger(h);
msg.arg1 = hasStatus ? 1 : 0;
msg.arg2 = hasNav ? 1 : 0;
try {
messenger.send(msg);
} catch (RemoteException e) {
Log.e(TAG, "Couldn't take screenshot: " + e);
if (completionConsumer != null) {
completionConsumer.accept(null);
}
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (mScreenshotLock) {
if (mScreenshotConnection != null) {
mContext.unbindService(mScreenshotConnection);
mScreenshotConnection = null;
handler.removeCallbacks(mScreenshotTimeout);
notifyScreenshotError();
}
}
}
};
if (mContext.bindServiceAsUser(serviceIntent, conn,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
UserHandle.CURRENT)) {
mScreenshotConnection = conn;
handler.postDelayed(mScreenshotTimeout, timeoutMs);
}
}
}
/**
* Notifies the screenshot service to show an error.
*/
private void notifyScreenshotError() {
// If the service process is killed, then ask it to clean up after itself
final ComponentName errorComponent = new ComponentName(SYSUI_PACKAGE,
SYSUI_SCREENSHOT_ERROR_RECEIVER);
// Broadcast needs to have a valid action. We'll just pick
// a generic one, since the receiver here doesn't care.
Intent errorIntent = new Intent(Intent.ACTION_USER_PRESENT);
errorIntent.setComponent(errorComponent);
errorIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT |
Intent.FLAG_RECEIVER_FOREGROUND);
mContext.sendBroadcastAsUser(errorIntent, UserHandle.CURRENT);
}
}