• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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