blob: 1e8446f8df1d5858219fc54cc158d1c6da71f676 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.wallpapers;
import android.app.WallpaperColors;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Trace;
import android.util.ArraySet;
import android.util.Log;
import android.util.MathUtils;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.systemui.dagger.qualifiers.LongRunning;
import com.android.systemui.util.Assert;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
/**
* This class is used by the {@link ImageWallpaper} to extract colors from areas of a wallpaper.
* It uses a background executor, and uses callbacks to inform that the work is done.
* It uses a downscaled version of the wallpaper to extract the colors.
*/
public class WallpaperLocalColorExtractor {
private Bitmap mMiniBitmap;
@VisibleForTesting
static final int SMALL_SIDE = 128;
private static final String TAG = WallpaperLocalColorExtractor.class.getSimpleName();
private static final @NonNull RectF LOCAL_COLOR_BOUNDS =
new RectF(0, 0, 1, 1);
private int mDisplayWidth = -1;
private int mDisplayHeight = -1;
private int mPages = -1;
private int mBitmapWidth = -1;
private int mBitmapHeight = -1;
private final Object mLock = new Object();
private final List<RectF> mPendingRegions = new ArrayList<>();
private final Set<RectF> mProcessedRegions = new ArraySet<>();
@LongRunning
private final Executor mLongExecutor;
private final WallpaperLocalColorExtractorCallback mWallpaperLocalColorExtractorCallback;
/**
* Interface to handle the callbacks after the different steps of the color extraction
*/
public interface WallpaperLocalColorExtractorCallback {
/**
* Callback after the colors of new regions have been extracted
* @param regions the list of new regions that have been processed
* @param colors the resulting colors for these regions, in the same order as the regions
*/
void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors);
/**
* Callback after the mini bitmap is computed, to indicate that the wallpaper bitmap is
* no longer used by the color extractor and can be safely recycled
*/
void onMiniBitmapUpdated();
/**
* Callback to inform that the extractor has started processing colors
*/
void onActivated();
/**
* Callback to inform that no more colors are being processed
*/
void onDeactivated();
}
/**
* Creates a new color extractor.
* @param longExecutor the executor on which the color extraction will be performed
* @param wallpaperLocalColorExtractorCallback an interface to handle the callbacks from
* the color extractor.
*/
public WallpaperLocalColorExtractor(@LongRunning Executor longExecutor,
WallpaperLocalColorExtractorCallback wallpaperLocalColorExtractorCallback) {
mLongExecutor = longExecutor;
mWallpaperLocalColorExtractorCallback = wallpaperLocalColorExtractorCallback;
}
/**
* Used by the outside to inform that the display size has changed.
* The new display size will be used in the next computations, but the current colors are
* not recomputed.
*/
public void setDisplayDimensions(int displayWidth, int displayHeight) {
mLongExecutor.execute(() ->
setDisplayDimensionsSynchronized(displayWidth, displayHeight));
}
private void setDisplayDimensionsSynchronized(int displayWidth, int displayHeight) {
synchronized (mLock) {
if (displayWidth == mDisplayWidth && displayHeight == mDisplayHeight) return;
mDisplayWidth = displayWidth;
mDisplayHeight = displayHeight;
processColorsInternal();
}
}
/**
* @return whether color extraction is currently in use
*/
private boolean isActive() {
return mPendingRegions.size() + mProcessedRegions.size() > 0;
}
/**
* Should be called when the wallpaper is changed.
* This will recompute the mini bitmap
* and restart the extraction of all areas
* @param bitmap the new wallpaper
*/
public void onBitmapChanged(@NonNull Bitmap bitmap) {
mLongExecutor.execute(() -> onBitmapChangedSynchronized(bitmap));
}
private void onBitmapChangedSynchronized(@NonNull Bitmap bitmap) {
synchronized (mLock) {
if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
Log.e(TAG, "Attempt to extract colors from an invalid bitmap");
return;
}
mBitmapWidth = bitmap.getWidth();
mBitmapHeight = bitmap.getHeight();
mMiniBitmap = createMiniBitmap(bitmap);
mWallpaperLocalColorExtractorCallback.onMiniBitmapUpdated();
recomputeColors();
}
}
/**
* Should be called when the number of pages is changed
* This will restart the extraction of all areas
* @param pages the total number of pages of the launcher
*/
public void onPageChanged(int pages) {
mLongExecutor.execute(() -> onPageChangedSynchronized(pages));
}
private void onPageChangedSynchronized(int pages) {
synchronized (mLock) {
if (mPages == pages) return;
mPages = pages;
if (mMiniBitmap != null && !mMiniBitmap.isRecycled()) {
recomputeColors();
}
}
}
// helper to recompute colors, to be called in synchronized methods
private void recomputeColors() {
mPendingRegions.addAll(mProcessedRegions);
mProcessedRegions.clear();
processColorsInternal();
}
/**
* Add new regions to extract
* This will trigger the color extraction and call the callback only for these new regions
* @param regions The areas of interest in our wallpaper (in screen pixel coordinates)
*/
public void addLocalColorsAreas(@NonNull List<RectF> regions) {
if (regions.size() > 0) {
mLongExecutor.execute(() -> addLocalColorsAreasSynchronized(regions));
} else {
Log.w(TAG, "Attempt to add colors with an empty list");
}
}
private void addLocalColorsAreasSynchronized(@NonNull List<RectF> regions) {
synchronized (mLock) {
boolean wasActive = isActive();
mPendingRegions.addAll(regions);
if (!wasActive && isActive()) {
mWallpaperLocalColorExtractorCallback.onActivated();
}
processColorsInternal();
}
}
/**
* Remove regions to extract. If a color extraction is ongoing does not stop it.
* But if there are subsequent changes that restart the extraction, the removed regions
* will not be recomputed.
* @param regions The areas of interest in our wallpaper (in screen pixel coordinates)
*/
public void removeLocalColorAreas(@NonNull List<RectF> regions) {
mLongExecutor.execute(() -> removeLocalColorAreasSynchronized(regions));
}
private void removeLocalColorAreasSynchronized(@NonNull List<RectF> regions) {
synchronized (mLock) {
boolean wasActive = isActive();
mPendingRegions.removeAll(regions);
regions.forEach(mProcessedRegions::remove);
if (wasActive && !isActive()) {
mWallpaperLocalColorExtractorCallback.onDeactivated();
}
}
}
/**
* Clean up the memory (in particular, the mini bitmap) used by this class.
*/
public void cleanUp() {
mLongExecutor.execute(this::cleanUpSynchronized);
}
private void cleanUpSynchronized() {
synchronized (mLock) {
if (mMiniBitmap != null) {
mMiniBitmap.recycle();
mMiniBitmap = null;
}
mProcessedRegions.clear();
mPendingRegions.clear();
}
}
private Bitmap createMiniBitmap(@NonNull Bitmap bitmap) {
Trace.beginSection("WallpaperLocalColorExtractor#createMiniBitmap");
// if both sides of the image are larger than SMALL_SIDE, downscale the bitmap.
int smallestSide = Math.min(bitmap.getWidth(), bitmap.getHeight());
float scale = Math.min(1.0f, (float) SMALL_SIDE / smallestSide);
Bitmap result = createMiniBitmap(bitmap,
(int) (scale * bitmap.getWidth()),
(int) (scale * bitmap.getHeight()));
Trace.endSection();
return result;
}
@VisibleForTesting
Bitmap createMiniBitmap(@NonNull Bitmap bitmap, int width, int height) {
return Bitmap.createScaledBitmap(bitmap, width, height, false);
}
private WallpaperColors getLocalWallpaperColors(@NonNull RectF area) {
RectF imageArea = pageToImgRect(area);
if (imageArea == null || !LOCAL_COLOR_BOUNDS.contains(imageArea)) {
return null;
}
Rect subImage = new Rect(
(int) Math.floor(imageArea.left * mMiniBitmap.getWidth()),
(int) Math.floor(imageArea.top * mMiniBitmap.getHeight()),
(int) Math.ceil(imageArea.right * mMiniBitmap.getWidth()),
(int) Math.ceil(imageArea.bottom * mMiniBitmap.getHeight()));
if (subImage.isEmpty()) {
// Do not notify client. treat it as too small to sample
return null;
}
return getLocalWallpaperColors(subImage);
}
@VisibleForTesting
WallpaperColors getLocalWallpaperColors(@NonNull Rect subImage) {
Assert.isNotMainThread();
Bitmap colorImg = Bitmap.createBitmap(mMiniBitmap,
subImage.left, subImage.top, subImage.width(), subImage.height());
return WallpaperColors.fromBitmap(colorImg);
}
/**
* Transform the logical coordinates into wallpaper coordinates.
*
* Logical coordinates are organised such that the various pages are non-overlapping. So,
* if there are n pages, the first page will have its X coordinate on the range [0-1/n].
*
* The real pages are overlapping. If the Wallpaper are a width Ww and the screen a width
* Ws, the relative width of a page Wr is Ws/Ww. This does not change if the number of
* pages increase.
* If there are n pages, the page k starts at the offset k * (1 - Wr) / (n - 1), as the
* last page is at position (1-Wr) and the others are regularly spread on the range [0-
* (1-Wr)].
*/
private RectF pageToImgRect(RectF area) {
// Width of a page for the caller of this API.
float virtualPageWidth = 1f / (float) mPages;
float leftPosOnPage = (area.left % virtualPageWidth) / virtualPageWidth;
float rightPosOnPage = (area.right % virtualPageWidth) / virtualPageWidth;
int currentPage = (int) Math.floor(area.centerX() / virtualPageWidth);
if (mDisplayWidth <= 0 || mDisplayHeight <= 0) {
Log.e(TAG, "Trying to extract colors with invalid display dimensions");
return null;
}
RectF imgArea = new RectF();
imgArea.bottom = area.bottom;
imgArea.top = area.top;
float imageScale = Math.min(((float) mBitmapHeight) / mDisplayHeight, 1);
float mappedScreenWidth = mDisplayWidth * imageScale;
float pageWidth = Math.min(1.0f,
mBitmapWidth > 0 ? mappedScreenWidth / (float) mBitmapWidth : 1.f);
float pageOffset = (1 - pageWidth) / (float) (mPages - 1);
imgArea.left = MathUtils.constrain(
leftPosOnPage * pageWidth + currentPage * pageOffset, 0, 1);
imgArea.right = MathUtils.constrain(
rightPosOnPage * pageWidth + currentPage * pageOffset, 0, 1);
if (imgArea.left > imgArea.right) {
// take full page
imgArea.left = 0;
imgArea.right = 1;
}
return imgArea;
}
/**
* Extract the colors from the pending regions,
* then notify the callback with the resulting colors for these regions
* This method should only be called synchronously
*/
private void processColorsInternal() {
/*
* if the miniBitmap is not yet loaded, that means the onBitmapChanged has not yet been
* called, and thus the wallpaper is not yet loaded. In that case, exit, the function
* will be called again when the bitmap is loaded and the miniBitmap is computed.
*/
if (mMiniBitmap == null || mMiniBitmap.isRecycled()) return;
/*
* if the screen size or number of pages is not yet known, exit
* the function will be called again once the screen size and page are known
*/
if (mDisplayWidth < 0 || mDisplayHeight < 0 || mPages < 0) return;
Trace.beginSection("WallpaperLocalColorExtractor#processColorsInternal");
List<WallpaperColors> processedColors = new ArrayList<>();
for (int i = 0; i < mPendingRegions.size(); i++) {
RectF nextArea = mPendingRegions.get(i);
WallpaperColors colors = getLocalWallpaperColors(nextArea);
mProcessedRegions.add(nextArea);
processedColors.add(colors);
}
List<RectF> processedRegions = new ArrayList<>(mPendingRegions);
mPendingRegions.clear();
Trace.endSection();
mWallpaperLocalColorExtractorCallback.onColorsProcessed(processedRegions, processedColors);
}
/**
* Called to dump current state.
* @param prefix prefix.
* @param fd fd.
* @param out out.
* @param args args.
*/
public void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) {
out.print(prefix); out.print("display="); out.println(mDisplayWidth + "x" + mDisplayHeight);
out.print(prefix); out.print("mPages="); out.println(mPages);
out.print(prefix); out.print("bitmap dimensions=");
out.println(mBitmapWidth + "x" + mBitmapHeight);
out.print(prefix); out.print("bitmap=");
out.println(mMiniBitmap == null ? "null"
: mMiniBitmap.isRecycled() ? "recycled"
: mMiniBitmap.getWidth() + "x" + mMiniBitmap.getHeight());
out.print(prefix); out.print("PendingRegions size="); out.print(mPendingRegions.size());
out.print(prefix); out.print("ProcessedRegions size="); out.print(mProcessedRegions.size());
}
}