blob: 1a8b43403b259d758abae2952ecb00c85192739d [file] [log] [blame]
/*
* 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.server.wm;
import static android.service.displayhash.DisplayHashingService.EXTRA_INTERVAL_BETWEEN_REQUESTS;
import static android.service.displayhash.DisplayHashingService.EXTRA_VERIFIED_DISPLAY_HASH;
import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_HASH_ALGORITHM;
import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_TOO_MANY_REQUESTS;
import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_UNKNOWN;
import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH_ERROR_CODE;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import android.Manifest;
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.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.HardwareBuffer;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.service.displayhash.DisplayHashParams;
import android.service.displayhash.DisplayHashingService;
import android.service.displayhash.IDisplayHashingService;
import android.util.Size;
import android.util.Slog;
import android.view.MagnificationSpec;
import android.view.displayhash.DisplayHash;
import android.view.displayhash.VerifiedDisplayHash;
import android.window.ScreenCapture;
import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
/**
* Handles requests into {@link DisplayHashingService}
*
* Do not hold the {@link WindowManagerService#mGlobalLock} when calling methods since they are
* blocking calls into another service.
*/
public class DisplayHashController {
private static final String TAG = TAG_WITH_CLASS_NAME ? "DisplayHashController" : TAG_WM;
private static final boolean DEBUG = false;
private final Object mServiceConnectionLock = new Object();
@GuardedBy("mServiceConnectionLock")
private DisplayHashingServiceConnection mServiceConnection;
private final Context mContext;
/**
* Lock used for the cached {@link #mDisplayHashAlgorithms} map
*/
private final Object mDisplayHashAlgorithmsLock = new Object();
/**
* The cached map of display hash algorithms to the {@link DisplayHashParams}
*/
@GuardedBy("mDisplayHashAlgorithmsLock")
private Map<String, DisplayHashParams> mDisplayHashAlgorithms;
private final Handler mHandler;
private final byte[] mSalt;
private final float[] mTmpFloat9 = new float[9];
private final Matrix mTmpMatrix = new Matrix();
private final RectF mTmpRectF = new RectF();
/**
* Lock used for the cached {@link #mIntervalBetweenRequestMillis}
*/
private final Object mIntervalBetweenRequestsLock = new Object();
/**
* Specified duration between requests to generate a display hash in milliseconds. Requests
* faster than this delay will be throttled.
*/
@GuardedBy("mDurationBetweenRequestsLock")
private int mIntervalBetweenRequestMillis = -1;
/**
* The last time an app requested to generate a display hash in System time.
*/
private long mLastRequestTimeMs;
/**
* The last uid that requested to generate a hash.
*/
private int mLastRequestUid;
/**
* Only used for testing. Throttling should always be enabled unless running tests
*/
private boolean mDisplayHashThrottlingEnabled = true;
private interface Command {
void run(IDisplayHashingService service) throws RemoteException;
}
DisplayHashController(Context context) {
mContext = context;
mHandler = new Handler(Looper.getMainLooper());
mSalt = UUID.randomUUID().toString().getBytes();
}
String[] getSupportedHashAlgorithms() {
Map<String, DisplayHashParams> displayHashAlgorithms = getDisplayHashAlgorithms();
return displayHashAlgorithms.keySet().toArray(new String[0]);
}
@Nullable
VerifiedDisplayHash verifyDisplayHash(DisplayHash displayHash) {
final SyncCommand syncCommand = new SyncCommand();
Bundle results = syncCommand.run((service, remoteCallback) -> {
try {
service.verifyDisplayHash(mSalt, displayHash, remoteCallback);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to invoke verifyDisplayHash command");
}
});
return results.getParcelable(EXTRA_VERIFIED_DISPLAY_HASH);
}
void setDisplayHashThrottlingEnabled(boolean enable) {
mDisplayHashThrottlingEnabled = enable;
}
private void generateDisplayHash(HardwareBuffer buffer, Rect bounds,
String hashAlgorithm, RemoteCallback callback) {
connectAndRun(
service -> service.generateDisplayHash(mSalt, buffer, bounds, hashAlgorithm,
callback));
}
private boolean allowedToGenerateHash(int uid) {
if (!mDisplayHashThrottlingEnabled) {
// Always allow to generate the hash. This is used to allow tests to run without
// waiting on the designated threshold.
return true;
}
long currentTime = System.currentTimeMillis();
if (mLastRequestUid != uid) {
mLastRequestUid = uid;
mLastRequestTimeMs = currentTime;
return true;
}
int mIntervalBetweenRequestsMs = getIntervalBetweenRequestMillis();
if (currentTime - mLastRequestTimeMs < mIntervalBetweenRequestsMs) {
return false;
}
mLastRequestTimeMs = currentTime;
return true;
}
void generateDisplayHash(ScreenCapture.LayerCaptureArgs.Builder args,
Rect boundsInWindow, String hashAlgorithm, int uid, RemoteCallback callback) {
if (!allowedToGenerateHash(uid)) {
sendDisplayHashError(callback, DISPLAY_HASH_ERROR_TOO_MANY_REQUESTS);
return;
}
final Map<String, DisplayHashParams> displayHashAlgorithmsMap = getDisplayHashAlgorithms();
DisplayHashParams displayHashParams = displayHashAlgorithmsMap.get(hashAlgorithm);
if (displayHashParams == null) {
Slog.w(TAG, "Failed to generateDisplayHash. Invalid hashAlgorithm");
sendDisplayHashError(callback, DISPLAY_HASH_ERROR_INVALID_HASH_ALGORITHM);
return;
}
Size size = displayHashParams.getBufferSize();
if (size != null && (size.getWidth() > 0 || size.getHeight() > 0)) {
args.setFrameScale((float) size.getWidth() / boundsInWindow.width(),
(float) size.getHeight() / boundsInWindow.height());
}
args.setGrayscale(displayHashParams.isGrayscaleBuffer());
ScreenCapture.ScreenshotHardwareBuffer screenshotHardwareBuffer =
ScreenCapture.captureLayers(args.build());
if (screenshotHardwareBuffer == null
|| screenshotHardwareBuffer.getHardwareBuffer() == null) {
Slog.w(TAG, "Failed to generate DisplayHash. Couldn't capture content");
sendDisplayHashError(callback, DISPLAY_HASH_ERROR_UNKNOWN);
return;
}
generateDisplayHash(screenshotHardwareBuffer.getHardwareBuffer(), boundsInWindow,
hashAlgorithm, callback);
}
private Map<String, DisplayHashParams> getDisplayHashAlgorithms() {
// We have a separate lock for the hashing params to ensure we can properly cache the
// hashing params so we don't need to call into the ExtServices process for each request.
synchronized (mDisplayHashAlgorithmsLock) {
if (mDisplayHashAlgorithms != null) {
return mDisplayHashAlgorithms;
}
final SyncCommand syncCommand = new SyncCommand();
Bundle results = syncCommand.run((service, remoteCallback) -> {
try {
service.getDisplayHashAlgorithms(remoteCallback);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to invoke getDisplayHashAlgorithms command", e);
}
});
mDisplayHashAlgorithms = new HashMap<>(results.size());
for (String key : results.keySet()) {
mDisplayHashAlgorithms.put(key, results.getParcelable(key));
}
return mDisplayHashAlgorithms;
}
}
void sendDisplayHashError(RemoteCallback callback, int errorCode) {
Bundle bundle = new Bundle();
bundle.putInt(EXTRA_DISPLAY_HASH_ERROR_CODE, errorCode);
callback.sendResult(bundle);
}
/**
* Calculate the bounds to generate the hash for. This takes into account window transform,
* magnification, and display bounds.
*
* Call while holding {@link WindowManagerService#mGlobalLock}
*
* @param win Window that the DisplayHash is generated for.
* @param boundsInWindow The bounds in the window where to generate the hash.
* @param outBounds The result of the calculated bounds
*/
void calculateDisplayHashBoundsLocked(WindowState win, Rect boundsInWindow,
Rect outBounds) {
if (DEBUG) {
Slog.d(TAG,
"calculateDisplayHashBoundsLocked: boundsInWindow=" + boundsInWindow);
}
outBounds.set(boundsInWindow);
DisplayContent displayContent = win.getDisplayContent();
if (displayContent == null) {
return;
}
// Intersect boundsInWindow with the window to make sure it's not outside the window
// requesting the token. Offset the window bounds to 0,0 since the boundsInWindow are
// offset from the window location, not display.
final Rect windowBounds = new Rect();
win.getBounds(windowBounds);
windowBounds.offsetTo(0, 0);
outBounds.intersectUnchecked(windowBounds);
if (DEBUG) {
Slog.d(TAG,
"calculateDisplayHashBoundsLocked: boundsIntersectWindow=" + outBounds);
}
if (outBounds.isEmpty()) {
return;
}
// Transform the bounds using the window transform in case there's a scale or offset.
// This allows the bounds to be in display space.
win.getTransformationMatrix(mTmpFloat9, mTmpMatrix);
mTmpRectF.set(outBounds);
mTmpMatrix.mapRect(mTmpRectF, mTmpRectF);
outBounds.set((int) mTmpRectF.left, (int) mTmpRectF.top, (int) mTmpRectF.right,
(int) mTmpRectF.bottom);
if (DEBUG) {
Slog.d(TAG, "calculateDisplayHashBoundsLocked: boundsInDisplay=" + outBounds);
}
// Apply the magnification spec values to the bounds since the content could be magnified
final MagnificationSpec magSpec = displayContent.getMagnificationSpec();
if (magSpec != null) {
outBounds.scale(magSpec.scale);
outBounds.offset((int) magSpec.offsetX, (int) magSpec.offsetY);
}
if (DEBUG) {
Slog.d(TAG, "calculateDisplayHashBoundsLocked: boundsWithMagnification="
+ outBounds);
}
if (outBounds.isEmpty()) {
return;
}
// Intersect with the display bounds since content outside the display are not visible to
// the user.
final Rect displayBounds = displayContent.getBounds();
outBounds.intersectUnchecked(displayBounds);
if (DEBUG) {
Slog.d(TAG, "calculateDisplayHashBoundsLocked: finalBounds=" + outBounds);
}
}
private int getIntervalBetweenRequestMillis() {
// We have a separate lock for the hashing params to ensure we can properly cache the
// hashing params so we don't need to call into the ExtServices process for each request.
synchronized (mIntervalBetweenRequestsLock) {
if (mIntervalBetweenRequestMillis != -1) {
return mIntervalBetweenRequestMillis;
}
final SyncCommand syncCommand = new SyncCommand();
Bundle results = syncCommand.run((service, remoteCallback) -> {
try {
service.getIntervalBetweenRequestsMillis(remoteCallback);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to invoke getDisplayHashAlgorithms command", e);
}
});
mIntervalBetweenRequestMillis = results.getInt(EXTRA_INTERVAL_BETWEEN_REQUESTS, 0);
return mIntervalBetweenRequestMillis;
}
}
/**
* Run a command, starting the service connection if necessary.
*/
private void connectAndRun(@NonNull Command command) {
synchronized (mServiceConnectionLock) {
mHandler.resetTimeoutMessage();
if (mServiceConnection == null) {
if (DEBUG) Slog.v(TAG, "creating connection");
final ComponentName component = getServiceComponentName();
if (DEBUG) Slog.v(TAG, "binding to: " + component);
if (component != null) {
final Intent intent = new Intent();
intent.setComponent(component);
final long token = Binder.clearCallingIdentity();
try {
// Create the connection
mServiceConnection = new DisplayHashingServiceConnection();
mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
if (DEBUG) Slog.v(TAG, "bound");
} finally {
Binder.restoreCallingIdentity(token);
}
}
}
if (mServiceConnection != null) {
mServiceConnection.runCommandLocked(command);
}
}
}
@Nullable
private ServiceInfo getServiceInfo() {
final String packageName =
mContext.getPackageManager().getServicesSystemSharedLibraryPackageName();
if (packageName == null) {
Slog.w(TAG, "no external services package!");
return null;
}
final Intent intent = new Intent(DisplayHashingService.SERVICE_INTERFACE);
intent.setPackage(packageName);
final ResolveInfo resolveInfo;
final long token = Binder.clearCallingIdentity();
try {
resolveInfo = mContext.getPackageManager().resolveService(intent,
PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
} finally {
Binder.restoreCallingIdentity(token);
}
if (resolveInfo == null || resolveInfo.serviceInfo == null) {
Slog.w(TAG, "No valid components found.");
return null;
}
return resolveInfo.serviceInfo;
}
@Nullable
private ComponentName getServiceComponentName() {
final ServiceInfo serviceInfo = getServiceInfo();
if (serviceInfo == null) return null;
final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name);
if (!Manifest.permission.BIND_DISPLAY_HASHING_SERVICE
.equals(serviceInfo.permission)) {
Slog.w(TAG, name.flattenToShortString() + " requires permission "
+ Manifest.permission.BIND_DISPLAY_HASHING_SERVICE);
return null;
}
if (DEBUG) Slog.v(TAG, "getServiceComponentName(): " + name);
return name;
}
private class SyncCommand {
private static final int WAIT_TIME_S = 5;
private Bundle mResult;
private final CountDownLatch mCountDownLatch = new CountDownLatch(1);
public Bundle run(BiConsumer<IDisplayHashingService, RemoteCallback> func) {
connectAndRun(service -> {
RemoteCallback callback = new RemoteCallback(result -> {
mResult = result;
mCountDownLatch.countDown();
});
func.accept(service, callback);
});
try {
mCountDownLatch.await(WAIT_TIME_S, TimeUnit.SECONDS);
} catch (Exception e) {
Slog.e(TAG, "Failed to wait for command", e);
}
return mResult;
}
}
private class DisplayHashingServiceConnection implements ServiceConnection {
@GuardedBy("mServiceConnectionLock")
private IDisplayHashingService mRemoteService;
@GuardedBy("mServiceConnectionLock")
private ArrayList<Command> mQueuedCommands;
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
if (DEBUG) Slog.v(TAG, "onServiceConnected(): " + name);
synchronized (mServiceConnectionLock) {
mRemoteService = IDisplayHashingService.Stub.asInterface(service);
if (mQueuedCommands != null) {
final int size = mQueuedCommands.size();
if (DEBUG) Slog.d(TAG, "running " + size + " queued commands");
for (int i = 0; i < size; i++) {
final Command queuedCommand = mQueuedCommands.get(i);
try {
if (DEBUG) Slog.v(TAG, "running queued command #" + i);
queuedCommand.run(mRemoteService);
} catch (RemoteException e) {
Slog.w(TAG, "exception calling " + name + ": " + e);
}
}
mQueuedCommands = null;
} else if (DEBUG) {
Slog.d(TAG, "no queued commands");
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
if (DEBUG) Slog.v(TAG, "onServiceDisconnected(): " + name);
synchronized (mServiceConnectionLock) {
mRemoteService = null;
}
}
@Override
public void onBindingDied(ComponentName name) {
if (DEBUG) Slog.v(TAG, "onBindingDied(): " + name);
synchronized (mServiceConnectionLock) {
mRemoteService = null;
}
}
@Override
public void onNullBinding(ComponentName name) {
if (DEBUG) Slog.v(TAG, "onNullBinding(): " + name);
synchronized (mServiceConnectionLock) {
mRemoteService = null;
}
}
/**
* Only call while holding {@link #mServiceConnectionLock}
*/
private void runCommandLocked(Command command) {
if (mRemoteService == null) {
if (DEBUG) Slog.d(TAG, "service is null; queuing command");
if (mQueuedCommands == null) {
mQueuedCommands = new ArrayList<>(1);
}
mQueuedCommands.add(command);
} else {
try {
if (DEBUG) Slog.v(TAG, "running command right away");
command.run(mRemoteService);
} catch (RemoteException e) {
Slog.w(TAG, "exception calling service: " + e);
}
}
}
}
private class Handler extends android.os.Handler {
static final long SERVICE_SHUTDOWN_TIMEOUT_MILLIS = 10000; // 10s
static final int MSG_SERVICE_SHUTDOWN_TIMEOUT = 1;
Handler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_SERVICE_SHUTDOWN_TIMEOUT) {
if (DEBUG) {
Slog.v(TAG, "Shutting down service");
}
synchronized (mServiceConnectionLock) {
if (mServiceConnection != null) {
mContext.unbindService(mServiceConnection);
mServiceConnection = null;
}
}
}
}
/**
* Set a timer for {@link #SERVICE_SHUTDOWN_TIMEOUT_MILLIS} so we can tear down the service
* if it's inactive. The requests will be coming from apps so it's hard to tell how often
* the requests can come in. Therefore, we leave the service running if requests continue
* to come in. Once there's been no activity for 10s, we can shut down the service and
* restart when we get a new request.
*/
void resetTimeoutMessage() {
if (DEBUG) {
Slog.v(TAG, "Reset shutdown message");
}
removeMessages(MSG_SERVICE_SHUTDOWN_TIMEOUT);
sendEmptyMessageDelayed(MSG_SERVICE_SHUTDOWN_TIMEOUT, SERVICE_SHUTDOWN_TIMEOUT_MILLIS);
}
}
}