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 17 18 package com.android.systemui.wallpapers; 19 20 import static com.android.window.flags.Flags.offloadColorExtraction; 21 22 import android.app.WallpaperColors; 23 import android.graphics.Bitmap; 24 import android.graphics.Rect; 25 import android.graphics.RectF; 26 import android.os.Trace; 27 import android.util.ArraySet; 28 import android.util.Log; 29 import android.util.MathUtils; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.VisibleForTesting; 33 34 import com.android.systemui.dagger.qualifiers.LongRunning; 35 import com.android.systemui.util.Assert; 36 37 import java.io.FileDescriptor; 38 import java.io.PrintWriter; 39 import java.util.ArrayList; 40 import java.util.List; 41 import java.util.Set; 42 import java.util.concurrent.Executor; 43 44 /** 45 * This class is used by the {@link ImageWallpaper} to extract colors from areas of a wallpaper. 46 * It uses a background executor, and uses callbacks to inform that the work is done. 47 * It uses a downscaled version of the wallpaper to extract the colors. 48 */ 49 public class WallpaperLocalColorExtractor { 50 51 private Bitmap mMiniBitmap; 52 53 @VisibleForTesting 54 static final int MINI_BITMAP_MAX_AREA = 112 * 112; 55 56 private static final String TAG = WallpaperLocalColorExtractor.class.getSimpleName(); 57 private static final @NonNull RectF LOCAL_COLOR_BOUNDS = 58 new RectF(0, 0, 1, 1); 59 60 private int mDisplayWidth = -1; 61 private int mDisplayHeight = -1; 62 private int mPages = -1; 63 private int mBitmapWidth = -1; 64 private int mBitmapHeight = -1; 65 66 private final Object mLock; 67 68 private final List<RectF> mPendingRegions = new ArrayList<>(); 69 private final Set<RectF> mProcessedRegions = new ArraySet<>(); 70 71 private float mWallpaperDimAmount = 0f; 72 private WallpaperColors mWallpaperColors; 73 74 // By default we assume that colors were loaded from disk and don't need to be recomputed 75 private boolean mRecomputeColors = false; 76 77 @LongRunning 78 private final Executor mLongExecutor; 79 80 private final WallpaperLocalColorExtractorCallback mWallpaperLocalColorExtractorCallback; 81 82 /** 83 * Interface to handle the callbacks after the different steps of the color extraction 84 */ 85 public interface WallpaperLocalColorExtractorCallback { 86 87 /** 88 * Callback after the wallpaper colors have been computed 89 */ onColorsProcessed()90 void onColorsProcessed(); 91 92 /** 93 * Callback after the colors of new regions have been extracted 94 * @param regions the list of new regions that have been processed 95 * @param colors the resulting colors for these regions, in the same order as the regions 96 */ onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors)97 void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors); 98 99 /** 100 * Callback after the mini bitmap is computed, to indicate that the wallpaper bitmap is 101 * no longer used by the color extractor and can be safely recycled 102 */ onMiniBitmapUpdated()103 void onMiniBitmapUpdated(); 104 105 /** 106 * Callback to inform that the extractor has started processing colors 107 */ onActivated()108 void onActivated(); 109 110 /** 111 * Callback to inform that no more colors are being processed 112 */ onDeactivated()113 void onDeactivated(); 114 } 115 116 /** 117 * Creates a new color extractor. 118 * @param longExecutor the executor on which the color extraction will be performed 119 * @param lock the lock object to use for computing colors or processing the bitmap 120 * @param wallpaperLocalColorExtractorCallback an interface to handle the callbacks from 121 * the color extractor. 122 */ WallpaperLocalColorExtractor(@ongRunning Executor longExecutor, Object lock, WallpaperLocalColorExtractorCallback wallpaperLocalColorExtractorCallback)123 public WallpaperLocalColorExtractor(@LongRunning Executor longExecutor, 124 Object lock, 125 WallpaperLocalColorExtractorCallback wallpaperLocalColorExtractorCallback) { 126 mLongExecutor = longExecutor; 127 mLock = lock; 128 mWallpaperLocalColorExtractorCallback = wallpaperLocalColorExtractorCallback; 129 } 130 131 /** 132 * Used by the outside to inform that the display size has changed. 133 * The new display size will be used in the next computations, but the current colors are 134 * not recomputed. 135 */ setDisplayDimensions(int displayWidth, int displayHeight)136 public void setDisplayDimensions(int displayWidth, int displayHeight) { 137 mLongExecutor.execute(() -> 138 setDisplayDimensionsSynchronized(displayWidth, displayHeight)); 139 } 140 setDisplayDimensionsSynchronized(int displayWidth, int displayHeight)141 private void setDisplayDimensionsSynchronized(int displayWidth, int displayHeight) { 142 synchronized (mLock) { 143 if (displayWidth == mDisplayWidth && displayHeight == mDisplayHeight) return; 144 mDisplayWidth = displayWidth; 145 mDisplayHeight = displayHeight; 146 processLocalColorsInternal(); 147 } 148 } 149 150 /** 151 * @return whether color extraction is currently in use 152 */ isActive()153 private boolean isActive() { 154 return mPendingRegions.size() + mProcessedRegions.size() > 0; 155 } 156 157 /** 158 * Should be called when the wallpaper is changed. 159 * This will recompute the mini bitmap 160 * and restart the extraction of all areas 161 * @param bitmap the new wallpaper 162 */ onBitmapChanged(@onNull Bitmap bitmap)163 public void onBitmapChanged(@NonNull Bitmap bitmap) { 164 mLongExecutor.execute(() -> onBitmapChangedSynchronized(bitmap)); 165 } 166 onBitmapChangedSynchronized(@onNull Bitmap bitmap)167 private void onBitmapChangedSynchronized(@NonNull Bitmap bitmap) { 168 synchronized (mLock) { 169 if (bitmap.isRecycled()) { 170 // ImageWallpaper loaded a new bitmap before the extraction of the previous one 171 // In that case, ImageWallpaper also scheduled the extraction of the next bitmap 172 Log.i(TAG, "Wallpaper has changed; deferring color extraction"); 173 return; 174 } 175 if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { 176 Log.e(TAG, "Attempt to extract colors from an invalid bitmap"); 177 return; 178 } 179 mBitmapWidth = bitmap.getWidth(); 180 mBitmapHeight = bitmap.getHeight(); 181 mMiniBitmap = createMiniBitmap(bitmap); 182 mWallpaperLocalColorExtractorCallback.onMiniBitmapUpdated(); 183 if (offloadColorExtraction() && mRecomputeColors) recomputeColorsInternal(); 184 recomputeLocalColors(); 185 } 186 } 187 188 /** 189 * Should be called when the number of pages is changed 190 * This will restart the extraction of all areas 191 * @param pages the total number of pages of the launcher 192 */ onPageChanged(int pages)193 public void onPageChanged(int pages) { 194 mLongExecutor.execute(() -> onPageChangedSynchronized(pages)); 195 } 196 onPageChangedSynchronized(int pages)197 private void onPageChangedSynchronized(int pages) { 198 synchronized (mLock) { 199 if (mPages == pages) return; 200 mPages = pages; 201 if (mMiniBitmap != null && !mMiniBitmap.isRecycled()) { 202 recomputeLocalColors(); 203 } 204 } 205 } 206 207 /** 208 * Should be called when the dim amount of the wallpaper changes, to recompute the colors 209 */ onDimAmountChanged(float dimAmount)210 public void onDimAmountChanged(float dimAmount) { 211 mLongExecutor.execute(() -> onDimAmountChangedSynchronized(dimAmount)); 212 } 213 onDimAmountChangedSynchronized(float dimAmount)214 private void onDimAmountChangedSynchronized(float dimAmount) { 215 synchronized (mLock) { 216 if (mWallpaperDimAmount == dimAmount) return; 217 mWallpaperDimAmount = dimAmount; 218 mRecomputeColors = true; 219 recomputeColorsInternal(); 220 } 221 } 222 223 /** 224 * To be called by {@link ImageWallpaper.CanvasEngine#onComputeColors}. This will either 225 * return the current wallpaper colors, or if the bitmap is not yet loaded, return null and call 226 * {@link WallpaperLocalColorExtractorCallback#onColorsProcessed()} when the colors are ready. 227 */ onComputeColors()228 public WallpaperColors onComputeColors() { 229 mLongExecutor.execute(this::onComputeColorsSynchronized); 230 return mWallpaperColors; 231 } 232 onComputeColorsSynchronized()233 private void onComputeColorsSynchronized() { 234 synchronized (mLock) { 235 if (mRecomputeColors) return; 236 mRecomputeColors = true; 237 recomputeColorsInternal(); 238 } 239 } 240 241 /** 242 * helper to recompute main colors, to be called in synchronized methods 243 */ recomputeColorsInternal()244 private void recomputeColorsInternal() { 245 if (mMiniBitmap == null) return; 246 mWallpaperColors = getWallpaperColors(mMiniBitmap, mWallpaperDimAmount); 247 mWallpaperLocalColorExtractorCallback.onColorsProcessed(); 248 } 249 250 @VisibleForTesting getWallpaperColors(@onNull Bitmap bitmap, float dimAmount)251 WallpaperColors getWallpaperColors(@NonNull Bitmap bitmap, float dimAmount) { 252 return WallpaperColors.fromBitmap(bitmap, dimAmount); 253 } 254 255 /** 256 * helper to recompute local colors, to be called in synchronized methods 257 */ recomputeLocalColors()258 private void recomputeLocalColors() { 259 mPendingRegions.addAll(mProcessedRegions); 260 mProcessedRegions.clear(); 261 processLocalColorsInternal(); 262 } 263 264 /** 265 * Add new regions to extract 266 * This will trigger the color extraction and call the callback only for these new regions 267 * @param regions The areas of interest in our wallpaper (in screen pixel coordinates) 268 */ addLocalColorsAreas(@onNull List<RectF> regions)269 public void addLocalColorsAreas(@NonNull List<RectF> regions) { 270 if (regions.size() > 0) { 271 mLongExecutor.execute(() -> addLocalColorsAreasSynchronized(regions)); 272 } else { 273 Log.w(TAG, "Attempt to add colors with an empty list"); 274 } 275 } 276 addLocalColorsAreasSynchronized(@onNull List<RectF> regions)277 private void addLocalColorsAreasSynchronized(@NonNull List<RectF> regions) { 278 synchronized (mLock) { 279 boolean wasActive = isActive(); 280 mPendingRegions.addAll(regions); 281 if (!wasActive && isActive()) { 282 mWallpaperLocalColorExtractorCallback.onActivated(); 283 } 284 processLocalColorsInternal(); 285 } 286 } 287 288 /** 289 * Remove regions to extract. If a color extraction is ongoing does not stop it. 290 * But if there are subsequent changes that restart the extraction, the removed regions 291 * will not be recomputed. 292 * @param regions The areas of interest in our wallpaper (in screen pixel coordinates) 293 */ removeLocalColorAreas(@onNull List<RectF> regions)294 public void removeLocalColorAreas(@NonNull List<RectF> regions) { 295 mLongExecutor.execute(() -> removeLocalColorAreasSynchronized(regions)); 296 } 297 removeLocalColorAreasSynchronized(@onNull List<RectF> regions)298 private void removeLocalColorAreasSynchronized(@NonNull List<RectF> regions) { 299 synchronized (mLock) { 300 boolean wasActive = isActive(); 301 mPendingRegions.removeAll(regions); 302 regions.forEach(mProcessedRegions::remove); 303 if (wasActive && !isActive()) { 304 mWallpaperLocalColorExtractorCallback.onDeactivated(); 305 } 306 } 307 } 308 309 /** 310 * Clean up the memory (in particular, the mini bitmap) used by this class. 311 */ cleanUp()312 public void cleanUp() { 313 mLongExecutor.execute(this::cleanUpSynchronized); 314 } 315 cleanUpSynchronized()316 private void cleanUpSynchronized() { 317 synchronized (mLock) { 318 if (mMiniBitmap != null) { 319 mMiniBitmap.recycle(); 320 mMiniBitmap = null; 321 } 322 mProcessedRegions.clear(); 323 mPendingRegions.clear(); 324 } 325 } 326 createMiniBitmap(@onNull Bitmap bitmap)327 private Bitmap createMiniBitmap(@NonNull Bitmap bitmap) { 328 Trace.beginSection("WallpaperLocalColorExtractor#createMiniBitmap"); 329 // if the area of the image is greater than MINI_BITMAP_MAX_AREA, downscale the bitmap. 330 int area = bitmap.getWidth() * bitmap.getHeight(); 331 double scale = Math.min(1, Math.sqrt((double) MINI_BITMAP_MAX_AREA / area)); 332 Bitmap result = createMiniBitmap(bitmap, 333 Math.max(1, (int) (scale * bitmap.getWidth())), 334 Math.max(1, (int) (scale * bitmap.getHeight()))); 335 Trace.endSection(); 336 return result; 337 } 338 339 @VisibleForTesting createMiniBitmap(@onNull Bitmap bitmap, int width, int height)340 Bitmap createMiniBitmap(@NonNull Bitmap bitmap, int width, int height) { 341 return Bitmap.createScaledBitmap(bitmap, width, height, false); 342 } 343 getLocalWallpaperColors(@onNull RectF area)344 private WallpaperColors getLocalWallpaperColors(@NonNull RectF area) { 345 RectF imageArea = pageToImgRect(area); 346 if (imageArea == null || !LOCAL_COLOR_BOUNDS.contains(imageArea)) { 347 return null; 348 } 349 Rect subImage = new Rect( 350 (int) Math.floor(imageArea.left * mMiniBitmap.getWidth()), 351 (int) Math.floor(imageArea.top * mMiniBitmap.getHeight()), 352 (int) Math.ceil(imageArea.right * mMiniBitmap.getWidth()), 353 (int) Math.ceil(imageArea.bottom * mMiniBitmap.getHeight())); 354 if (subImage.isEmpty()) { 355 // Do not notify client. treat it as too small to sample 356 return null; 357 } 358 return getLocalWallpaperColors(subImage); 359 } 360 361 @VisibleForTesting getLocalWallpaperColors(@onNull Rect subImage)362 WallpaperColors getLocalWallpaperColors(@NonNull Rect subImage) { 363 Assert.isNotMainThread(); 364 Bitmap colorImg = Bitmap.createBitmap(mMiniBitmap, 365 subImage.left, subImage.top, subImage.width(), subImage.height()); 366 return WallpaperColors.fromBitmap(colorImg); 367 } 368 369 /** 370 * Transform the logical coordinates into wallpaper coordinates. 371 * 372 * Logical coordinates are organised such that the various pages are non-overlapping. So, 373 * if there are n pages, the first page will have its X coordinate on the range [0-1/n]. 374 * 375 * The real pages are overlapping. If the Wallpaper are a width Ww and the screen a width 376 * Ws, the relative width of a page Wr is Ws/Ww. This does not change if the number of 377 * pages increase. 378 * If there are n pages, the page k starts at the offset k * (1 - Wr) / (n - 1), as the 379 * last page is at position (1-Wr) and the others are regularly spread on the range [0- 380 * (1-Wr)]. 381 */ pageToImgRect(RectF area)382 private RectF pageToImgRect(RectF area) { 383 // Width of a page for the caller of this API. 384 float virtualPageWidth = 1f / (float) mPages; 385 float leftPosOnPage = (area.left % virtualPageWidth) / virtualPageWidth; 386 float rightPosOnPage = (area.right % virtualPageWidth) / virtualPageWidth; 387 int currentPage = (int) Math.floor(area.centerX() / virtualPageWidth); 388 389 if (mDisplayWidth <= 0 || mDisplayHeight <= 0) { 390 Log.e(TAG, "Trying to extract colors with invalid display dimensions"); 391 return null; 392 } 393 394 RectF imgArea = new RectF(); 395 imgArea.bottom = area.bottom; 396 imgArea.top = area.top; 397 398 float imageScale = Math.min(((float) mBitmapHeight) / mDisplayHeight, 1); 399 float mappedScreenWidth = mDisplayWidth * imageScale; 400 float pageWidth = Math.min(1.0f, 401 mBitmapWidth > 0 ? mappedScreenWidth / (float) mBitmapWidth : 1.f); 402 float pageOffset = (1 - pageWidth) / (float) (mPages - 1); 403 404 imgArea.left = MathUtils.constrain( 405 leftPosOnPage * pageWidth + currentPage * pageOffset, 0, 1); 406 imgArea.right = MathUtils.constrain( 407 rightPosOnPage * pageWidth + currentPage * pageOffset, 0, 1); 408 if (imgArea.left > imgArea.right) { 409 // take full page 410 imgArea.left = 0; 411 imgArea.right = 1; 412 } 413 return imgArea; 414 } 415 416 /** 417 * Extract the colors from the pending regions, 418 * then notify the callback with the resulting colors for these regions 419 * This method should only be called synchronously 420 */ processLocalColorsInternal()421 private void processLocalColorsInternal() { 422 /* 423 * if the miniBitmap is not yet loaded, that means the onBitmapChanged has not yet been 424 * called, and thus the wallpaper is not yet loaded. In that case, exit, the function 425 * will be called again when the bitmap is loaded and the miniBitmap is computed. 426 */ 427 if (mMiniBitmap == null || mMiniBitmap.isRecycled()) return; 428 429 /* 430 * if the screen size or number of pages is not yet known, exit 431 * the function will be called again once the screen size and page are known 432 */ 433 if (mDisplayWidth < 0 || mDisplayHeight < 0 || mPages < 0) return; 434 435 Trace.beginSection("WallpaperLocalColorExtractor#processColorsInternal"); 436 List<WallpaperColors> processedColors = new ArrayList<>(); 437 for (int i = 0; i < mPendingRegions.size(); i++) { 438 RectF nextArea = mPendingRegions.get(i); 439 WallpaperColors colors = getLocalWallpaperColors(nextArea); 440 441 mProcessedRegions.add(nextArea); 442 processedColors.add(colors); 443 } 444 List<RectF> processedRegions = new ArrayList<>(mPendingRegions); 445 mPendingRegions.clear(); 446 Trace.endSection(); 447 448 mWallpaperLocalColorExtractorCallback.onColorsProcessed(processedRegions, processedColors); 449 } 450 451 /** 452 * Called to dump current state. 453 * @param prefix prefix. 454 * @param fd fd. 455 * @param out out. 456 * @param args args. 457 */ dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args)458 public void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) { 459 out.print(prefix); out.print("display="); out.println(mDisplayWidth + "x" + mDisplayHeight); 460 out.print(prefix); out.print("mPages="); out.println(mPages); 461 462 out.print(prefix); out.print("bitmap dimensions="); 463 out.println(mBitmapWidth + "x" + mBitmapHeight); 464 465 out.print(prefix); out.print("bitmap="); 466 out.println(mMiniBitmap == null ? "null" 467 : mMiniBitmap.isRecycled() ? "recycled" 468 : mMiniBitmap.getWidth() + "x" + mMiniBitmap.getHeight()); 469 470 out.print(prefix); out.print("PendingRegions size="); out.print(mPendingRegions.size()); 471 out.print(prefix); out.print("ProcessedRegions size="); out.print(mProcessedRegions.size()); 472 } 473 } 474