1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.wm; 18 19 import static android.service.displayhash.DisplayHashingService.EXTRA_INTERVAL_BETWEEN_REQUESTS; 20 import static android.service.displayhash.DisplayHashingService.EXTRA_VERIFIED_DISPLAY_HASH; 21 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_HASH_ALGORITHM; 22 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_TOO_MANY_REQUESTS; 23 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_UNKNOWN; 24 import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH_ERROR_CODE; 25 26 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; 27 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; 28 29 import android.Manifest; 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.content.ComponentName; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.content.ServiceConnection; 36 import android.content.pm.PackageManager; 37 import android.content.pm.ResolveInfo; 38 import android.content.pm.ServiceInfo; 39 import android.graphics.Matrix; 40 import android.graphics.Rect; 41 import android.graphics.RectF; 42 import android.hardware.HardwareBuffer; 43 import android.os.Binder; 44 import android.os.Bundle; 45 import android.os.IBinder; 46 import android.os.Looper; 47 import android.os.Message; 48 import android.os.RemoteCallback; 49 import android.os.RemoteException; 50 import android.service.displayhash.DisplayHashParams; 51 import android.service.displayhash.DisplayHashingService; 52 import android.service.displayhash.IDisplayHashingService; 53 import android.util.Size; 54 import android.util.Slog; 55 import android.view.MagnificationSpec; 56 import android.view.SurfaceControl; 57 import android.view.displayhash.DisplayHash; 58 import android.view.displayhash.VerifiedDisplayHash; 59 60 import com.android.internal.annotations.GuardedBy; 61 62 import java.util.ArrayList; 63 import java.util.HashMap; 64 import java.util.Map; 65 import java.util.UUID; 66 import java.util.concurrent.CountDownLatch; 67 import java.util.concurrent.TimeUnit; 68 import java.util.function.BiConsumer; 69 70 /** 71 * Handles requests into {@link DisplayHashingService} 72 * 73 * Do not hold the {@link WindowManagerService#mGlobalLock} when calling methods since they are 74 * blocking calls into another service. 75 */ 76 public class DisplayHashController { 77 private static final String TAG = TAG_WITH_CLASS_NAME ? "DisplayHashController" : TAG_WM; 78 private static final boolean DEBUG = false; 79 80 private final Object mServiceConnectionLock = new Object(); 81 82 @GuardedBy("mServiceConnectionLock") 83 private DisplayHashingServiceConnection mServiceConnection; 84 85 private final Context mContext; 86 87 /** 88 * Lock used for the cached {@link #mDisplayHashAlgorithms} map 89 */ 90 private final Object mDisplayHashAlgorithmsLock = new Object(); 91 92 /** 93 * The cached map of display hash algorithms to the {@link DisplayHashParams} 94 */ 95 @GuardedBy("mDisplayHashAlgorithmsLock") 96 private Map<String, DisplayHashParams> mDisplayHashAlgorithms; 97 98 private final Handler mHandler; 99 100 private final byte[] mSalt; 101 102 private final float[] mTmpFloat9 = new float[9]; 103 private final Matrix mTmpMatrix = new Matrix(); 104 private final RectF mTmpRectF = new RectF(); 105 106 107 /** 108 * Lock used for the cached {@link #mIntervalBetweenRequestMillis} 109 */ 110 private final Object mIntervalBetweenRequestsLock = new Object(); 111 112 /** 113 * Specified duration between requests to generate a display hash in milliseconds. Requests 114 * faster than this delay will be throttled. 115 */ 116 @GuardedBy("mDurationBetweenRequestsLock") 117 private int mIntervalBetweenRequestMillis = -1; 118 119 /** 120 * The last time an app requested to generate a display hash in System time. 121 */ 122 private long mLastRequestTimeMs; 123 124 /** 125 * The last uid that requested to generate a hash. 126 */ 127 private int mLastRequestUid; 128 129 /** 130 * Only used for testing. Throttling should always be enabled unless running tests 131 */ 132 private boolean mDisplayHashThrottlingEnabled = true; 133 134 private interface Command { run(IDisplayHashingService service)135 void run(IDisplayHashingService service) throws RemoteException; 136 } 137 DisplayHashController(Context context)138 DisplayHashController(Context context) { 139 mContext = context; 140 mHandler = new Handler(Looper.getMainLooper()); 141 mSalt = UUID.randomUUID().toString().getBytes(); 142 } 143 getSupportedHashAlgorithms()144 String[] getSupportedHashAlgorithms() { 145 Map<String, DisplayHashParams> displayHashAlgorithms = getDisplayHashAlgorithms(); 146 return displayHashAlgorithms.keySet().toArray(new String[0]); 147 } 148 149 @Nullable verifyDisplayHash(DisplayHash displayHash)150 VerifiedDisplayHash verifyDisplayHash(DisplayHash displayHash) { 151 final SyncCommand syncCommand = new SyncCommand(); 152 Bundle results = syncCommand.run((service, remoteCallback) -> { 153 try { 154 service.verifyDisplayHash(mSalt, displayHash, remoteCallback); 155 } catch (RemoteException e) { 156 Slog.e(TAG, "Failed to invoke verifyDisplayHash command"); 157 } 158 }); 159 160 return results.getParcelable(EXTRA_VERIFIED_DISPLAY_HASH); 161 } 162 setDisplayHashThrottlingEnabled(boolean enable)163 void setDisplayHashThrottlingEnabled(boolean enable) { 164 mDisplayHashThrottlingEnabled = enable; 165 } 166 generateDisplayHash(HardwareBuffer buffer, Rect bounds, String hashAlgorithm, RemoteCallback callback)167 private void generateDisplayHash(HardwareBuffer buffer, Rect bounds, 168 String hashAlgorithm, RemoteCallback callback) { 169 connectAndRun( 170 service -> service.generateDisplayHash(mSalt, buffer, bounds, hashAlgorithm, 171 callback)); 172 } 173 allowedToGenerateHash(int uid)174 private boolean allowedToGenerateHash(int uid) { 175 if (!mDisplayHashThrottlingEnabled) { 176 // Always allow to generate the hash. This is used to allow tests to run without 177 // waiting on the designated threshold. 178 return true; 179 } 180 181 long currentTime = System.currentTimeMillis(); 182 if (mLastRequestUid != uid) { 183 mLastRequestUid = uid; 184 mLastRequestTimeMs = currentTime; 185 return true; 186 } 187 188 int mIntervalBetweenRequestsMs = getIntervalBetweenRequestMillis(); 189 if (currentTime - mLastRequestTimeMs < mIntervalBetweenRequestsMs) { 190 return false; 191 } 192 193 mLastRequestTimeMs = currentTime; 194 return true; 195 } 196 generateDisplayHash(SurfaceControl.LayerCaptureArgs.Builder args, Rect boundsInWindow, String hashAlgorithm, int uid, RemoteCallback callback)197 void generateDisplayHash(SurfaceControl.LayerCaptureArgs.Builder args, 198 Rect boundsInWindow, String hashAlgorithm, int uid, RemoteCallback callback) { 199 if (!allowedToGenerateHash(uid)) { 200 sendDisplayHashError(callback, DISPLAY_HASH_ERROR_TOO_MANY_REQUESTS); 201 return; 202 } 203 204 final Map<String, DisplayHashParams> displayHashAlgorithmsMap = getDisplayHashAlgorithms(); 205 DisplayHashParams displayHashParams = displayHashAlgorithmsMap.get(hashAlgorithm); 206 if (displayHashParams == null) { 207 Slog.w(TAG, "Failed to generateDisplayHash. Invalid hashAlgorithm"); 208 sendDisplayHashError(callback, DISPLAY_HASH_ERROR_INVALID_HASH_ALGORITHM); 209 return; 210 } 211 212 Size size = displayHashParams.getBufferSize(); 213 if (size != null && (size.getWidth() > 0 || size.getHeight() > 0)) { 214 args.setFrameScale((float) size.getWidth() / boundsInWindow.width(), 215 (float) size.getHeight() / boundsInWindow.height()); 216 } 217 218 args.setGrayscale(displayHashParams.isGrayscaleBuffer()); 219 220 SurfaceControl.ScreenshotHardwareBuffer screenshotHardwareBuffer = 221 SurfaceControl.captureLayers(args.build()); 222 if (screenshotHardwareBuffer == null 223 || screenshotHardwareBuffer.getHardwareBuffer() == null) { 224 Slog.w(TAG, "Failed to generate DisplayHash. Couldn't capture content"); 225 sendDisplayHashError(callback, DISPLAY_HASH_ERROR_UNKNOWN); 226 return; 227 } 228 229 generateDisplayHash(screenshotHardwareBuffer.getHardwareBuffer(), boundsInWindow, 230 hashAlgorithm, callback); 231 } 232 getDisplayHashAlgorithms()233 private Map<String, DisplayHashParams> getDisplayHashAlgorithms() { 234 // We have a separate lock for the hashing params to ensure we can properly cache the 235 // hashing params so we don't need to call into the ExtServices process for each request. 236 synchronized (mDisplayHashAlgorithmsLock) { 237 if (mDisplayHashAlgorithms != null) { 238 return mDisplayHashAlgorithms; 239 } 240 241 final SyncCommand syncCommand = new SyncCommand(); 242 Bundle results = syncCommand.run((service, remoteCallback) -> { 243 try { 244 service.getDisplayHashAlgorithms(remoteCallback); 245 } catch (RemoteException e) { 246 Slog.e(TAG, "Failed to invoke getDisplayHashAlgorithms command", e); 247 } 248 }); 249 250 mDisplayHashAlgorithms = new HashMap<>(results.size()); 251 for (String key : results.keySet()) { 252 mDisplayHashAlgorithms.put(key, results.getParcelable(key)); 253 } 254 255 return mDisplayHashAlgorithms; 256 } 257 } 258 sendDisplayHashError(RemoteCallback callback, int errorCode)259 void sendDisplayHashError(RemoteCallback callback, int errorCode) { 260 Bundle bundle = new Bundle(); 261 bundle.putInt(EXTRA_DISPLAY_HASH_ERROR_CODE, errorCode); 262 callback.sendResult(bundle); 263 } 264 265 /** 266 * Calculate the bounds to generate the hash for. This takes into account window transform, 267 * magnification, and display bounds. 268 * 269 * Call while holding {@link WindowManagerService#mGlobalLock} 270 * 271 * @param win Window that the DisplayHash is generated for. 272 * @param boundsInWindow The bounds in the window where to generate the hash. 273 * @param outBounds The result of the calculated bounds 274 */ calculateDisplayHashBoundsLocked(WindowState win, Rect boundsInWindow, Rect outBounds)275 void calculateDisplayHashBoundsLocked(WindowState win, Rect boundsInWindow, 276 Rect outBounds) { 277 if (DEBUG) { 278 Slog.d(TAG, 279 "calculateDisplayHashBoundsLocked: boundsInWindow=" + boundsInWindow); 280 } 281 outBounds.set(boundsInWindow); 282 283 DisplayContent displayContent = win.getDisplayContent(); 284 if (displayContent == null) { 285 return; 286 } 287 288 // Intersect boundsInWindow with the window to make sure it's not outside the window 289 // requesting the token. Offset the window bounds to 0,0 since the boundsInWindow are 290 // offset from the window location, not display. 291 final Rect windowBounds = new Rect(); 292 win.getBounds(windowBounds); 293 windowBounds.offsetTo(0, 0); 294 outBounds.intersectUnchecked(windowBounds); 295 296 if (DEBUG) { 297 Slog.d(TAG, 298 "calculateDisplayHashBoundsLocked: boundsIntersectWindow=" + outBounds); 299 } 300 301 if (outBounds.isEmpty()) { 302 return; 303 } 304 305 // Transform the bounds using the window transform in case there's a scale or offset. 306 // This allows the bounds to be in display space. 307 win.getTransformationMatrix(mTmpFloat9, mTmpMatrix); 308 mTmpRectF.set(outBounds); 309 mTmpMatrix.mapRect(mTmpRectF, mTmpRectF); 310 outBounds.set((int) mTmpRectF.left, (int) mTmpRectF.top, (int) mTmpRectF.right, 311 (int) mTmpRectF.bottom); 312 if (DEBUG) { 313 Slog.d(TAG, "calculateDisplayHashBoundsLocked: boundsInDisplay=" + outBounds); 314 } 315 316 // Apply the magnification spec values to the bounds since the content could be magnified 317 final MagnificationSpec magSpec = displayContent.getMagnificationSpec(); 318 if (magSpec != null) { 319 outBounds.scale(magSpec.scale); 320 outBounds.offset((int) magSpec.offsetX, (int) magSpec.offsetY); 321 } 322 323 if (DEBUG) { 324 Slog.d(TAG, "calculateDisplayHashBoundsLocked: boundsWithMagnification=" 325 + outBounds); 326 } 327 328 if (outBounds.isEmpty()) { 329 return; 330 } 331 332 // Intersect with the display bounds since content outside the display are not visible to 333 // the user. 334 final Rect displayBounds = displayContent.getBounds(); 335 outBounds.intersectUnchecked(displayBounds); 336 if (DEBUG) { 337 Slog.d(TAG, "calculateDisplayHashBoundsLocked: finalBounds=" + outBounds); 338 } 339 } 340 getIntervalBetweenRequestMillis()341 private int getIntervalBetweenRequestMillis() { 342 // We have a separate lock for the hashing params to ensure we can properly cache the 343 // hashing params so we don't need to call into the ExtServices process for each request. 344 synchronized (mIntervalBetweenRequestsLock) { 345 if (mIntervalBetweenRequestMillis != -1) { 346 return mIntervalBetweenRequestMillis; 347 } 348 349 final SyncCommand syncCommand = new SyncCommand(); 350 Bundle results = syncCommand.run((service, remoteCallback) -> { 351 try { 352 service.getIntervalBetweenRequestsMillis(remoteCallback); 353 } catch (RemoteException e) { 354 Slog.e(TAG, "Failed to invoke getDisplayHashAlgorithms command", e); 355 } 356 }); 357 358 mIntervalBetweenRequestMillis = results.getInt(EXTRA_INTERVAL_BETWEEN_REQUESTS, 0); 359 return mIntervalBetweenRequestMillis; 360 } 361 } 362 363 /** 364 * Run a command, starting the service connection if necessary. 365 */ connectAndRun(@onNull Command command)366 private void connectAndRun(@NonNull Command command) { 367 synchronized (mServiceConnectionLock) { 368 mHandler.resetTimeoutMessage(); 369 if (mServiceConnection == null) { 370 if (DEBUG) Slog.v(TAG, "creating connection"); 371 372 // Create the connection 373 mServiceConnection = new DisplayHashingServiceConnection(); 374 375 final ComponentName component = getServiceComponentName(); 376 if (DEBUG) Slog.v(TAG, "binding to: " + component); 377 if (component != null) { 378 final Intent intent = new Intent(); 379 intent.setComponent(component); 380 final long token = Binder.clearCallingIdentity(); 381 try { 382 mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); 383 if (DEBUG) Slog.v(TAG, "bound"); 384 } finally { 385 Binder.restoreCallingIdentity(token); 386 } 387 } 388 } 389 390 mServiceConnection.runCommandLocked(command); 391 } 392 } 393 394 @Nullable getServiceInfo()395 private ServiceInfo getServiceInfo() { 396 final String packageName = 397 mContext.getPackageManager().getServicesSystemSharedLibraryPackageName(); 398 if (packageName == null) { 399 Slog.w(TAG, "no external services package!"); 400 return null; 401 } 402 403 final Intent intent = new Intent(DisplayHashingService.SERVICE_INTERFACE); 404 intent.setPackage(packageName); 405 final ResolveInfo resolveInfo; 406 final long token = Binder.clearCallingIdentity(); 407 try { 408 resolveInfo = mContext.getPackageManager().resolveService(intent, 409 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); 410 } finally { 411 Binder.restoreCallingIdentity(token); 412 } 413 414 if (resolveInfo == null || resolveInfo.serviceInfo == null) { 415 Slog.w(TAG, "No valid components found."); 416 return null; 417 } 418 return resolveInfo.serviceInfo; 419 } 420 421 @Nullable getServiceComponentName()422 private ComponentName getServiceComponentName() { 423 final ServiceInfo serviceInfo = getServiceInfo(); 424 if (serviceInfo == null) return null; 425 426 final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name); 427 if (!Manifest.permission.BIND_DISPLAY_HASHING_SERVICE 428 .equals(serviceInfo.permission)) { 429 Slog.w(TAG, name.flattenToShortString() + " requires permission " 430 + Manifest.permission.BIND_DISPLAY_HASHING_SERVICE); 431 return null; 432 } 433 434 if (DEBUG) Slog.v(TAG, "getServiceComponentName(): " + name); 435 return name; 436 } 437 438 private class SyncCommand { 439 private static final int WAIT_TIME_S = 5; 440 private Bundle mResult; 441 private final CountDownLatch mCountDownLatch = new CountDownLatch(1); 442 run(BiConsumer<IDisplayHashingService, RemoteCallback> func)443 public Bundle run(BiConsumer<IDisplayHashingService, RemoteCallback> func) { 444 connectAndRun(service -> { 445 RemoteCallback callback = new RemoteCallback(result -> { 446 mResult = result; 447 mCountDownLatch.countDown(); 448 }); 449 func.accept(service, callback); 450 }); 451 452 try { 453 mCountDownLatch.await(WAIT_TIME_S, TimeUnit.SECONDS); 454 } catch (Exception e) { 455 Slog.e(TAG, "Failed to wait for command", e); 456 } 457 458 return mResult; 459 } 460 } 461 462 private class DisplayHashingServiceConnection implements ServiceConnection { 463 @GuardedBy("mServiceConnectionLock") 464 private IDisplayHashingService mRemoteService; 465 466 @GuardedBy("mServiceConnectionLock") 467 private ArrayList<Command> mQueuedCommands; 468 469 @Override onServiceConnected(ComponentName name, IBinder service)470 public void onServiceConnected(ComponentName name, IBinder service) { 471 if (DEBUG) Slog.v(TAG, "onServiceConnected(): " + name); 472 synchronized (mServiceConnectionLock) { 473 mRemoteService = IDisplayHashingService.Stub.asInterface(service); 474 if (mQueuedCommands != null) { 475 final int size = mQueuedCommands.size(); 476 if (DEBUG) Slog.d(TAG, "running " + size + " queued commands"); 477 for (int i = 0; i < size; i++) { 478 final Command queuedCommand = mQueuedCommands.get(i); 479 try { 480 if (DEBUG) Slog.v(TAG, "running queued command #" + i); 481 queuedCommand.run(mRemoteService); 482 } catch (RemoteException e) { 483 Slog.w(TAG, "exception calling " + name + ": " + e); 484 } 485 } 486 mQueuedCommands = null; 487 } else if (DEBUG) { 488 Slog.d(TAG, "no queued commands"); 489 } 490 } 491 } 492 493 @Override onServiceDisconnected(ComponentName name)494 public void onServiceDisconnected(ComponentName name) { 495 if (DEBUG) Slog.v(TAG, "onServiceDisconnected(): " + name); 496 synchronized (mServiceConnectionLock) { 497 mRemoteService = null; 498 } 499 } 500 501 @Override onBindingDied(ComponentName name)502 public void onBindingDied(ComponentName name) { 503 if (DEBUG) Slog.v(TAG, "onBindingDied(): " + name); 504 synchronized (mServiceConnectionLock) { 505 mRemoteService = null; 506 } 507 } 508 509 @Override onNullBinding(ComponentName name)510 public void onNullBinding(ComponentName name) { 511 if (DEBUG) Slog.v(TAG, "onNullBinding(): " + name); 512 synchronized (mServiceConnectionLock) { 513 mRemoteService = null; 514 } 515 } 516 517 /** 518 * Only call while holding {@link #mServiceConnectionLock} 519 */ runCommandLocked(Command command)520 private void runCommandLocked(Command command) { 521 if (mRemoteService == null) { 522 if (DEBUG) Slog.d(TAG, "service is null; queuing command"); 523 if (mQueuedCommands == null) { 524 mQueuedCommands = new ArrayList<>(1); 525 } 526 mQueuedCommands.add(command); 527 } else { 528 try { 529 if (DEBUG) Slog.v(TAG, "running command right away"); 530 command.run(mRemoteService); 531 } catch (RemoteException e) { 532 Slog.w(TAG, "exception calling service: " + e); 533 } 534 } 535 } 536 } 537 538 private class Handler extends android.os.Handler { 539 static final long SERVICE_SHUTDOWN_TIMEOUT_MILLIS = 10000; // 10s 540 static final int MSG_SERVICE_SHUTDOWN_TIMEOUT = 1; 541 Handler(Looper looper)542 Handler(Looper looper) { 543 super(looper); 544 } 545 546 @Override handleMessage(Message msg)547 public void handleMessage(Message msg) { 548 if (msg.what == MSG_SERVICE_SHUTDOWN_TIMEOUT) { 549 if (DEBUG) { 550 Slog.v(TAG, "Shutting down service"); 551 } 552 synchronized (mServiceConnectionLock) { 553 if (mServiceConnection != null) { 554 mContext.unbindService(mServiceConnection); 555 mServiceConnection = null; 556 } 557 } 558 } 559 } 560 561 /** 562 * Set a timer for {@link #SERVICE_SHUTDOWN_TIMEOUT_MILLIS} so we can tear down the service 563 * if it's inactive. The requests will be coming from apps so it's hard to tell how often 564 * the requests can come in. Therefore, we leave the service running if requests continue 565 * to come in. Once there's been no activity for 10s, we can shut down the service and 566 * restart when we get a new request. 567 */ resetTimeoutMessage()568 void resetTimeoutMessage() { 569 if (DEBUG) { 570 Slog.v(TAG, "Reset shutdown message"); 571 } 572 removeMessages(MSG_SERVICE_SHUTDOWN_TIMEOUT); 573 sendEmptyMessageDelayed(MSG_SERVICE_SHUTDOWN_TIMEOUT, SERVICE_SHUTDOWN_TIMEOUT_MILLIS); 574 } 575 } 576 577 } 578