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.server.wallpaper; 18 19 import static android.view.Display.DEFAULT_DISPLAY; 20 21 import static com.android.server.wallpaper.WallpaperUtils.RECORD_FILE; 22 import static com.android.server.wallpaper.WallpaperUtils.RECORD_LOCK_FILE; 23 import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER; 24 import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir; 25 26 import android.graphics.Bitmap; 27 import android.graphics.BitmapFactory; 28 import android.graphics.ImageDecoder; 29 import android.graphics.Rect; 30 import android.os.FileUtils; 31 import android.os.SELinux; 32 import android.util.Slog; 33 import android.view.DisplayInfo; 34 35 import com.android.server.utils.TimingsTraceAndSlog; 36 37 import libcore.io.IoUtils; 38 39 import java.io.BufferedOutputStream; 40 import java.io.File; 41 import java.io.FileOutputStream; 42 43 /** 44 * Helper file for wallpaper cropping 45 * Meant to have a single instance, only used by the WallpaperManagerService 46 */ 47 class WallpaperCropper { 48 49 private static final String TAG = WallpaperCropper.class.getSimpleName(); 50 private static final boolean DEBUG = false; 51 private static final boolean DEBUG_CROP = true; 52 53 private final WallpaperDisplayHelper mWallpaperDisplayHelper; 54 WallpaperCropper(WallpaperDisplayHelper wallpaperDisplayHelper)55 WallpaperCropper(WallpaperDisplayHelper wallpaperDisplayHelper) { 56 mWallpaperDisplayHelper = wallpaperDisplayHelper; 57 } 58 59 /** 60 * Once a new wallpaper has been written via setWallpaper(...), it needs to be cropped 61 * for display. 62 * 63 * This will generate the crop and write it in the file 64 */ generateCrop(WallpaperData wallpaper)65 void generateCrop(WallpaperData wallpaper) { 66 TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG); 67 t.traceBegin("WPMS.generateCrop"); 68 generateCropInternal(wallpaper); 69 t.traceEnd(); 70 } 71 generateCropInternal(WallpaperData wallpaper)72 private void generateCropInternal(WallpaperData wallpaper) { 73 boolean success = false; 74 75 // Only generate crop for default display. 76 final WallpaperDisplayHelper.DisplayData wpData = 77 mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY); 78 final Rect cropHint = new Rect(wallpaper.cropHint); 79 final DisplayInfo displayInfo = mWallpaperDisplayHelper.getDisplayInfo(DEFAULT_DISPLAY); 80 81 if (DEBUG) { 82 Slog.v(TAG, "Generating crop for new wallpaper(s): 0x" 83 + Integer.toHexString(wallpaper.mWhich) 84 + " to " + wallpaper.getCropFile().getName() 85 + " crop=(" + cropHint.width() + 'x' + cropHint.height() 86 + ") dim=(" + wpData.mWidth + 'x' + wpData.mHeight + ')'); 87 } 88 89 // Analyse the source; needed in multiple cases 90 BitmapFactory.Options options = new BitmapFactory.Options(); 91 options.inJustDecodeBounds = true; 92 BitmapFactory.decodeFile(wallpaper.getWallpaperFile().getAbsolutePath(), options); 93 if (options.outWidth <= 0 || options.outHeight <= 0) { 94 Slog.w(TAG, "Invalid wallpaper data"); 95 success = false; 96 } else { 97 boolean needCrop = false; 98 boolean needScale; 99 100 // Empty crop means use the full image 101 if (cropHint.isEmpty()) { 102 cropHint.left = cropHint.top = 0; 103 cropHint.right = options.outWidth; 104 cropHint.bottom = options.outHeight; 105 } else { 106 // force the crop rect to lie within the measured bounds 107 int dx = cropHint.right > options.outWidth ? options.outWidth - cropHint.right : 0; 108 int dy = cropHint.bottom > options.outHeight 109 ? options.outHeight - cropHint.bottom : 0; 110 cropHint.offset(dx, dy); 111 112 // If the crop hint was larger than the image we just overshot. Patch things up. 113 if (cropHint.left < 0) { 114 cropHint.left = 0; 115 } 116 if (cropHint.top < 0) { 117 cropHint.top = 0; 118 } 119 120 // Don't bother cropping if what we're left with is identity 121 needCrop = (options.outHeight > cropHint.height() 122 || options.outWidth > cropHint.width()); 123 } 124 125 // scale if the crop height winds up not matching the recommended metrics 126 needScale = cropHint.height() > wpData.mHeight 127 || cropHint.height() > GLHelper.getMaxTextureSize() 128 || cropHint.width() > GLHelper.getMaxTextureSize(); 129 130 //make sure screen aspect ratio is preserved if width is scaled under screen size 131 if (needScale) { 132 final float scaleByHeight = (float) wpData.mHeight / (float) cropHint.height(); 133 final int newWidth = (int) (cropHint.width() * scaleByHeight); 134 if (newWidth < displayInfo.logicalWidth) { 135 final float screenAspectRatio = 136 (float) displayInfo.logicalHeight / (float) displayInfo.logicalWidth; 137 cropHint.bottom = (int) (cropHint.width() * screenAspectRatio); 138 needCrop = true; 139 } 140 } 141 142 if (DEBUG_CROP) { 143 Slog.v(TAG, "crop: w=" + cropHint.width() + " h=" + cropHint.height()); 144 Slog.v(TAG, "dims: w=" + wpData.mWidth + " h=" + wpData.mHeight); 145 Slog.v(TAG, "meas: w=" + options.outWidth + " h=" + options.outHeight); 146 Slog.v(TAG, "crop?=" + needCrop + " scale?=" + needScale); 147 } 148 149 if (!needCrop && !needScale) { 150 // Simple case: the nominal crop fits what we want, so we take 151 // the whole thing and just copy the image file directly. 152 153 // TODO: It is not accurate to estimate bitmap size without decoding it, 154 // may be we can try to remove this optimized way in the future, 155 // that means, we will always go into the 'else' block. 156 157 success = FileUtils.copyFile(wallpaper.getWallpaperFile(), wallpaper.getCropFile()); 158 159 if (!success) { 160 wallpaper.getCropFile().delete(); 161 } 162 163 if (DEBUG) { 164 long estimateSize = (long) options.outWidth * options.outHeight * 4; 165 Slog.v(TAG, "Null crop of new wallpaper, estimate size=" 166 + estimateSize + ", success=" + success); 167 } 168 } else { 169 // Fancy case: crop and scale. First, we decode and scale down if appropriate. 170 FileOutputStream f = null; 171 BufferedOutputStream bos = null; 172 try { 173 // This actually downsamples only by powers of two, but that's okay; we do 174 // a proper scaling blit later. This is to minimize transient RAM use. 175 // We calculate the largest power-of-two under the actual ratio rather than 176 // just let the decode take care of it because we also want to remap where the 177 // cropHint rectangle lies in the decoded [super]rect. 178 final int actualScale = cropHint.height() / wpData.mHeight; 179 int scale = 1; 180 while (2 * scale <= actualScale) { 181 scale *= 2; 182 } 183 options.inSampleSize = scale; 184 options.inJustDecodeBounds = false; 185 186 final Rect estimateCrop = new Rect(cropHint); 187 estimateCrop.scale(1f / options.inSampleSize); 188 final float hRatio = (float) wpData.mHeight / estimateCrop.height(); 189 final int destHeight = (int) (estimateCrop.height() * hRatio); 190 final int destWidth = (int) (estimateCrop.width() * hRatio); 191 192 // We estimated an invalid crop, try to adjust the cropHint to get a valid one. 193 if (destWidth > GLHelper.getMaxTextureSize()) { 194 int newHeight = (int) (wpData.mHeight / hRatio); 195 int newWidth = (int) (wpData.mWidth / hRatio); 196 197 if (DEBUG) { 198 Slog.v(TAG, "Invalid crop dimensions, trying to adjust."); 199 } 200 201 estimateCrop.set(cropHint); 202 estimateCrop.left += (cropHint.width() - newWidth) / 2; 203 estimateCrop.top += (cropHint.height() - newHeight) / 2; 204 estimateCrop.right = estimateCrop.left + newWidth; 205 estimateCrop.bottom = estimateCrop.top + newHeight; 206 cropHint.set(estimateCrop); 207 estimateCrop.scale(1f / options.inSampleSize); 208 } 209 210 // We've got the safe cropHint; now we want to scale it properly to 211 // the desired rectangle. 212 // That's a height-biased operation: make it fit the hinted height. 213 final int safeHeight = (int) (estimateCrop.height() * hRatio); 214 final int safeWidth = (int) (estimateCrop.width() * hRatio); 215 216 if (DEBUG_CROP) { 217 Slog.v(TAG, "Decode parameters:"); 218 Slog.v(TAG, " cropHint=" + cropHint + ", estimateCrop=" + estimateCrop); 219 Slog.v(TAG, " down sampling=" + options.inSampleSize 220 + ", hRatio=" + hRatio); 221 Slog.v(TAG, " dest=" + destWidth + "x" + destHeight); 222 Slog.v(TAG, " safe=" + safeWidth + "x" + safeHeight); 223 Slog.v(TAG, " maxTextureSize=" + GLHelper.getMaxTextureSize()); 224 } 225 226 //Create a record file and will delete if ImageDecoder work well. 227 final String recordName = 228 (wallpaper.getWallpaperFile().getName().equals(WALLPAPER) 229 ? RECORD_FILE : RECORD_LOCK_FILE); 230 final File record = new File(getWallpaperDir(wallpaper.userId), recordName); 231 record.createNewFile(); 232 Slog.v(TAG, "record path =" + record.getPath() 233 + ", record name =" + record.getName()); 234 235 final ImageDecoder.Source srcData = 236 ImageDecoder.createSource(wallpaper.getWallpaperFile()); 237 final int sampleSize = scale; 238 Bitmap cropped = ImageDecoder.decodeBitmap(srcData, (decoder, info, src) -> { 239 decoder.setTargetSampleSize(sampleSize); 240 decoder.setCrop(estimateCrop); 241 }); 242 243 record.delete(); 244 245 if (cropped == null) { 246 Slog.e(TAG, "Could not decode new wallpaper"); 247 } else { 248 // We are safe to create final crop with safe dimensions now. 249 final Bitmap finalCrop = Bitmap.createScaledBitmap(cropped, 250 safeWidth, safeHeight, true); 251 if (DEBUG) { 252 Slog.v(TAG, "Final extract:"); 253 Slog.v(TAG, " dims: w=" + wpData.mWidth 254 + " h=" + wpData.mHeight); 255 Slog.v(TAG, " out: w=" + finalCrop.getWidth() 256 + " h=" + finalCrop.getHeight()); 257 } 258 259 f = new FileOutputStream(wallpaper.getCropFile()); 260 bos = new BufferedOutputStream(f, 32 * 1024); 261 finalCrop.compress(Bitmap.CompressFormat.PNG, 100, bos); 262 // don't rely on the implicit flush-at-close when noting success 263 bos.flush(); 264 success = true; 265 } 266 } catch (Exception e) { 267 if (DEBUG) { 268 Slog.e(TAG, "Error decoding crop", e); 269 } 270 } finally { 271 IoUtils.closeQuietly(bos); 272 IoUtils.closeQuietly(f); 273 } 274 } 275 } 276 277 if (!success) { 278 Slog.e(TAG, "Unable to apply new wallpaper"); 279 wallpaper.getCropFile().delete(); 280 } 281 282 if (wallpaper.getCropFile().exists()) { 283 boolean didRestorecon = SELinux.restorecon(wallpaper.getCropFile().getAbsoluteFile()); 284 if (DEBUG) { 285 Slog.v(TAG, "restorecon() of crop file returned " + didRestorecon); 286 } 287 } 288 } 289 } 290