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