• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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