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.displayhash.DisplayHash; 57 import android.view.displayhash.VerifiedDisplayHash; 58 import android.window.ScreenCapture; 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(ScreenCapture.LayerCaptureArgs.Builder args, Rect boundsInWindow, String hashAlgorithm, int uid, RemoteCallback callback)197 void generateDisplayHash(ScreenCapture.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 ScreenCapture.ScreenshotHardwareBuffer screenshotHardwareBuffer = 221 ScreenCapture.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 final ComponentName component = getServiceComponentName(); 373 if (DEBUG) Slog.v(TAG, "binding to: " + component); 374 if (component != null) { 375 final Intent intent = new Intent(); 376 intent.setComponent(component); 377 final long token = Binder.clearCallingIdentity(); 378 try { 379 // Create the connection 380 mServiceConnection = new DisplayHashingServiceConnection(); 381 mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); 382 if (DEBUG) Slog.v(TAG, "bound"); 383 } finally { 384 Binder.restoreCallingIdentity(token); 385 } 386 } 387 } 388 389 if (mServiceConnection != null) { 390 mServiceConnection.runCommandLocked(command); 391 } 392 } 393 } 394 395 @Nullable getServiceInfo()396 private ServiceInfo getServiceInfo() { 397 final String packageName = 398 mContext.getPackageManager().getServicesSystemSharedLibraryPackageName(); 399 if (packageName == null) { 400 Slog.w(TAG, "no external services package!"); 401 return null; 402 } 403 404 final Intent intent = new Intent(DisplayHashingService.SERVICE_INTERFACE); 405 intent.setPackage(packageName); 406 final ResolveInfo resolveInfo; 407 final long token = Binder.clearCallingIdentity(); 408 try { 409 resolveInfo = mContext.getPackageManager().resolveService(intent, 410 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); 411 } finally { 412 Binder.restoreCallingIdentity(token); 413 } 414 415 if (resolveInfo == null || resolveInfo.serviceInfo == null) { 416 Slog.w(TAG, "No valid components found."); 417 return null; 418 } 419 return resolveInfo.serviceInfo; 420 } 421 422 @Nullable getServiceComponentName()423 private ComponentName getServiceComponentName() { 424 final ServiceInfo serviceInfo = getServiceInfo(); 425 if (serviceInfo == null) return null; 426 427 final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name); 428 if (!Manifest.permission.BIND_DISPLAY_HASHING_SERVICE 429 .equals(serviceInfo.permission)) { 430 Slog.w(TAG, name.flattenToShortString() + " requires permission " 431 + Manifest.permission.BIND_DISPLAY_HASHING_SERVICE); 432 return null; 433 } 434 435 if (DEBUG) Slog.v(TAG, "getServiceComponentName(): " + name); 436 return name; 437 } 438 439 private class SyncCommand { 440 private static final int WAIT_TIME_S = 5; 441 private Bundle mResult; 442 private final CountDownLatch mCountDownLatch = new CountDownLatch(1); 443 run(BiConsumer<IDisplayHashingService, RemoteCallback> func)444 public Bundle run(BiConsumer<IDisplayHashingService, RemoteCallback> func) { 445 connectAndRun(service -> { 446 RemoteCallback callback = new RemoteCallback(result -> { 447 mResult = result; 448 mCountDownLatch.countDown(); 449 }); 450 func.accept(service, callback); 451 }); 452 453 try { 454 mCountDownLatch.await(WAIT_TIME_S, TimeUnit.SECONDS); 455 } catch (Exception e) { 456 Slog.e(TAG, "Failed to wait for command", e); 457 } 458 459 return mResult; 460 } 461 } 462 463 private class DisplayHashingServiceConnection implements ServiceConnection { 464 @GuardedBy("mServiceConnectionLock") 465 private IDisplayHashingService mRemoteService; 466 467 @GuardedBy("mServiceConnectionLock") 468 private ArrayList<Command> mQueuedCommands; 469 470 @Override onServiceConnected(ComponentName name, IBinder service)471 public void onServiceConnected(ComponentName name, IBinder service) { 472 if (DEBUG) Slog.v(TAG, "onServiceConnected(): " + name); 473 synchronized (mServiceConnectionLock) { 474 mRemoteService = IDisplayHashingService.Stub.asInterface(service); 475 if (mQueuedCommands != null) { 476 final int size = mQueuedCommands.size(); 477 if (DEBUG) Slog.d(TAG, "running " + size + " queued commands"); 478 for (int i = 0; i < size; i++) { 479 final Command queuedCommand = mQueuedCommands.get(i); 480 try { 481 if (DEBUG) Slog.v(TAG, "running queued command #" + i); 482 queuedCommand.run(mRemoteService); 483 } catch (RemoteException e) { 484 Slog.w(TAG, "exception calling " + name + ": " + e); 485 } 486 } 487 mQueuedCommands = null; 488 } else if (DEBUG) { 489 Slog.d(TAG, "no queued commands"); 490 } 491 } 492 } 493 494 @Override onServiceDisconnected(ComponentName name)495 public void onServiceDisconnected(ComponentName name) { 496 if (DEBUG) Slog.v(TAG, "onServiceDisconnected(): " + name); 497 synchronized (mServiceConnectionLock) { 498 mRemoteService = null; 499 } 500 } 501 502 @Override onBindingDied(ComponentName name)503 public void onBindingDied(ComponentName name) { 504 if (DEBUG) Slog.v(TAG, "onBindingDied(): " + name); 505 synchronized (mServiceConnectionLock) { 506 mRemoteService = null; 507 } 508 } 509 510 @Override onNullBinding(ComponentName name)511 public void onNullBinding(ComponentName name) { 512 if (DEBUG) Slog.v(TAG, "onNullBinding(): " + name); 513 synchronized (mServiceConnectionLock) { 514 mRemoteService = null; 515 } 516 } 517 518 /** 519 * Only call while holding {@link #mServiceConnectionLock} 520 */ runCommandLocked(Command command)521 private void runCommandLocked(Command command) { 522 if (mRemoteService == null) { 523 if (DEBUG) Slog.d(TAG, "service is null; queuing command"); 524 if (mQueuedCommands == null) { 525 mQueuedCommands = new ArrayList<>(1); 526 } 527 mQueuedCommands.add(command); 528 } else { 529 try { 530 if (DEBUG) Slog.v(TAG, "running command right away"); 531 command.run(mRemoteService); 532 } catch (RemoteException e) { 533 Slog.w(TAG, "exception calling service: " + e); 534 } 535 } 536 } 537 } 538 539 private class Handler extends android.os.Handler { 540 static final long SERVICE_SHUTDOWN_TIMEOUT_MILLIS = 10000; // 10s 541 static final int MSG_SERVICE_SHUTDOWN_TIMEOUT = 1; 542 Handler(Looper looper)543 Handler(Looper looper) { 544 super(looper); 545 } 546 547 @Override handleMessage(Message msg)548 public void handleMessage(Message msg) { 549 if (msg.what == MSG_SERVICE_SHUTDOWN_TIMEOUT) { 550 if (DEBUG) { 551 Slog.v(TAG, "Shutting down service"); 552 } 553 synchronized (mServiceConnectionLock) { 554 if (mServiceConnection != null) { 555 mContext.unbindService(mServiceConnection); 556 mServiceConnection = null; 557 } 558 } 559 } 560 } 561 562 /** 563 * Set a timer for {@link #SERVICE_SHUTDOWN_TIMEOUT_MILLIS} so we can tear down the service 564 * if it's inactive. The requests will be coming from apps so it's hard to tell how often 565 * the requests can come in. Therefore, we leave the service running if requests continue 566 * to come in. Once there's been no activity for 10s, we can shut down the service and 567 * restart when we get a new request. 568 */ resetTimeoutMessage()569 void resetTimeoutMessage() { 570 if (DEBUG) { 571 Slog.v(TAG, "Reset shutdown message"); 572 } 573 removeMessages(MSG_SERVICE_SHUTDOWN_TIMEOUT); 574 sendEmptyMessageDelayed(MSG_SERVICE_SHUTDOWN_TIMEOUT, SERVICE_SHUTDOWN_TIMEOUT_MILLIS); 575 } 576 } 577 578 } 579