1 /*
2  * Copyright (C) 2022 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 package com.android.server.wm;
17 
18 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT;
19 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
20 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.app.ActivityManager;
25 import android.content.pm.PackageManager;
26 import android.graphics.Bitmap;
27 import android.graphics.PixelFormat;
28 import android.graphics.Point;
29 import android.graphics.RecordingCanvas;
30 import android.graphics.Rect;
31 import android.graphics.RenderNode;
32 import android.hardware.HardwareBuffer;
33 import android.os.SystemClock;
34 import android.os.Trace;
35 import android.util.Pair;
36 import android.util.Slog;
37 import android.view.InsetsState;
38 import android.view.SurfaceControl;
39 import android.view.ThreadedRenderer;
40 import android.view.WindowInsets;
41 import android.view.WindowInsetsController;
42 import android.view.WindowManager;
43 import android.window.ScreenCapture;
44 import android.window.SnapshotDrawerUtils;
45 import android.window.TaskSnapshot;
46 
47 import com.android.internal.annotations.VisibleForTesting;
48 import com.android.internal.graphics.ColorUtils;
49 import com.android.server.wm.utils.InsetUtils;
50 
51 import java.io.PrintWriter;
52 
53 /**
54  * Base class for a Snapshot controller
55  * @param <TYPE> The basic type, either Task or ActivityRecord
56  * @param <CACHE> The basic cache for either Task or ActivityRecord
57  */
58 abstract class AbsAppSnapshotController<TYPE extends WindowContainer,
59         CACHE extends SnapshotCache<TYPE>> {
60     static final String TAG = TAG_WITH_CLASS_NAME ? "SnapshotController" : TAG_WM;
61     /**
62      * Return value for {@link #getSnapshotMode}: We are allowed to take a real screenshot to be
63      * used as the snapshot.
64      */
65     @VisibleForTesting
66     static final int SNAPSHOT_MODE_REAL = 0;
67     /**
68      * Return value for {@link #getSnapshotMode}: We are not allowed to take a real screenshot but
69      * we should try to use the app theme to create a fake representation of the app.
70      */
71     @VisibleForTesting
72     static final int SNAPSHOT_MODE_APP_THEME = 1;
73     /**
74      * Return value for {@link #getSnapshotMode}: We aren't allowed to take any snapshot.
75      */
76     @VisibleForTesting
77     static final int SNAPSHOT_MODE_NONE = 2;
78 
79     protected final WindowManagerService mService;
80     protected final float mHighResSnapshotScale;
81     private final Rect mTmpRect = new Rect();
82     /**
83      * Flag indicating whether we are running on an Android TV device.
84      */
85     protected final boolean mIsRunningOnTv;
86     /**
87      * Flag indicating whether we are running on an IoT device.
88      */
89     protected final boolean mIsRunningOnIoT;
90 
91     protected CACHE mCache;
92     /**
93      * Flag indicating if task snapshot is enabled on this device.
94      */
95     private boolean mSnapshotEnabled;
96 
AbsAppSnapshotController(WindowManagerService service)97     AbsAppSnapshotController(WindowManagerService service) {
98         mService = service;
99         mIsRunningOnTv = mService.mContext.getPackageManager().hasSystemFeature(
100                 PackageManager.FEATURE_LEANBACK);
101         mIsRunningOnIoT = mService.mContext.getPackageManager().hasSystemFeature(
102                 PackageManager.FEATURE_EMBEDDED);
103         mHighResSnapshotScale = initSnapshotScale();
104     }
105 
initSnapshotScale()106     protected float initSnapshotScale() {
107         final float config = mService.mContext.getResources().getFloat(
108                 com.android.internal.R.dimen.config_highResTaskSnapshotScale);
109         return Math.max(Math.min(config, 1f), 0.1f);
110     }
111 
112     /**
113      * Set basic cache to the controller.
114      */
initialize(CACHE cache)115     protected void initialize(CACHE cache) {
116         mCache = cache;
117     }
118 
setSnapshotEnabled(boolean enabled)119     void setSnapshotEnabled(boolean enabled) {
120         mSnapshotEnabled = enabled;
121     }
122 
shouldDisableSnapshots()123     boolean shouldDisableSnapshots() {
124         return mIsRunningOnTv || mIsRunningOnIoT || !mSnapshotEnabled;
125     }
126 
getTopActivity(TYPE source)127     abstract ActivityRecord getTopActivity(TYPE source);
getTopFullscreenActivity(TYPE source)128     abstract ActivityRecord getTopFullscreenActivity(TYPE source);
getTaskDescription(TYPE source)129     abstract ActivityManager.TaskDescription getTaskDescription(TYPE source);
130     /**
131      * Find the window for a given task to take a snapshot. Top child of the task is usually the one
132      * we're looking for, but during app transitions, trampoline activities can appear in the
133      * children, which should be ignored.
134      */
135     @Nullable
findAppTokenForSnapshot(TYPE source)136     protected abstract ActivityRecord findAppTokenForSnapshot(TYPE source);
use16BitFormat()137     protected abstract boolean use16BitFormat();
138 
139     /**
140      * This is different than {@link #recordSnapshotInner(TYPE, boolean)} because it doesn't store
141      * the snapshot to the cache and returns the TaskSnapshot immediately.
142      *
143      * This is only used for testing so the snapshot content can be verified.
144      */
145     // TODO(b/264551777): clean up the "snapshotHome" argument
146     @VisibleForTesting
captureSnapshot(TYPE source, boolean snapshotHome)147     TaskSnapshot captureSnapshot(TYPE source, boolean snapshotHome) {
148         final TaskSnapshot snapshot;
149         if (snapshotHome) {
150             snapshot = snapshot(source);
151         } else {
152             switch (getSnapshotMode(source)) {
153                 case SNAPSHOT_MODE_NONE:
154                     return null;
155                 case SNAPSHOT_MODE_APP_THEME:
156                     snapshot = drawAppThemeSnapshot(source);
157                     break;
158                 case SNAPSHOT_MODE_REAL:
159                     snapshot = snapshot(source);
160                     break;
161                 default:
162                     snapshot = null;
163                     break;
164             }
165         }
166         return snapshot;
167     }
168 
recordSnapshotInner(TYPE source, boolean allowSnapshotHome)169     final TaskSnapshot recordSnapshotInner(TYPE source, boolean allowSnapshotHome) {
170         if (shouldDisableSnapshots()) {
171             return null;
172         }
173         final boolean snapshotHome = allowSnapshotHome && source.isActivityTypeHome();
174         final TaskSnapshot snapshot = captureSnapshot(source, snapshotHome);
175         if (snapshot == null) {
176             return null;
177         }
178         final HardwareBuffer buffer = snapshot.getHardwareBuffer();
179         if (buffer.getWidth() == 0 || buffer.getHeight() == 0) {
180             buffer.close();
181             Slog.e(TAG, "Invalid snapshot dimensions " + buffer.getWidth() + "x"
182                     + buffer.getHeight());
183             return null;
184         } else {
185             mCache.putSnapshot(source, snapshot);
186             return snapshot;
187         }
188     }
189 
190     @VisibleForTesting
getSnapshotMode(TYPE source)191     int getSnapshotMode(TYPE source) {
192         final ActivityRecord topChild = getTopActivity(source);
193         if (!source.isActivityTypeStandardOrUndefined() && !source.isActivityTypeAssistant()) {
194             return SNAPSHOT_MODE_NONE;
195         } else if (topChild != null && topChild.shouldUseAppThemeSnapshot()) {
196             return SNAPSHOT_MODE_APP_THEME;
197         } else {
198             return SNAPSHOT_MODE_REAL;
199         }
200     }
201 
202     @Nullable
snapshot(TYPE source)203     TaskSnapshot snapshot(TYPE source) {
204         return snapshot(source, PixelFormat.UNKNOWN);
205     }
206 
207     @Nullable
snapshot(TYPE source, int pixelFormat)208     TaskSnapshot snapshot(TYPE source, int pixelFormat) {
209         TaskSnapshot.Builder builder = new TaskSnapshot.Builder();
210         if (!prepareTaskSnapshot(source, pixelFormat, builder)) {
211             // Failed some pre-req. Has been logged.
212             return null;
213         }
214         final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer =
215                 createSnapshot(source, builder);
216         if (screenshotBuffer == null) {
217             // Failed to acquire image. Has been logged.
218             return null;
219         }
220         builder.setCaptureTime(SystemClock.elapsedRealtimeNanos());
221         builder.setSnapshot(screenshotBuffer.getHardwareBuffer());
222         builder.setColorSpace(screenshotBuffer.getColorSpace());
223         return builder.build();
224     }
225 
226     @Nullable
createSnapshot(@onNull TYPE source, TaskSnapshot.Builder builder)227     ScreenCapture.ScreenshotHardwareBuffer createSnapshot(@NonNull TYPE source,
228             TaskSnapshot.Builder builder) {
229         Point taskSize = new Point();
230         Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "createSnapshot");
231         final ScreenCapture.ScreenshotHardwareBuffer taskSnapshot = createSnapshot(source,
232                 mHighResSnapshotScale, builder.getPixelFormat(), taskSize, builder);
233         Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
234         builder.setTaskSize(taskSize);
235         return taskSnapshot;
236     }
237 
238     @Nullable
createSnapshot(@onNull TYPE source, float scaleFraction, int pixelFormat, Point outTaskSize, TaskSnapshot.Builder builder)239     ScreenCapture.ScreenshotHardwareBuffer createSnapshot(@NonNull TYPE source,
240             float scaleFraction, int pixelFormat, Point outTaskSize, TaskSnapshot.Builder builder) {
241         if (source.getSurfaceControl() == null) {
242             if (DEBUG_SCREENSHOT) {
243                 Slog.w(TAG_WM, "Failed to take screenshot. No surface control for " + source);
244             }
245             return null;
246         }
247         mTmpRect.setEmpty();
248         if (source.mTransitionController.inFinishingTransition(source)) {
249             final Transition.ChangeInfo changeInfo = source.mTransitionController
250                     .mFinishingTransition.mChanges.get(source);
251             if (changeInfo != null) {
252                 mTmpRect.set(changeInfo.mAbsoluteBounds);
253             }
254         }
255         if (mTmpRect.isEmpty()) {
256             source.getBounds(mTmpRect);
257         }
258         mTmpRect.offsetTo(0, 0);
259         SurfaceControl[] excludeLayers;
260         final WindowState imeWindow = source.getDisplayContent().mInputMethodWindow;
261         // Exclude IME window snapshot when IME isn't proper to attach to app.
262         final boolean excludeIme = imeWindow != null && imeWindow.getSurfaceControl() != null
263                 && !source.getDisplayContent().shouldImeAttachedToApp();
264         final WindowState navWindow =
265                 source.getDisplayContent().getDisplayPolicy().getNavigationBar();
266         // If config_attachNavBarToAppDuringTransition is true, the nav bar will be reparent to the
267         // the swiped app when entering recent app, therefore the task will contain the navigation
268         // bar and we should exclude it from snapshot.
269         final boolean excludeNavBar = navWindow != null;
270         if (excludeIme && excludeNavBar) {
271             excludeLayers = new SurfaceControl[2];
272             excludeLayers[0] = imeWindow.getSurfaceControl();
273             excludeLayers[1] = navWindow.getSurfaceControl();
274         } else if (excludeIme || excludeNavBar) {
275             excludeLayers = new SurfaceControl[1];
276             excludeLayers[0] =
277                     excludeIme ? imeWindow.getSurfaceControl() : navWindow.getSurfaceControl();
278         } else {
279             excludeLayers = new SurfaceControl[0];
280         }
281         builder.setHasImeSurface(!excludeIme && imeWindow != null && imeWindow.isVisible());
282         final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer =
283                 ScreenCapture.captureLayersExcluding(
284                         source.getSurfaceControl(), mTmpRect, scaleFraction,
285                         pixelFormat, excludeLayers);
286         if (outTaskSize != null) {
287             outTaskSize.x = mTmpRect.width();
288             outTaskSize.y = mTmpRect.height();
289         }
290         final HardwareBuffer buffer = screenshotBuffer == null ? null
291                 : screenshotBuffer.getHardwareBuffer();
292         if (isInvalidHardwareBuffer(buffer)) {
293             return null;
294         }
295         return screenshotBuffer;
296     }
297 
isInvalidHardwareBuffer(HardwareBuffer buffer)298     static boolean isInvalidHardwareBuffer(HardwareBuffer buffer) {
299         return buffer == null || buffer.isClosed() // This must be checked before getting size.
300                 || buffer.getWidth() <= 1 || buffer.getHeight() <= 1;
301     }
302 
303     /**
304      * Validates the state of the Task is appropriate to capture a snapshot, collects
305      * information from the task and populates the builder.
306      *
307      * @param source the window to capture
308      * @param pixelFormat the desired pixel format, or {@link PixelFormat#UNKNOWN} to
309      *                    automatically select
310      * @param builder the snapshot builder to populate
311      *
312      * @return true if the state of the task is ok to proceed
313      */
314     @VisibleForTesting
prepareTaskSnapshot(TYPE source, int pixelFormat, TaskSnapshot.Builder builder)315     boolean prepareTaskSnapshot(TYPE source, int pixelFormat, TaskSnapshot.Builder builder) {
316         final Pair<ActivityRecord, WindowState> result = checkIfReadyToSnapshot(source);
317         if (result == null) {
318             return false;
319         }
320         final ActivityRecord activity = result.first;
321         final WindowState mainWindow = result.second;
322         final Rect contentInsets = getSystemBarInsets(mainWindow.getFrame(),
323                 mainWindow.getInsetsStateWithVisibilityOverride());
324         final Rect letterboxInsets = activity.getLetterboxInsets();
325         InsetUtils.addInsets(contentInsets, letterboxInsets);
326         builder.setIsRealSnapshot(true);
327         builder.setId(System.currentTimeMillis());
328         builder.setContentInsets(contentInsets);
329         builder.setLetterboxInsets(letterboxInsets);
330         final boolean isWindowTranslucent = mainWindow.getAttrs().format != PixelFormat.OPAQUE;
331         final boolean isShowWallpaper = mainWindow.hasWallpaper();
332         if (pixelFormat == PixelFormat.UNKNOWN) {
333             pixelFormat = use16BitFormat() && activity.fillsParent()
334                     && !(isWindowTranslucent && isShowWallpaper)
335                     ? PixelFormat.RGB_565
336                     : PixelFormat.RGBA_8888;
337         }
338         final boolean isTranslucent = PixelFormat.formatHasAlpha(pixelFormat)
339                 && (!activity.fillsParent() || isWindowTranslucent);
340         builder.setTopActivityComponent(activity.mActivityComponent);
341         builder.setPixelFormat(pixelFormat);
342         builder.setIsTranslucent(isTranslucent);
343         builder.setOrientation(activity.getTask().getConfiguration().orientation);
344         builder.setRotation(activity.getTask().getDisplayContent().getRotation());
345         builder.setWindowingMode(source.getWindowingMode());
346         builder.setAppearance(getAppearance(source));
347         return true;
348     }
349 
350     /**
351      * Check if the state of the Task is appropriate to capture a snapshot, such like the task
352      * snapshot or the associated IME surface snapshot.
353      *
354      * @param source the target object to capture the snapshot
355      * @return Pair of (the top activity of the task, the main window of the task) if passed the
356      * state checking. Returns {@code null} if the task state isn't ready to snapshot.
357      */
checkIfReadyToSnapshot(TYPE source)358     Pair<ActivityRecord, WindowState> checkIfReadyToSnapshot(TYPE source) {
359         if (!mService.mPolicy.isScreenOn()) {
360             if (DEBUG_SCREENSHOT) {
361                 Slog.i(TAG_WM, "Attempted to take screenshot while display was off.");
362             }
363             return null;
364         }
365         final ActivityRecord activity = findAppTokenForSnapshot(source);
366         if (activity == null) {
367             if (DEBUG_SCREENSHOT) {
368                 Slog.w(TAG_WM, "Failed to take screenshot. No visible windows for " + source);
369             }
370             return null;
371         }
372         if (activity.hasCommittedReparentToAnimationLeash()) {
373             if (DEBUG_SCREENSHOT) {
374                 Slog.w(TAG_WM, "Failed to take screenshot. App is animating " + activity);
375             }
376             return null;
377         }
378         final WindowState mainWindow = activity.findMainWindow();
379         if (mainWindow == null) {
380             Slog.w(TAG_WM, "Failed to take screenshot. No main window for " + source);
381             return null;
382         }
383         if (activity.hasFixedRotationTransform()) {
384             if (DEBUG_SCREENSHOT) {
385                 Slog.i(TAG_WM, "Skip taking screenshot. App has fixed rotation " + activity);
386             }
387             // The activity is in a temporal state that it has different rotation than the task.
388             return null;
389         }
390         return new Pair<>(activity, mainWindow);
391     }
392 
393     /**
394      * If we are not allowed to take a real screenshot, this attempts to represent the app as best
395      * as possible by using the theme's window background.
396      */
drawAppThemeSnapshot(TYPE source)397     private TaskSnapshot drawAppThemeSnapshot(TYPE source) {
398         final ActivityRecord topActivity = getTopActivity(source);
399         if (topActivity == null) {
400             return null;
401         }
402         final WindowState mainWindow = topActivity.findMainWindow();
403         if (mainWindow == null) {
404             return null;
405         }
406         final ActivityManager.TaskDescription taskDescription = getTaskDescription(source);
407         final int color = ColorUtils.setAlphaComponent(
408                 taskDescription.getBackgroundColor(), 255);
409         final WindowManager.LayoutParams attrs = mainWindow.getAttrs();
410         final Rect taskBounds = source.getBounds();
411         final InsetsState insetsState = mainWindow.getInsetsStateWithVisibilityOverride();
412         final Rect systemBarInsets = getSystemBarInsets(mainWindow.getFrame(), insetsState);
413         final SnapshotDrawerUtils.SystemBarBackgroundPainter
414                 decorPainter = new SnapshotDrawerUtils.SystemBarBackgroundPainter(attrs.flags,
415                 attrs.privateFlags, attrs.insetsFlags.appearance, taskDescription,
416                 mHighResSnapshotScale, mainWindow.getRequestedVisibleTypes());
417         final int taskWidth = taskBounds.width();
418         final int taskHeight = taskBounds.height();
419         final int width = (int) (taskWidth * mHighResSnapshotScale);
420         final int height = (int) (taskHeight * mHighResSnapshotScale);
421         final RenderNode node = RenderNode.create("SnapshotController", null);
422         node.setLeftTopRightBottom(0, 0, width, height);
423         node.setClipToBounds(false);
424         final RecordingCanvas c = node.start(width, height);
425         c.drawColor(color);
426         decorPainter.setInsets(systemBarInsets);
427         decorPainter.drawDecors(c /* statusBarExcludeFrame */, null /* alreadyDrawFrame */);
428         node.end(c);
429         final Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, width, height);
430         if (hwBitmap == null) {
431             return null;
432         }
433         final Rect contentInsets = new Rect(systemBarInsets);
434         final Rect letterboxInsets = topActivity.getLetterboxInsets();
435         InsetUtils.addInsets(contentInsets, letterboxInsets);
436         // Note, the app theme snapshot is never translucent because we enforce a non-translucent
437         // color above
438         return new TaskSnapshot(
439                 System.currentTimeMillis() /* id */,
440                 SystemClock.elapsedRealtimeNanos() /* captureTime */,
441                 topActivity.mActivityComponent, hwBitmap.getHardwareBuffer(),
442                 hwBitmap.getColorSpace(), mainWindow.getConfiguration().orientation,
443                 mainWindow.getWindowConfiguration().getRotation(), new Point(taskWidth, taskHeight),
444                 contentInsets, letterboxInsets, false /* isLowResolution */,
445                 false /* isRealSnapshot */, source.getWindowingMode(),
446                 getAppearance(source), false /* isTranslucent */, false /* hasImeSurface */);
447     }
448 
getSystemBarInsets(Rect frame, InsetsState state)449     static Rect getSystemBarInsets(Rect frame, InsetsState state) {
450         return state.calculateInsets(
451                 frame, WindowInsets.Type.systemBars(), false /* ignoreVisibility */).toRect();
452     }
453 
454     /**
455      * @return The {@link WindowInsetsController.Appearance} flags for the top fullscreen opaque
456      * window in the given {@param TYPE}.
457      */
458     @WindowInsetsController.Appearance
getAppearance(TYPE source)459     private int getAppearance(TYPE source) {
460         final ActivityRecord topFullscreenActivity = getTopFullscreenActivity(source);
461         final WindowState topFullscreenOpaqueWindow = topFullscreenActivity != null
462                 ? topFullscreenActivity.getTopFullscreenOpaqueWindow()
463                 : null;
464         if (topFullscreenOpaqueWindow != null) {
465             return topFullscreenOpaqueWindow.mAttrs.insetsFlags.appearance;
466         }
467         return 0;
468     }
469 
470     /**
471      * Called when an {@link ActivityRecord} has been removed.
472      */
onAppRemoved(ActivityRecord activity)473     void onAppRemoved(ActivityRecord activity) {
474         mCache.onAppRemoved(activity);
475     }
476 
477     /**
478      * Called when the process of an {@link ActivityRecord} has died.
479      */
onAppDied(ActivityRecord activity)480     void onAppDied(ActivityRecord activity) {
481         mCache.onAppDied(activity);
482     }
483 
isAnimatingByRecents(@onNull Task task)484     boolean isAnimatingByRecents(@NonNull Task task) {
485         return task.isAnimatingByRecents();
486     }
487 
dump(PrintWriter pw, String prefix)488     void dump(PrintWriter pw, String prefix) {
489         pw.println(prefix + "mHighResSnapshotScale=" + mHighResSnapshotScale);
490         pw.println(prefix + "mSnapshotEnabled=" + mSnapshotEnabled);
491         mCache.dump(pw, prefix);
492     }
493 }
494