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