Rewrite thread logic, add tests and add color extraction for CanvasEngine

Thread logic has been rewritten to use two executors (main and
background) instead of handlers or asyncTasks.

Color extraction has been implemented similarly to the GL engine, but has been refactored into a new class.
New tests have been added for both the color extraction and the
CanvasEngine.

Other details have been changed to make the CanvasEngine behaviour as close as possible to the previous GL Engine.

Trace and dump logic have been added.

Many details have changed and this CL requires careful review.

Test: manual
Test: atest ImageWallpaperTest
Test: atest WallpaperColorExtractorTest
Bug: 243402530

Change-Id: Ic1db1151bc83e0ac2a13301adfa778e78333af05
This commit is contained in:
Aurélien Pomini
2022-09-26 11:08:10 +00:00
parent c65bf000b9
commit c0ee98f704
6 changed files with 1241 additions and 466 deletions

View File

@@ -16,21 +16,17 @@
package com.android.systemui.wallpapers;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.systemui.flags.Flags.USE_CANVAS_RENDERER;
import android.app.WallpaperColors;
import android.app.WallpaperManager;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.RecordingCanvas;
import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
@@ -40,8 +36,6 @@ import android.util.ArraySet;
import android.util.Log;
import android.util.MathUtils;
import android.util.Size;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.WindowManager;
@@ -49,8 +43,11 @@ import android.view.WindowManager;
import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.wallpapers.canvas.ImageCanvasWallpaperRenderer;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.wallpapers.canvas.WallpaperColorExtractor;
import com.android.systemui.wallpapers.gl.EglHelper;
import com.android.systemui.wallpapers.gl.ImageWallpaperRenderer;
@@ -59,6 +56,7 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import javax.inject.Inject;
@@ -78,15 +76,28 @@ public class ImageWallpaper extends WallpaperService {
private final ArrayList<RectF> mLocalColorsToAdd = new ArrayList<>();
private final ArraySet<RectF> mColorAreas = new ArraySet<>();
private volatile int mPages = 1;
private boolean mPagesComputed = false;
private HandlerThread mWorker;
// scaled down version
private Bitmap mMiniBitmap;
private final FeatureFlags mFeatureFlags;
// used in canvasEngine to load/unload the bitmap and extract the colors
@Background
private final DelayableExecutor mBackgroundExecutor;
private static final int DELAY_UNLOAD_BITMAP = 2000;
@Main
private final Executor mMainExecutor;
@Inject
public ImageWallpaper(FeatureFlags featureFlags) {
public ImageWallpaper(FeatureFlags featureFlags,
@Background DelayableExecutor backgroundExecutor,
@Main Executor mainExecutor) {
super();
mFeatureFlags = featureFlags;
mBackgroundExecutor = backgroundExecutor;
mMainExecutor = mainExecutor;
}
@Override
@@ -339,7 +350,6 @@ public class ImageWallpaper extends WallpaperService {
imgArea.left = 0;
imgArea.right = 1;
}
return imgArea;
}
@@ -510,69 +520,84 @@ public class ImageWallpaper extends WallpaperService {
class CanvasEngine extends WallpaperService.Engine implements DisplayListener {
// time [ms] before unloading the wallpaper after it is loaded
private static final int DELAY_FORGET_WALLPAPER = 5000;
private final Runnable mUnloadWallpaperCallback = this::unloadWallpaper;
private WallpaperManager mWallpaperManager;
private ImageCanvasWallpaperRenderer mImageCanvasWallpaperRenderer;
private final WallpaperColorExtractor mWallpaperColorExtractor;
private SurfaceHolder mSurfaceHolder;
@VisibleForTesting
static final int MIN_SURFACE_WIDTH = 128;
@VisibleForTesting
static final int MIN_SURFACE_HEIGHT = 128;
private Bitmap mBitmap;
private Display mDisplay;
private final DisplayInfo mTmpDisplayInfo = new DisplayInfo();
private AsyncTask<Void, Void, Bitmap> mLoader;
private boolean mNeedsDrawAfterLoadingWallpaper = false;
/*
* Counter to unload the bitmap as soon as possible.
* Before any bitmap operation, this is incremented.
* After an operation completion, this is decremented (synchronously),
* and if the count is 0, unload the bitmap
*/
private int mBitmapUsages = 0;
private final Object mLock = new Object();
CanvasEngine() {
super();
setFixedSizeAllowed(true);
setShowForAllUsers(true);
}
mWallpaperColorExtractor = new WallpaperColorExtractor(
mBackgroundExecutor,
new WallpaperColorExtractor.WallpaperColorExtractorCallback() {
@Override
public void onColorsProcessed(List<RectF> regions,
List<WallpaperColors> colors) {
CanvasEngine.this.onColorsProcessed(regions, colors);
}
void trimMemory(int level) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW
&& level <= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL
&& isBitmapLoaded()) {
if (DEBUG) {
Log.d(TAG, "trimMemory");
}
unloadWallpaper();
@Override
public void onMiniBitmapUpdated() {
CanvasEngine.this.onMiniBitmapUpdated();
}
@Override
public void onActivated() {
setOffsetNotificationsEnabled(true);
}
@Override
public void onDeactivated() {
setOffsetNotificationsEnabled(false);
}
});
// if the number of pages is already computed, transmit it to the color extractor
if (mPagesComputed) {
mWallpaperColorExtractor.onPageChanged(mPages);
}
}
@Override
public void onCreate(SurfaceHolder surfaceHolder) {
Trace.beginSection("ImageWallpaper.CanvasEngine#onCreate");
if (DEBUG) {
Log.d(TAG, "onCreate");
}
mWallpaperManager = getDisplayContext().getSystemService(WallpaperManager.class);
mSurfaceHolder = surfaceHolder;
Rect dimensions = mWallpaperManager.peekBitmapDimensions();
int width = Math.max(MIN_SURFACE_WIDTH, dimensions.width());
int height = Math.max(MIN_SURFACE_HEIGHT, dimensions.height());
mSurfaceHolder.setFixedSize(width, height);
mWallpaperManager = getSystemService(WallpaperManager.class);
super.onCreate(surfaceHolder);
final Context displayContext = getDisplayContext();
final int displayId = displayContext == null ? DEFAULT_DISPLAY :
displayContext.getDisplayId();
DisplayManager dm = getSystemService(DisplayManager.class);
if (dm != null) {
mDisplay = dm.getDisplay(displayId);
if (mDisplay == null) {
Log.e(TAG, "Cannot find display! Fallback to default.");
mDisplay = dm.getDisplay(DEFAULT_DISPLAY);
}
}
setOffsetNotificationsEnabled(false);
mImageCanvasWallpaperRenderer = new ImageCanvasWallpaperRenderer(surfaceHolder);
loadWallpaper(false);
getDisplayContext().getSystemService(DisplayManager.class)
.registerDisplayListener(this, null);
getDisplaySizeAndUpdateColorExtractor();
Trace.endSection();
}
@Override
public void onDestroy() {
super.onDestroy();
unloadWallpaper();
getDisplayContext().getSystemService(DisplayManager.class)
.unregisterDisplayListener(this);
mWallpaperColorExtractor.cleanUp();
unloadBitmap();
}
@Override
@@ -580,32 +605,31 @@ public class ImageWallpaper extends WallpaperService {
return true;
}
@Override
public boolean shouldWaitForEngineShown() {
return true;
}
@Override
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (DEBUG) {
Log.d(TAG, "onSurfaceChanged: width=" + width + ", height=" + height);
}
super.onSurfaceChanged(holder, format, width, height);
mImageCanvasWallpaperRenderer.setSurfaceHolder(holder);
drawFrame(false);
}
@Override
public void onSurfaceDestroyed(SurfaceHolder holder) {
super.onSurfaceDestroyed(holder);
if (DEBUG) {
Log.i(TAG, "onSurfaceDestroyed");
}
mImageCanvasWallpaperRenderer.setSurfaceHolder(null);
mSurfaceHolder = null;
}
@Override
public void onSurfaceCreated(SurfaceHolder holder) {
super.onSurfaceCreated(holder);
if (DEBUG) {
Log.i(TAG, "onSurfaceCreated");
}
mImageCanvasWallpaperRenderer.setSurfaceHolder(holder);
}
@Override
@@ -613,135 +637,88 @@ public class ImageWallpaper extends WallpaperService {
if (DEBUG) {
Log.d(TAG, "onSurfaceRedrawNeeded");
}
super.onSurfaceRedrawNeeded(holder);
// At the end of this method we should have drawn into the surface.
// This means that the bitmap should be loaded synchronously if
// it was already unloaded.
if (!isBitmapLoaded()) {
setBitmap(mWallpaperManager.getBitmap(true /* hardware */));
drawFrame();
}
private void drawFrame() {
mBackgroundExecutor.execute(this::drawFrameSynchronized);
}
private void drawFrameSynchronized() {
synchronized (mLock) {
drawFrameInternal();
}
drawFrame(true);
}
private DisplayInfo getDisplayInfo() {
mDisplay.getDisplayInfo(mTmpDisplayInfo);
return mTmpDisplayInfo;
}
private void drawFrame(boolean forceRedraw) {
if (!mImageCanvasWallpaperRenderer.isSurfaceHolderLoaded()) {
private void drawFrameInternal() {
if (mSurfaceHolder == null) {
Log.e(TAG, "attempt to draw a frame without a valid surface");
return;
}
// load the wallpaper if not already done
if (!isBitmapLoaded()) {
// ensure that we load the wallpaper.
// if the wallpaper is currently loading, this call will have no effect.
loadWallpaper(true);
return;
}
mImageCanvasWallpaperRenderer.drawFrame(mBitmap, forceRedraw);
}
private void setBitmap(Bitmap bitmap) {
if (bitmap == null) {
Log.e(TAG, "Attempt to set a null bitmap");
} else if (mBitmap == bitmap) {
Log.e(TAG, "The value of bitmap is the same");
} else if (bitmap.getWidth() < 1 || bitmap.getHeight() < 1) {
Log.e(TAG, "Attempt to set an invalid wallpaper of length "
+ bitmap.getWidth() + "x" + bitmap.getHeight());
loadWallpaperAndDrawFrameInternal();
} else {
if (mBitmap != null) {
mBitmap.recycle();
}
mBitmap = bitmap;
mBitmapUsages++;
// drawing is done on the main thread
mMainExecutor.execute(() -> {
drawFrameOnCanvas(mBitmap);
reportEngineShown(false);
unloadBitmapIfNotUsed();
});
}
}
private boolean isBitmapLoaded() {
@VisibleForTesting
void drawFrameOnCanvas(Bitmap bitmap) {
Trace.beginSection("ImageWallpaper.CanvasEngine#drawFrame");
// TODO change SurfaceHolder API to add wcg support
Canvas c = mSurfaceHolder.lockHardwareCanvas();
if (c != null) {
Rect dest = mSurfaceHolder.getSurfaceFrame();
try {
c.drawBitmap(bitmap, null, dest, null);
} finally {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
Trace.endSection();
}
@VisibleForTesting
boolean isBitmapLoaded() {
return mBitmap != null && !mBitmap.isRecycled();
}
/**
* Loads the wallpaper on background thread and schedules updating the surface frame,
* and if {@code needsDraw} is set also draws a frame.
*
* If loading is already in-flight, subsequent loads are ignored (but needDraw is or-ed to
* the active request).
*
*/
private void loadWallpaper(boolean needsDraw) {
mNeedsDrawAfterLoadingWallpaper |= needsDraw;
if (mLoader != null) {
if (DEBUG) {
Log.d(TAG, "Skipping loadWallpaper, already in flight ");
}
return;
}
mLoader = new AsyncTask<Void, Void, Bitmap>() {
@Override
protected Bitmap doInBackground(Void... params) {
Throwable exception;
try {
Bitmap wallpaper = mWallpaperManager.getBitmap(true /* hardware */);
if (wallpaper != null
&& wallpaper.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) {
throw new RuntimeException("Wallpaper is too large to draw!");
}
return wallpaper;
} catch (RuntimeException | OutOfMemoryError e) {
exception = e;
}
if (isCancelled()) {
return null;
}
// Note that if we do fail at this, and the default wallpaper can't
// be loaded, we will go into a cycle. Don't do a build where the
// default wallpaper can't be loaded.
Log.w(TAG, "Unable to load wallpaper!", exception);
try {
mWallpaperManager.clear();
} catch (IOException ex) {
// now we're really screwed.
Log.w(TAG, "Unable reset to default wallpaper!", ex);
}
if (isCancelled()) {
return null;
}
try {
return mWallpaperManager.getBitmap(true /* hardware */);
} catch (RuntimeException | OutOfMemoryError e) {
Log.w(TAG, "Unable to load default wallpaper!", e);
}
return null;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
setBitmap(bitmap);
if (mNeedsDrawAfterLoadingWallpaper) {
drawFrame(true);
}
mLoader = null;
mNeedsDrawAfterLoadingWallpaper = false;
scheduleUnloadWallpaper();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
private void unloadBitmapIfNotUsed() {
mBackgroundExecutor.execute(this::unloadBitmapIfNotUsedSynchronized);
}
private void unloadWallpaper() {
if (mLoader != null) {
mLoader.cancel(false);
mLoader = null;
private void unloadBitmapIfNotUsedSynchronized() {
synchronized (mLock) {
mBitmapUsages -= 1;
if (mBitmapUsages <= 0) {
mBitmapUsages = 0;
unloadBitmapInternal();
}
}
}
private void unloadBitmap() {
mBackgroundExecutor.execute(this::unloadBitmapSynchronized);
}
private void unloadBitmapSynchronized() {
synchronized (mLock) {
mBitmapUsages = 0;
unloadBitmapInternal();
}
}
private void unloadBitmapInternal() {
Trace.beginSection("ImageWallpaper.CanvasEngine#unloadBitmap");
if (mBitmap != null) {
mBitmap.recycle();
}
@@ -750,12 +727,133 @@ public class ImageWallpaper extends WallpaperService {
final Surface surface = getSurfaceHolder().getSurface();
surface.hwuiDestroy();
mWallpaperManager.forgetLoadedWallpaper();
Trace.endSection();
}
private void scheduleUnloadWallpaper() {
Handler handler = getMainThreadHandler();
handler.removeCallbacks(mUnloadWallpaperCallback);
handler.postDelayed(mUnloadWallpaperCallback, DELAY_FORGET_WALLPAPER);
private void loadWallpaperAndDrawFrameInternal() {
Trace.beginSection("ImageWallpaper.CanvasEngine#loadWallpaper");
boolean loadSuccess = false;
Bitmap bitmap;
try {
bitmap = mWallpaperManager.getBitmap(false);
if (bitmap != null
&& bitmap.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) {
throw new RuntimeException("Wallpaper is too large to draw!");
}
} catch (RuntimeException | OutOfMemoryError exception) {
// Note that if we do fail at this, and the default wallpaper can't
// be loaded, we will go into a cycle. Don't do a build where the
// default wallpaper can't be loaded.
Log.w(TAG, "Unable to load wallpaper!", exception);
try {
mWallpaperManager.clear(WallpaperManager.FLAG_SYSTEM);
} catch (IOException ex) {
// now we're really screwed.
Log.w(TAG, "Unable reset to default wallpaper!", ex);
}
try {
bitmap = mWallpaperManager.getBitmap(false);
} catch (RuntimeException | OutOfMemoryError e) {
Log.w(TAG, "Unable to load default wallpaper!", e);
bitmap = null;
}
}
if (bitmap == null) {
Log.w(TAG, "Could not load bitmap");
} else if (bitmap.isRecycled()) {
Log.e(TAG, "Attempt to load a recycled bitmap");
} else if (mBitmap == bitmap) {
Log.e(TAG, "Loaded a bitmap that was already loaded");
} else if (bitmap.getWidth() < 1 || bitmap.getHeight() < 1) {
Log.e(TAG, "Attempt to load an invalid wallpaper of length "
+ bitmap.getWidth() + "x" + bitmap.getHeight());
} else {
// at this point, loading is done correctly.
loadSuccess = true;
// recycle the previously loaded bitmap
if (mBitmap != null) {
mBitmap.recycle();
}
mBitmap = bitmap;
// +2 usages for the color extraction and the delayed unload.
mBitmapUsages += 2;
recomputeColorExtractorMiniBitmap();
drawFrameInternal();
/*
* after loading, the bitmap will be unloaded after all these conditions:
* - the frame is redrawn
* - the mini bitmap from color extractor is recomputed
* - the DELAY_UNLOAD_BITMAP has passed
*/
mBackgroundExecutor.executeDelayed(
this::unloadBitmapIfNotUsedSynchronized, DELAY_UNLOAD_BITMAP);
}
// even if the bitmap cannot be loaded, call reportEngineShown
if (!loadSuccess) reportEngineShown(false);
Trace.endSection();
}
private void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors) {
try {
notifyLocalColorsChanged(regions, colors);
} catch (RuntimeException e) {
Log.e(TAG, e.getMessage(), e);
}
}
@VisibleForTesting
void recomputeColorExtractorMiniBitmap() {
mWallpaperColorExtractor.onBitmapChanged(mBitmap);
}
@VisibleForTesting
void onMiniBitmapUpdated() {
unloadBitmapIfNotUsed();
}
@Override
public boolean supportsLocalColorExtraction() {
return true;
}
@Override
public void addLocalColorsAreas(@NonNull List<RectF> regions) {
// this call will activate the offset notifications
// if no colors were being processed before
mWallpaperColorExtractor.addLocalColorsAreas(regions);
}
@Override
public void removeLocalColorsAreas(@NonNull List<RectF> regions) {
// this call will deactivate the offset notifications
// if we are no longer processing colors
mWallpaperColorExtractor.removeLocalColorAreas(regions);
}
@Override
public void onOffsetsChanged(float xOffset, float yOffset,
float xOffsetStep, float yOffsetStep,
int xPixelOffset, int yPixelOffset) {
/*
* TODO check this formula. mPages is always >= 4, even when launcher is single-paged
* this formula is also used in the GL engine
*/
final int pages;
if (xOffsetStep > 0 && xOffsetStep <= 1) {
pages = Math.round(1 / xOffsetStep) + 1;
} else {
pages = 1;
}
if (pages != mPages || !mPagesComputed) {
mPages = pages;
mPagesComputed = true;
mWallpaperColorExtractor.onPageChanged(mPages);
}
}
@Override
@@ -764,13 +862,46 @@ public class ImageWallpaper extends WallpaperService {
}
@Override
public void onDisplayChanged(int displayId) {
public void onDisplayRemoved(int displayId) {
}
@Override
public void onDisplayRemoved(int displayId) {
public void onDisplayChanged(int displayId) {
// changes the display in the color extractor
// the new display dimensions will be used in the next color computation
if (displayId == getDisplayContext().getDisplayId()) {
getDisplaySizeAndUpdateColorExtractor();
}
}
private void getDisplaySizeAndUpdateColorExtractor() {
Rect window = getDisplayContext()
.getSystemService(WindowManager.class)
.getCurrentWindowMetrics()
.getBounds();
mWallpaperColorExtractor.setDisplayDimensions(window.width(), window.height());
}
@Override
protected void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) {
super.dump(prefix, fd, out, args);
out.print(prefix); out.print("Engine="); out.println(this);
out.print(prefix); out.print("valid surface=");
out.println(getSurfaceHolder() != null && getSurfaceHolder().getSurface() != null
? getSurfaceHolder().getSurface().isValid()
: "null");
out.print(prefix); out.print("surface frame=");
out.println(getSurfaceHolder() != null ? getSurfaceHolder().getSurfaceFrame() : "null");
out.print(prefix); out.print("bitmap=");
out.println(mBitmap == null ? "null"
: mBitmap.isRecycled() ? "recycled"
: mBitmap.getWidth() + "x" + mBitmap.getHeight());
mWallpaperColorExtractor.dump(prefix, fd, out, args);
}
}
}

View File

@@ -1,145 +0,0 @@
/*
* 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.canvas;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.Log;
import android.view.SurfaceHolder;
import com.android.internal.annotations.VisibleForTesting;
/**
* Helper to draw a wallpaper on a surface.
* It handles the geometry regarding the dimensions of the display and the wallpaper,
* and rescales the surface and the wallpaper accordingly.
*/
public class ImageCanvasWallpaperRenderer {
private static final String TAG = ImageCanvasWallpaperRenderer.class.getSimpleName();
private static final boolean DEBUG = false;
private SurfaceHolder mSurfaceHolder;
//private Bitmap mBitmap = null;
@VisibleForTesting
static final int MIN_SURFACE_WIDTH = 128;
@VisibleForTesting
static final int MIN_SURFACE_HEIGHT = 128;
private boolean mSurfaceRedrawNeeded;
private int mLastSurfaceWidth = -1;
private int mLastSurfaceHeight = -1;
public ImageCanvasWallpaperRenderer(SurfaceHolder surfaceHolder) {
mSurfaceHolder = surfaceHolder;
}
/**
* Set the surface holder on which to draw.
* Should be called when the surface holder is created or changed
* @param surfaceHolder the surface on which to draw the wallpaper
*/
public void setSurfaceHolder(SurfaceHolder surfaceHolder) {
mSurfaceHolder = surfaceHolder;
}
/**
* Check if a surface holder is loaded
* @return true if a valid surfaceHolder has been set.
*/
public boolean isSurfaceHolderLoaded() {
return mSurfaceHolder != null;
}
/**
* Computes and set the surface dimensions, by using the play and the bitmap dimensions.
* The Bitmap must be loaded before any call to this function
*/
private boolean updateSurfaceSize(Bitmap bitmap) {
int surfaceWidth = Math.max(MIN_SURFACE_WIDTH, bitmap.getWidth());
int surfaceHeight = Math.max(MIN_SURFACE_HEIGHT, bitmap.getHeight());
boolean surfaceChanged =
surfaceWidth != mLastSurfaceWidth || surfaceHeight != mLastSurfaceHeight;
if (surfaceChanged) {
/*
Used a fixed size surface, because we are special. We can do
this because we know the current design of window animations doesn't
cause this to break.
*/
mSurfaceHolder.setFixedSize(surfaceWidth, surfaceHeight);
mLastSurfaceWidth = surfaceWidth;
mLastSurfaceHeight = surfaceHeight;
}
return surfaceChanged;
}
/**
* Draw a the wallpaper on the surface.
* The bitmap and the surface must be loaded before calling
* this function.
* @param forceRedraw redraw the wallpaper even if no changes are detected
*/
public void drawFrame(Bitmap bitmap, boolean forceRedraw) {
if (bitmap == null || bitmap.isRecycled()) {
Log.e(TAG, "Attempt to draw frame before background is loaded:");
return;
}
if (bitmap.getWidth() < 1 || bitmap.getHeight() < 1) {
Log.e(TAG, "Attempt to set an invalid wallpaper of length "
+ bitmap.getWidth() + "x" + bitmap.getHeight());
return;
}
mSurfaceRedrawNeeded |= forceRedraw;
boolean surfaceChanged = updateSurfaceSize(bitmap);
boolean redrawNeeded = surfaceChanged || mSurfaceRedrawNeeded;
mSurfaceRedrawNeeded = false;
if (!redrawNeeded) {
if (DEBUG) {
Log.d(TAG, "Suppressed drawFrame since redraw is not needed ");
}
return;
}
if (DEBUG) {
Log.d(TAG, "Redrawing wallpaper");
}
drawWallpaperWithCanvas(bitmap);
}
@VisibleForTesting
void drawWallpaperWithCanvas(Bitmap bitmap) {
Canvas c = mSurfaceHolder.lockHardwareCanvas();
if (c != null) {
Rect dest = mSurfaceHolder.getSurfaceFrame();
Log.i(TAG, "Redrawing in rect: " + dest + " with surface size: "
+ mLastSurfaceWidth + "x" + mLastSurfaceHeight);
try {
c.drawBitmap(bitmap, null, dest, null);
} finally {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}

View File

@@ -0,0 +1,400 @@
/*
* 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.canvas;
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.Background;
import com.android.systemui.util.Assert;
import com.android.systemui.wallpapers.ImageWallpaper;
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 WallpaperColorExtractor {
private Bitmap mMiniBitmap;
@VisibleForTesting
static final int SMALL_SIDE = 128;
private static final String TAG = WallpaperColorExtractor.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<>();
@Background
private final Executor mBackgroundExecutor;
private final WallpaperColorExtractorCallback mWallpaperColorExtractorCallback;
/**
* Interface to handle the callbacks after the different steps of the color extraction
*/
public interface WallpaperColorExtractorCallback {
/**
* 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 backgroundExecutor the executor on which the color extraction will be performed
* @param wallpaperColorExtractorCallback an interface to handle the callbacks from
* the color extractor.
*/
public WallpaperColorExtractor(@Background Executor backgroundExecutor,
WallpaperColorExtractorCallback wallpaperColorExtractorCallback) {
mBackgroundExecutor = backgroundExecutor;
mWallpaperColorExtractorCallback = wallpaperColorExtractorCallback;
}
/**
* 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) {
mBackgroundExecutor.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) {
mBackgroundExecutor.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);
mWallpaperColorExtractorCallback.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) {
mBackgroundExecutor.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) {
mBackgroundExecutor.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()) {
mWallpaperColorExtractorCallback.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) {
mBackgroundExecutor.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()) {
mWallpaperColorExtractorCallback.onDeactivated();
}
}
}
/**
* Clean up the memory (in particular, the mini bitmap) used by this class.
*/
public void cleanUp() {
mBackgroundExecutor.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("WallpaperColorExtractor#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("WallpaperColorExtractor#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();
mWallpaperColorExtractorCallback.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());
}
}

View File

@@ -16,14 +16,23 @@
package com.android.systemui.wallpapers;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.hamcrest.MockitoHamcrest.intThat;
import android.app.WallpaperManager;
import android.content.Context;
@@ -31,17 +40,25 @@ import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.ColorSpace;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerGlobal;
import android.os.Handler;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.WindowManager;
import android.view.WindowMetrics;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
import com.android.systemui.wallpapers.gl.ImageWallpaperRenderer;
import org.junit.Before;
@@ -56,7 +73,6 @@ import java.util.concurrent.CountDownLatch;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@Ignore
public class ImageWallpaperTest extends SysuiTestCase {
private static final int LOW_BMP_WIDTH = 128;
private static final int LOW_BMP_HEIGHT = 128;
@@ -65,45 +81,87 @@ public class ImageWallpaperTest extends SysuiTestCase {
private static final int DISPLAY_WIDTH = 1920;
private static final int DISPLAY_HEIGHT = 1080;
@Mock
private WindowManager mWindowManager;
@Mock
private WindowMetrics mWindowMetrics;
@Mock
private DisplayManager mDisplayManager;
@Mock
private WallpaperManager mWallpaperManager;
@Mock
private SurfaceHolder mSurfaceHolder;
@Mock
private Surface mSurface;
@Mock
private Context mMockContext;
@Mock
private Bitmap mWallpaperBitmap;
private int mBitmapWidth = 1;
private int mBitmapHeight = 1;
@Mock
private Handler mHandler;
@Mock
private FeatureFlags mFeatureFlags;
FakeSystemClock mFakeSystemClock = new FakeSystemClock();
FakeExecutor mFakeMainExecutor = new FakeExecutor(mFakeSystemClock);
FakeExecutor mFakeBackgroundExecutor = new FakeExecutor(mFakeSystemClock);
private CountDownLatch mEventCountdown;
@Before
public void setUp() throws Exception {
allowTestableLooperAsMainThread();
MockitoAnnotations.initMocks(this);
mEventCountdown = new CountDownLatch(1);
//mEventCountdown = new CountDownLatch(1);
WallpaperManager wallpaperManager = mock(WallpaperManager.class);
// set up window manager
when(mWindowMetrics.getBounds()).thenReturn(
new Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT));
when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
when(mMockContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager);
// set up display manager
doNothing().when(mDisplayManager).registerDisplayListener(any(), any());
when(mMockContext.getSystemService(DisplayManager.class)).thenReturn(mDisplayManager);
// set up bitmap
when(mWallpaperBitmap.getColorSpace()).thenReturn(ColorSpace.get(ColorSpace.Named.SRGB));
when(mWallpaperBitmap.getConfig()).thenReturn(Bitmap.Config.ARGB_8888);
when(mWallpaperBitmap.getWidth()).thenReturn(mBitmapWidth);
when(mWallpaperBitmap.getHeight()).thenReturn(mBitmapHeight);
// set up wallpaper manager
when(mWallpaperManager.peekBitmapDimensions()).thenReturn(
new Rect(0, 0, mBitmapWidth, mBitmapHeight));
when(mWallpaperManager.getBitmap(false)).thenReturn(mWallpaperBitmap);
when(mMockContext.getSystemService(WallpaperManager.class)).thenReturn(mWallpaperManager);
// set up surface
when(mSurfaceHolder.getSurface()).thenReturn(mSurface);
doNothing().when(mSurface).hwuiDestroy();
// TODO remove code below. Outdated, used in only in old GL tests (that are ignored)
Resources resources = mock(Resources.class);
when(mMockContext.getSystemService(WallpaperManager.class)).thenReturn(wallpaperManager);
when(mMockContext.getResources()).thenReturn(resources);
when(resources.getConfiguration()).thenReturn(mock(Configuration.class));
when(mMockContext.getResources()).thenReturn(resources);
DisplayInfo displayInfo = new DisplayInfo();
displayInfo.logicalWidth = DISPLAY_WIDTH;
displayInfo.logicalHeight = DISPLAY_HEIGHT;
when(mMockContext.getDisplay()).thenReturn(
new Display(mock(DisplayManagerGlobal.class), 0, displayInfo, (Resources) null));
}
when(wallpaperManager.getBitmap(false)).thenReturn(mWallpaperBitmap);
when(mWallpaperBitmap.getColorSpace()).thenReturn(ColorSpace.get(ColorSpace.Named.SRGB));
when(mWallpaperBitmap.getConfig()).thenReturn(Bitmap.Config.ARGB_8888);
private void setBitmapDimensions(int bitmapWidth, int bitmapHeight) {
mBitmapWidth = bitmapWidth;
mBitmapHeight = bitmapHeight;
}
private ImageWallpaper createImageWallpaper() {
return new ImageWallpaper(mFeatureFlags) {
return new ImageWallpaper(mFeatureFlags, mFakeBackgroundExecutor, mFakeMainExecutor) {
@Override
public Engine onCreateEngine() {
return new GLEngine(mHandler) {
@@ -130,6 +188,7 @@ public class ImageWallpaperTest extends SysuiTestCase {
}
@Test
@Ignore
public void testBitmapWallpaper_normal() {
// Will use a image wallpaper with dimensions DISPLAY_WIDTH x DISPLAY_WIDTH.
// Then we expect the surface size will be also DISPLAY_WIDTH x DISPLAY_WIDTH.
@@ -140,6 +199,7 @@ public class ImageWallpaperTest extends SysuiTestCase {
}
@Test
@Ignore
public void testBitmapWallpaper_low_resolution() {
// Will use a image wallpaper with dimensions BMP_WIDTH x BMP_HEIGHT.
// Then we expect the surface size will be also BMP_WIDTH x BMP_HEIGHT.
@@ -150,6 +210,7 @@ public class ImageWallpaperTest extends SysuiTestCase {
}
@Test
@Ignore
public void testBitmapWallpaper_too_small() {
// Will use a image wallpaper with dimensions INVALID_BMP_WIDTH x INVALID_BMP_HEIGHT.
// Then we expect the surface size will be also MIN_SURFACE_WIDTH x MIN_SURFACE_HEIGHT.
@@ -166,8 +227,7 @@ public class ImageWallpaperTest extends SysuiTestCase {
ImageWallpaper.GLEngine engineSpy = spy(wallpaperEngine);
when(mWallpaperBitmap.getWidth()).thenReturn(bmpWidth);
when(mWallpaperBitmap.getHeight()).thenReturn(bmpHeight);
setBitmapDimensions(bmpWidth, bmpHeight);
ImageWallpaperRenderer renderer = new ImageWallpaperRenderer(mMockContext);
doReturn(renderer).when(engineSpy).getRendererInstance();
@@ -177,4 +237,116 @@ public class ImageWallpaperTest extends SysuiTestCase {
assertWithMessage("setFixedSizeAllowed should have been called.").that(
mEventCountdown.getCount()).isEqualTo(0);
}
private ImageWallpaper createImageWallpaperCanvas() {
return new ImageWallpaper(mFeatureFlags, mFakeBackgroundExecutor, mFakeMainExecutor) {
@Override
public Engine onCreateEngine() {
return new CanvasEngine() {
@Override
public Context getDisplayContext() {
return mMockContext;
}
@Override
public SurfaceHolder getSurfaceHolder() {
return mSurfaceHolder;
}
@Override
public void setFixedSizeAllowed(boolean allowed) {
super.setFixedSizeAllowed(allowed);
assertWithMessage("mFixedSizeAllowed should be true").that(
allowed).isTrue();
}
};
}
};
}
private ImageWallpaper.CanvasEngine getSpyEngine() {
ImageWallpaper imageWallpaper = createImageWallpaperCanvas();
ImageWallpaper.CanvasEngine engine =
(ImageWallpaper.CanvasEngine) imageWallpaper.onCreateEngine();
ImageWallpaper.CanvasEngine spyEngine = spy(engine);
doNothing().when(spyEngine).drawFrameOnCanvas(any(Bitmap.class));
doNothing().when(spyEngine).reportEngineShown(anyBoolean());
doAnswer(invocation -> {
((ImageWallpaper.CanvasEngine) invocation.getMock()).onMiniBitmapUpdated();
return null;
}).when(spyEngine).recomputeColorExtractorMiniBitmap();
return spyEngine;
}
@Test
public void testMinSurface() {
// test that the surface is always at least MIN_SURFACE_WIDTH x MIN_SURFACE_HEIGHT
testMinSurfaceHelper(8, 8);
testMinSurfaceHelper(100, 2000);
testMinSurfaceHelper(200, 1);
testMinSurfaceHelper(0, 1);
testMinSurfaceHelper(1, 0);
testMinSurfaceHelper(0, 0);
}
private void testMinSurfaceHelper(int bitmapWidth, int bitmapHeight) {
clearInvocations(mSurfaceHolder);
setBitmapDimensions(bitmapWidth, bitmapHeight);
ImageWallpaper imageWallpaper = createImageWallpaperCanvas();
ImageWallpaper.CanvasEngine engine =
(ImageWallpaper.CanvasEngine) imageWallpaper.onCreateEngine();
engine.onCreate(mSurfaceHolder);
verify(mSurfaceHolder, times(1)).setFixedSize(
intThat(greaterThanOrEqualTo(ImageWallpaper.CanvasEngine.MIN_SURFACE_WIDTH)),
intThat(greaterThanOrEqualTo(ImageWallpaper.CanvasEngine.MIN_SURFACE_HEIGHT)));
}
@Test
public void testZeroBitmap() {
// test that a frame is never drawn with a 0 bitmap
testZeroBitmapHelper(0, 1);
testZeroBitmapHelper(1, 0);
testZeroBitmapHelper(0, 0);
}
private void testZeroBitmapHelper(int bitmapWidth, int bitmapHeight) {
clearInvocations(mSurfaceHolder);
setBitmapDimensions(bitmapWidth, bitmapHeight);
ImageWallpaper imageWallpaper = createImageWallpaperCanvas();
ImageWallpaper.CanvasEngine engine =
(ImageWallpaper.CanvasEngine) imageWallpaper.onCreateEngine();
ImageWallpaper.CanvasEngine spyEngine = spy(engine);
spyEngine.onCreate(mSurfaceHolder);
spyEngine.onSurfaceRedrawNeeded(mSurfaceHolder);
verify(spyEngine, never()).drawFrameOnCanvas(any());
}
@Test
public void testLoadDrawAndUnloadBitmap() {
setBitmapDimensions(LOW_BMP_WIDTH, LOW_BMP_HEIGHT);
ImageWallpaper.CanvasEngine spyEngine = getSpyEngine();
spyEngine.onCreate(mSurfaceHolder);
spyEngine.onSurfaceRedrawNeeded(mSurfaceHolder);
assertThat(mFakeBackgroundExecutor.numPending()).isAtLeast(1);
int n = 0;
while (mFakeBackgroundExecutor.numPending() + mFakeMainExecutor.numPending() >= 1) {
n++;
assertThat(n).isAtMost(10);
mFakeBackgroundExecutor.runNextReady();
mFakeMainExecutor.runNextReady();
mFakeSystemClock.advanceTime(1000);
}
verify(spyEngine, times(1)).drawFrameOnCanvas(mWallpaperBitmap);
assertThat(spyEngine.isBitmapLoaded()).isFalse();
}
}

View File

@@ -1,133 +0,0 @@
/*
* 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.canvas;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.hamcrest.MockitoHamcrest.intThat;
import android.graphics.Bitmap;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.DisplayInfo;
import android.view.SurfaceHolder;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class ImageCanvasWallpaperRendererTest extends SysuiTestCase {
private static final int MOBILE_DISPLAY_WIDTH = 720;
private static final int MOBILE_DISPLAY_HEIGHT = 1600;
@Mock
private SurfaceHolder mMockSurfaceHolder;
@Mock
private DisplayInfo mMockDisplayInfo;
@Mock
private Bitmap mMockBitmap;
@Before
public void setUp() throws Exception {
allowTestableLooperAsMainThread();
MockitoAnnotations.initMocks(this);
}
private void setDimensions(
int bitmapWidth, int bitmapHeight,
int displayWidth, int displayHeight) {
when(mMockBitmap.getWidth()).thenReturn(bitmapWidth);
when(mMockBitmap.getHeight()).thenReturn(bitmapHeight);
mMockDisplayInfo.logicalWidth = displayWidth;
mMockDisplayInfo.logicalHeight = displayHeight;
}
private void testMinDimensions(
int bitmapWidth, int bitmapHeight) {
clearInvocations(mMockSurfaceHolder);
setDimensions(bitmapWidth, bitmapHeight,
ImageCanvasWallpaperRendererTest.MOBILE_DISPLAY_WIDTH,
ImageCanvasWallpaperRendererTest.MOBILE_DISPLAY_HEIGHT);
ImageCanvasWallpaperRenderer renderer =
new ImageCanvasWallpaperRenderer(mMockSurfaceHolder);
renderer.drawFrame(mMockBitmap, true);
verify(mMockSurfaceHolder, times(1)).setFixedSize(
intThat(greaterThanOrEqualTo(ImageCanvasWallpaperRenderer.MIN_SURFACE_WIDTH)),
intThat(greaterThanOrEqualTo(ImageCanvasWallpaperRenderer.MIN_SURFACE_HEIGHT)));
}
@Test
public void testMinSurface() {
// test that the surface is always at least MIN_SURFACE_WIDTH x MIN_SURFACE_HEIGHT
testMinDimensions(8, 8);
testMinDimensions(100, 2000);
testMinDimensions(200, 1);
}
private void testZeroDimensions(int bitmapWidth, int bitmapHeight) {
clearInvocations(mMockSurfaceHolder);
setDimensions(bitmapWidth, bitmapHeight,
ImageCanvasWallpaperRendererTest.MOBILE_DISPLAY_WIDTH,
ImageCanvasWallpaperRendererTest.MOBILE_DISPLAY_HEIGHT);
ImageCanvasWallpaperRenderer renderer =
new ImageCanvasWallpaperRenderer(mMockSurfaceHolder);
ImageCanvasWallpaperRenderer spyRenderer = spy(renderer);
spyRenderer.drawFrame(mMockBitmap, true);
verify(mMockSurfaceHolder, never()).setFixedSize(anyInt(), anyInt());
verify(spyRenderer, never()).drawWallpaperWithCanvas(any());
}
@Test
public void testZeroBitmap() {
// test that updateSurfaceSize is not called with a bitmap of width 0 or height 0
testZeroDimensions(
0, 1
);
testZeroDimensions(1, 0
);
testZeroDimensions(0, 0
);
}
}

View File

@@ -0,0 +1,350 @@
/*
* 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.canvas;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.app.WallpaperColors;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.RectF;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class WallpaperColorExtractorTest extends SysuiTestCase {
private static final int LOW_BMP_WIDTH = 128;
private static final int LOW_BMP_HEIGHT = 128;
private static final int HIGH_BMP_WIDTH = 3000;
private static final int HIGH_BMP_HEIGHT = 4000;
private static final int VERY_LOW_BMP_WIDTH = 1;
private static final int VERY_LOW_BMP_HEIGHT = 1;
private static final int DISPLAY_WIDTH = 1920;
private static final int DISPLAY_HEIGHT = 1080;
private static final int PAGES_LOW = 4;
private static final int PAGES_HIGH = 7;
private static final int MIN_AREAS = 4;
private static final int MAX_AREAS = 10;
private int mMiniBitmapWidth;
private int mMiniBitmapHeight;
@Mock
private Executor mBackgroundExecutor;
private int mColorsProcessed;
private int mMiniBitmapUpdatedCount;
private int mActivatedCount;
private int mDeactivatedCount;
@Before
public void setUp() throws Exception {
allowTestableLooperAsMainThread();
MockitoAnnotations.initMocks(this);
doAnswer(invocation -> {
((Runnable) invocation.getArgument(0)).run();
return null;
}).when(mBackgroundExecutor).execute(any(Runnable.class));
}
private void resetCounters() {
mColorsProcessed = 0;
mMiniBitmapUpdatedCount = 0;
mActivatedCount = 0;
mDeactivatedCount = 0;
}
private Bitmap getMockBitmap(int width, int height) {
Bitmap bitmap = mock(Bitmap.class);
when(bitmap.getWidth()).thenReturn(width);
when(bitmap.getHeight()).thenReturn(height);
return bitmap;
}
private WallpaperColorExtractor getSpyWallpaperColorExtractor() {
WallpaperColorExtractor wallpaperColorExtractor = new WallpaperColorExtractor(
mBackgroundExecutor,
new WallpaperColorExtractor.WallpaperColorExtractorCallback() {
@Override
public void onColorsProcessed(List<RectF> regions,
List<WallpaperColors> colors) {
assertThat(regions.size()).isEqualTo(colors.size());
mColorsProcessed += regions.size();
}
@Override
public void onMiniBitmapUpdated() {
mMiniBitmapUpdatedCount++;
}
@Override
public void onActivated() {
mActivatedCount++;
}
@Override
public void onDeactivated() {
mDeactivatedCount++;
}
});
WallpaperColorExtractor spyWallpaperColorExtractor = spy(wallpaperColorExtractor);
doAnswer(invocation -> {
mMiniBitmapWidth = invocation.getArgument(1);
mMiniBitmapHeight = invocation.getArgument(2);
return getMockBitmap(mMiniBitmapWidth, mMiniBitmapHeight);
}).when(spyWallpaperColorExtractor).createMiniBitmap(any(Bitmap.class), anyInt(), anyInt());
doAnswer(invocation -> getMockBitmap(
invocation.getArgument(1),
invocation.getArgument(2)))
.when(spyWallpaperColorExtractor)
.createMiniBitmap(any(Bitmap.class), anyInt(), anyInt());
doReturn(new WallpaperColors(Color.valueOf(0), Color.valueOf(0), Color.valueOf(0)))
.when(spyWallpaperColorExtractor).getLocalWallpaperColors(any(Rect.class));
return spyWallpaperColorExtractor;
}
private RectF randomArea() {
float width = (float) Math.random();
float startX = (float) (Math.random() * (1 - width));
float height = (float) Math.random();
float startY = (float) (Math.random() * (1 - height));
return new RectF(startX, startY, startX + width, startY + height);
}
private List<RectF> listOfRandomAreas(int min, int max) {
int nAreas = randomBetween(min, max);
List<RectF> result = new ArrayList<>();
for (int i = 0; i < nAreas; i++) {
result.add(randomArea());
}
return result;
}
private int randomBetween(int minIncluded, int maxIncluded) {
return (int) (Math.random() * ((maxIncluded - minIncluded) + 1)) + minIncluded;
}
/**
* Test that for bitmaps of random dimensions, the mini bitmap is always created
* with either a width <= SMALL_SIDE or a height <= SMALL_SIDE
*/
@Test
public void testMiniBitmapCreation() {
WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
int nSimulations = 10;
for (int i = 0; i < nSimulations; i++) {
resetCounters();
int width = randomBetween(LOW_BMP_WIDTH, HIGH_BMP_WIDTH);
int height = randomBetween(LOW_BMP_HEIGHT, HIGH_BMP_HEIGHT);
Bitmap bitmap = getMockBitmap(width, height);
spyWallpaperColorExtractor.onBitmapChanged(bitmap);
assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
assertThat(Math.min(mMiniBitmapWidth, mMiniBitmapHeight))
.isAtMost(WallpaperColorExtractor.SMALL_SIDE);
}
}
/**
* Test that for bitmaps with both width and height <= SMALL_SIDE,
* the mini bitmap is always created with both width and height <= SMALL_SIDE
*/
@Test
public void testSmallMiniBitmapCreation() {
WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
int nSimulations = 10;
for (int i = 0; i < nSimulations; i++) {
resetCounters();
int width = randomBetween(VERY_LOW_BMP_WIDTH, LOW_BMP_WIDTH);
int height = randomBetween(VERY_LOW_BMP_HEIGHT, LOW_BMP_HEIGHT);
Bitmap bitmap = getMockBitmap(width, height);
spyWallpaperColorExtractor.onBitmapChanged(bitmap);
assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
assertThat(Math.max(mMiniBitmapWidth, mMiniBitmapHeight))
.isAtMost(WallpaperColorExtractor.SMALL_SIDE);
}
}
/**
* Test that for a new color extractor with information
* (number of pages, display dimensions, wallpaper bitmap) given in random order,
* the colors are processed and all the callbacks are properly executed.
*/
@Test
public void testNewColorExtraction() {
Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
int nSimulations = 10;
for (int i = 0; i < nSimulations; i++) {
resetCounters();
WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
List<RectF> regions = listOfRandomAreas(MIN_AREAS, MAX_AREAS);
int nPages = randomBetween(PAGES_LOW, PAGES_HIGH);
List<Runnable> tasks = Arrays.asList(
() -> spyWallpaperColorExtractor.onPageChanged(nPages),
() -> spyWallpaperColorExtractor.onBitmapChanged(bitmap),
() -> spyWallpaperColorExtractor.setDisplayDimensions(
DISPLAY_WIDTH, DISPLAY_HEIGHT),
() -> spyWallpaperColorExtractor.addLocalColorsAreas(
regions));
Collections.shuffle(tasks);
tasks.forEach(Runnable::run);
assertThat(mActivatedCount).isEqualTo(1);
assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
assertThat(mColorsProcessed).isEqualTo(regions.size());
spyWallpaperColorExtractor.removeLocalColorAreas(regions);
assertThat(mDeactivatedCount).isEqualTo(1);
}
}
/**
* Test that the method removeLocalColorAreas behaves properly and does not call
* the onDeactivated callback unless all color areas are removed.
*/
@Test
public void testRemoveColors() {
Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
int nSimulations = 10;
for (int i = 0; i < nSimulations; i++) {
resetCounters();
WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
List<RectF> regions = new ArrayList<>();
regions.addAll(regions1);
regions.addAll(regions2);
int nPages = randomBetween(PAGES_LOW, PAGES_HIGH);
List<Runnable> tasks = Arrays.asList(
() -> spyWallpaperColorExtractor.onPageChanged(nPages),
() -> spyWallpaperColorExtractor.onBitmapChanged(bitmap),
() -> spyWallpaperColorExtractor.setDisplayDimensions(
DISPLAY_WIDTH, DISPLAY_HEIGHT),
() -> spyWallpaperColorExtractor.removeLocalColorAreas(regions1));
spyWallpaperColorExtractor.addLocalColorsAreas(regions);
assertThat(mActivatedCount).isEqualTo(1);
Collections.shuffle(tasks);
tasks.forEach(Runnable::run);
assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
assertThat(mDeactivatedCount).isEqualTo(0);
spyWallpaperColorExtractor.removeLocalColorAreas(regions2);
assertThat(mDeactivatedCount).isEqualTo(1);
}
}
/**
* Test that if we change some information (wallpaper bitmap, number of pages),
* the colors are correctly recomputed.
* Test that if we remove some color areas in the middle of the process,
* only the remaining areas are recomputed.
*/
@Test
public void testRecomputeColorExtraction() {
Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
List<RectF> regions = new ArrayList<>();
regions.addAll(regions1);
regions.addAll(regions2);
spyWallpaperColorExtractor.addLocalColorsAreas(regions);
assertThat(mActivatedCount).isEqualTo(1);
int nPages = PAGES_LOW;
spyWallpaperColorExtractor.onBitmapChanged(bitmap);
spyWallpaperColorExtractor.onPageChanged(nPages);
spyWallpaperColorExtractor.setDisplayDimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT);
int nSimulations = 20;
for (int i = 0; i < nSimulations; i++) {
resetCounters();
// verify that if we remove some regions, they are not recomputed after other changes
if (i == nSimulations / 2) {
regions.removeAll(regions2);
spyWallpaperColorExtractor.removeLocalColorAreas(regions2);
}
if (Math.random() >= 0.5) {
int nPagesNew = randomBetween(PAGES_LOW, PAGES_HIGH);
if (nPagesNew == nPages) continue;
nPages = nPagesNew;
spyWallpaperColorExtractor.onPageChanged(nPagesNew);
} else {
Bitmap newBitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
spyWallpaperColorExtractor.onBitmapChanged(newBitmap);
assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
}
assertThat(mColorsProcessed).isEqualTo(regions.size());
}
spyWallpaperColorExtractor.removeLocalColorAreas(regions);
assertThat(mDeactivatedCount).isEqualTo(1);
}
@Test
public void testCleanUp() {
resetCounters();
Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
doNothing().when(bitmap).recycle();
WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
spyWallpaperColorExtractor.onPageChanged(PAGES_LOW);
spyWallpaperColorExtractor.onBitmapChanged(bitmap);
assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
spyWallpaperColorExtractor.cleanUp();
spyWallpaperColorExtractor.addLocalColorsAreas(listOfRandomAreas(MIN_AREAS, MAX_AREAS));
assertThat(mColorsProcessed).isEqualTo(0);
}
}