1 /*
2  * Copyright (C) 2023 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.car.bugreport;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.car.Car;
22 import android.car.CarOccupantZoneManager;
23 import android.content.Context;
24 import android.content.pm.PackageManager;
25 import android.graphics.Bitmap;
26 import android.graphics.Canvas;
27 import android.graphics.Color;
28 import android.graphics.Paint;
29 import android.graphics.Rect;
30 import android.hardware.display.DisplayManager;
31 import android.os.Environment;
32 import android.os.RemoteException;
33 import android.util.Log;
34 import android.view.Display;
35 import android.view.IWindowManager;
36 import android.view.WindowManagerGlobal;
37 import android.window.ScreenCapture;
38 
39 import java.io.File;
40 import java.io.FileNotFoundException;
41 import java.io.FileOutputStream;
42 import java.time.Instant;
43 import java.time.ZoneId;
44 import java.time.format.DateTimeFormatter;
45 import java.util.ArrayList;
46 import java.util.HashSet;
47 import java.util.List;
48 import java.util.OptionalInt;
49 import java.util.Set;
50 
51 
52 final class ScreenshotUtils {
53     private static final String TAG = ScreenshotUtils.class.getSimpleName();
54 
55     private static final float TITLE_TEXT_SIZE = 30;
56     private static final float TITLE_TEXT_MARGIN = 10;
57     private static final String SCREENSHOT_FILE_EXTENSION = "png";
58     private static final Bitmap.CompressFormat SCREENSHOT_BITMAP_COMPRESS_FORMAT =
59             Bitmap.CompressFormat.PNG;
60     private static final Bitmap.Config SCREENSHOT_BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
61 
62     /**
63      * Gets a screenshot directory in the Environment.getExternalStorageDirectory(). Creates if the
64      * directory doesn't exist.
65      */
66     @Nullable
getScreenshotDir()67     public static String getScreenshotDir() {
68         File filesDir = Environment.getExternalStorageDirectory();
69         if (filesDir == null) {
70             Log.e(TAG, "Failed to create a directory, filesDir is null.");
71             return null;
72         }
73 
74         String dir = filesDir.getAbsolutePath() + "/screenshots";
75         File storeDirectory = new File(dir);
76         if (!storeDirectory.exists()) {
77             if (!storeDirectory.mkdirs()) {
78                 Log.e(TAG, "Failed to create file storage directory.");
79                 return null;
80             }
81         }
82 
83         return dir;
84     }
85 
86     /** Takes screenshots of all displays and stores them to a storage. */
takeScreenshot(@onNull Context context, @Nullable Car car)87     public static void takeScreenshot(@NonNull Context context, @Nullable Car car) {
88         Log.i(TAG, "takeScreenshot is started.");
89 
90         CarOccupantZoneManager carOccupantZoneManager = null;
91         if (car != null) {
92             carOccupantZoneManager = (CarOccupantZoneManager) car.getCarManager(
93                     Car.CAR_OCCUPANT_ZONE_SERVICE);
94         }
95 
96         Set<Integer> displayIds = getDisplayIds(context, carOccupantZoneManager);
97         List<Bitmap> images = new ArrayList<>();
98         for (int displayId : displayIds) {
99             Bitmap image = takeScreenshotOfDisplay(displayId);
100             if (image == null) {
101                 continue;
102             }
103             image = addTextToImage(image, "Display ID: " + displayId);
104             images.add(image);
105         }
106 
107         if (images.size() == 0) {
108             Log.w(TAG, "There is no screenshot taken successfully.");
109             return;
110         }
111 
112         Bitmap fullImage = mergeImagesVertically(images);
113 
114         storeImage(fullImage, getScreenshotFilename());
115         Log.i(TAG, "takeScreenshot is finished.");
116     }
117 
118     /**
119      * Gets all display ids including a cluster display id if possible. It requires a permission
120      * android.car.permission.ACCESS_PRIVATE_DISPLAY_ID to get a cluster display's id.
121      */
122     @NonNull
getDisplayIds(@onNull Context context, @Nullable CarOccupantZoneManager carOccupantZoneManager)123     private static Set<Integer> getDisplayIds(@NonNull Context context,
124             @Nullable CarOccupantZoneManager carOccupantZoneManager) {
125         Set<Integer> displayIds = new HashSet<>();
126 
127         DisplayManager displayManager = context.getSystemService(DisplayManager.class);
128         if (displayManager == null) {
129             Log.e(TAG, "Failed to get DisplayManager.");
130             return displayIds;
131         }
132 
133         Display[] displays = displayManager.getDisplays(
134                 DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED);
135         for (Display display : displays) {
136             displayIds.add(display.getDisplayId());
137         }
138 
139         OptionalInt clusterDisplayId = getClusterDisplayId(context, carOccupantZoneManager);
140         if (clusterDisplayId.isPresent()) {
141             displayIds.add(clusterDisplayId.getAsInt());
142         }
143 
144         Log.d(TAG, "Display ids : " + displayIds);
145 
146         return displayIds;
147     }
148 
149     /** Gets cluster display id if possible. Or returns an empty instance. */
getClusterDisplayId(@onNull Context context, @Nullable CarOccupantZoneManager carOccupantZoneManager)150     private static OptionalInt getClusterDisplayId(@NonNull Context context,
151             @Nullable CarOccupantZoneManager carOccupantZoneManager) {
152         if (context.checkSelfPermission(Car.ACCESS_PRIVATE_DISPLAY_ID)
153                 != PackageManager.PERMISSION_GRANTED) {
154             Log.w(TAG, "android.car.permission.ACCESS_PRIVATE_DISPLAY_ID is not granted.");
155             return OptionalInt.empty();
156         }
157         if (carOccupantZoneManager == null) {
158             Log.w(TAG, "CarOccupantZoneManager is null.");
159             return OptionalInt.empty();
160         }
161 
162         int clusterDisplayId = carOccupantZoneManager.getDisplayIdForDriver(
163                 CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER);
164         return OptionalInt.of(clusterDisplayId);
165     }
166 
167     /** Gets filename of screenshot based on the current time. */
getScreenshotFilename()168     private static String getScreenshotFilename() {
169         DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss").withZone(
170                 ZoneId.systemDefault());
171         String nowInDateTimeFormat = formatter.format(Instant.now());
172         return "extra_screenshot_" + nowInDateTimeFormat + "." + SCREENSHOT_FILE_EXTENSION;
173     }
174 
175     /** Adds a text to the top of the image. */
176     @NonNull
addTextToImage(@onNull Bitmap image, String text)177     private static Bitmap addTextToImage(@NonNull Bitmap image, String text) {
178         Paint paint = new Paint();
179         paint.setColor(Color.BLACK);
180         paint.setTextSize(TITLE_TEXT_SIZE);
181         Rect textBounds = new Rect();
182         paint.getTextBounds(text, 0, text.length(), textBounds);
183 
184         float extraHeight = textBounds.height() + TITLE_TEXT_MARGIN * 2;
185 
186         Bitmap imageWithTitle = Bitmap.createBitmap(image.getWidth(),
187                 image.getHeight() + (int) extraHeight, image.getConfig());
188         Canvas canvas = new Canvas(imageWithTitle);
189         canvas.drawColor(Color.WHITE);
190         canvas.drawText(text, TITLE_TEXT_MARGIN,
191                 extraHeight - TITLE_TEXT_MARGIN - textBounds.bottom, paint);
192         canvas.drawBitmap(image, 0, extraHeight, null);
193         return imageWithTitle;
194     }
195 
196     @NonNull
mergeImagesVertically(@onNull List<Bitmap> images)197     private static Bitmap mergeImagesVertically(@NonNull List<Bitmap> images) {
198         int width = images.stream().mapToInt(Bitmap::getWidth).max().orElse(0);
199         int height = images.stream().mapToInt(Bitmap::getHeight).sum();
200 
201         Bitmap mergedImage = Bitmap.createBitmap(width, height, SCREENSHOT_BITMAP_CONFIG);
202         Canvas canvas = new Canvas(mergedImage);
203         canvas.drawColor(Color.WHITE);
204 
205         float curHeight = 0;
206         for (Bitmap image : images) {
207             canvas.drawBitmap(image, 0f, curHeight, null);
208             curHeight += image.getHeight();
209         }
210         return mergedImage;
211     }
212 
213     /** Stores an image with the given fileName. */
storeImage(@onNull Bitmap image, String fileName)214     private static void storeImage(@NonNull Bitmap image, String fileName) {
215         String screenshotDir = getScreenshotDir();
216         if (screenshotDir == null) {
217             return;
218         }
219 
220         String filePath = screenshotDir + "/" + fileName;
221         try {
222             FileOutputStream fos = new FileOutputStream(filePath);
223             image.compress(SCREENSHOT_BITMAP_COMPRESS_FORMAT, 100, fos);
224         } catch (FileNotFoundException e) {
225             Log.e(TAG, "File " + filePath + " not found to store screenshot.", e);
226             return;
227         }
228 
229         Log.i(TAG, "Screenshot is stored in " + filePath);
230     }
231 
232     /** Takes screenshots of the certain display. Returns null if it fails to take a screenshot. */
233     @Nullable
takeScreenshotOfDisplay(int displayId)234     private static Bitmap takeScreenshotOfDisplay(int displayId) {
235         Log.d(TAG, "Take screenshot of display " + displayId);
236         IWindowManager windowManager = WindowManagerGlobal.getWindowManagerService();
237 
238         ScreenCapture.CaptureArgs captureArgs = new ScreenCapture.CaptureArgs.Builder<>().build();
239         ScreenCapture.SynchronousScreenCaptureListener syncScreenCaptureListener =
240                 ScreenCapture.createSyncCaptureListener();
241 
242         try {
243             windowManager.captureDisplay(displayId, captureArgs, syncScreenCaptureListener);
244         } catch (RemoteException e) {
245             Log.e(TAG, "Failed to take screenshot", e);
246             return null;
247         }
248 
249         final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer =
250                 syncScreenCaptureListener.getBuffer();
251         if (screenshotBuffer == null) {
252             return null;
253         }
254         return screenshotBuffer.asBitmap().copy(SCREENSHOT_BITMAP_CONFIG, /* isMutable= */ true);
255     }
256 }
257