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